Giới thiệu về mẫu thiết kế
Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 85 trên 107.
Mẫu thiết kế là những giải pháp có thể tái sử dụng cho các vấn đề thường gặp trong thiết kế phần mềm. Chúng không phải là code để bạn sao chép trực tiếp, mà là những khuôn mẫu hướng dẫn cách bạn cấu trúc các kiểu dữ liệu và cách chúng tương tác để giải quyết những thử thách cụ thể.
Các mẫu thiết kế thường được chia thành ba nhóm:
| Nhóm | Mục đích | Ví dụ |
|---|---|---|
| Khởi tạo | Cơ chế tạo đối tượng | Singleton, Factory, Builder |
| Cấu trúc | Kết hợp các kiểu thành cấu trúc lớn hơn | Adapter, Decorator, Composite |
| Hành vi | Giao tiếp giữa các đối tượng | Observer, Strategy, Command |
Trong Go, các mẫu thiết kế trông khác với các ngôn ngữ OOP truyền thống.
Không dùng lớp (class) hay kế thừa (inheritance), Go triển khai các mẫu bằng giao diện (interface), nhúng struct và phép hợp thành. Ví dụ, Java có thể dùng lớp trừu tượng, còn Go dùng giao diện. C++ có thể dùng kế thừa, còn Go dùng cách nhúng.
Học các mẫu thiết kế giúp bạn nhận ra những vấn đề thường gặp và trao đổi giải pháp hiệu quả với các nhà phát triển khác. Khi ai đó nói "hãy dùng mẫu Strategy ở đây", cả nhóm sẽ hiểu cách tiếp cận mà không cần giải thích dài dòng.
Trong các bài học tiếp theo, bạn sẽ triển khai một số mẫu thiết kế cơ bản trong Go, bắt đầu với các mẫu khởi tạo như Singleton và Factory, sau đó chuyển sang các mẫu hành vi như Observer và Strategy.
Thử thách
DễHãy xây dựng một hệ thống thông báo đơn giản để tìm hiểu cách các mẫu thiết kế giúp tổ chức code! Bạn sẽ tạo một cấu trúc có thể dễ dàng mở rộng bằng các mẫu như Observer hoặc Strategy trong những bài học sau. Còn bây giờ, hãy tập trung thiết lập các giao diện và kiểu dữ liệu rõ ràng, tuân theo những nguyên tắc lập trình hướng đối tượng tốt.
Bạn sẽ tổ chức code trong ba tệp:
notifier.go: Định nghĩa giao diện cốt lõi và các kiểu dữ liệu cho hệ thống thông báo.Tạo một giao diện
Notifiervới một phương thức duy nhất làSend(message string) string, trả về một thông báo xác nhận.Triển khai hai kiểu thông báo cụ thể:
EmailNotifiercó trườngAddress. Phương thứcSendcủa kiểu này trả vềEmail to [address]: [message]SMSNotifiercó trườngPhoneNumber. Phương thứcSendcủa kiểu này trả vềSMS to [phone]: [message]
dispatcher.go: Tạo một bộ điều phối có thể làm việc với mọi kiểu thông báo thông qua giao diện.Xây dựng một struct
NotificationDispatcherchứa một lát cắt các giao diệnNotifier. Thêm các phương thức sau:AddNotifier(n Notifier)để đăng ký một kiểu thông báoBroadcast(message string) []stringđể gửi thông báo qua tất cả các kiểu thông báo đã đăng ký và trả về một lát cắt chứa tất cả các chuỗi xác nhận
Đồng thời tạo hàm khởi tạo
NewNotificationDispatcher()để trả về một bộ điều phối đã được khởi tạo.main.go: Thiết lập các kiểu thông báo và phát thông báo.Đọc số lượng kiểu thông báo cần tạo. Với mỗi kiểu thông báo, đọc loại (
emailhoặcsms) và địa chỉ nhận (địa chỉ email hoặc số điện thoại). Thêm từng kiểu vào bộ điều phối.Sau đó, đọc nội dung thông báo cần phát. Gửi thông báo qua tất cả các kiểu thông báo và in từng xác nhận trên một dòng riêng.
Đầu vào sẽ có dạng sau:
- Dòng 1: Số lượng kiểu thông báo
- Với mỗi kiểu thông báo: loại (
emailhoặcsms), sau đó là địa chỉ nhận - Dòng cuối: Nội dung thông báo cần phát
Ví dụ, với đầu vào:
3
email
alice@example.com
sms
555-1234
email
bob@example.com
Server maintenance at midnightĐầu ra sẽ là:
Email to alice@example.com: Server maintenance at midnight
SMS to 555-1234: Server maintenance at midnight
Email to bob@example.com: Server maintenance at midnightVới đầu vào:
2
sms
555-9999
sms
555-0000
Meeting canceledĐầu ra sẽ là:
SMS to 555-9999: Meeting canceled
SMS to 555-0000: Meeting canceledCấu trúc này cho thấy giao diện giúp thiết kế linh hoạt và dễ mở rộng như thế nào. Bộ điều phối không cần biết các kiểu thông báo cụ thể. Nó chỉ cần biết chúng có thể Send. Đây là nền tảng để xây dựng các mẫu như Observer và Strategy!
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc số lượng bộ thông báo
countStr, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countStr))
// Tạo bộ điều phối mới
dispatcher := NewNotificationDispatcher()
// Đọc từng bộ thông báo và thêm vào bộ điều phối
for i := 0; i < count; i++ {
notifierType, _ := reader.ReadString('\n')
notifierType = strings.TrimSpace(notifierType)
destination, _ := reader.ReadString('\n')
destination = strings.TrimSpace(destination)
// TODO: Tạo bộ thông báo phù hợp dựa trên loại
// và thêm vào bộ điều phối
// Gợi ý: Kiểm tra xem notifierType là "email" hay "sms"
}
// Đọc thông điệp cần gửi đến tất cả
message, _ := reader.ReadString('\n')
message = strings.TrimSpace(message)
// TODO: Gửi thông điệp đến tất cả và in ra từng xác nhận
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online