Vì sao Go không có kế thừa
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 34 trên 107.
Các ngôn ngữ hướng đối tượng truyền thống như Java và C++ dùng tính kế thừa (inheritance) để chia sẻ code giữa các kiểu. Lớp con mở rộng lớp cha và kế thừa tất cả các trường cùng phương thức của lớp đó. Go cố ý không có tính năng này.
Tính kế thừa tạo ra sự liên kết chặt chẽ giữa các kiểu. Khi lớp cha thay đổi, mọi lớp con đều bị ảnh hưởng.
Các hệ thống kế thừa nhiều tầng trở nên khó hiểu và khó bảo trì. “Vấn đề lớp cơ sở dễ bị ảnh hưởng” xảy ra khi những thay đổi trong lớp cơ sở bất ngờ làm hỏng các lớp dẫn xuất.
Các nhà thiết kế Go chọn một hướng đi khác: ưu tiên kết hợp hơn kế thừa. Thay vì nói “Dog là một Animal”, Go khuyến khích bạn nói “Dog có những hành vi giống Animal”. Sự thay đổi tinh tế này giúp code linh hoạt và dễ bảo trì hơn.
Go cho phép tái sử dụng code thông qua hai cơ chế mà bạn đã học:
- Giao diện xác định các hợp đồng về hành vi mà không nêu chi tiết triển khai
- Nhúng struct cho phép các kiểu bao gồm những kiểu khác và tái sử dụng các phương thức của chúng
Hãy xem phép so sánh này. Trong lập trình hướng đối tượng truyền thống, bạn có thể viết class Dog extends Animal. Trong Go, bạn nhúng một cấu trúc Animal vào bên trong Dog và triển khai các interface dùng chung. Kết quả là chức năng tương tự nhưng các kiểu ít phụ thuộc lẫn nhau hơn.
Chương này tìm hiểu sâu về cách nhúng struct (struct embedding), cho thấy Go đạt được lợi ích của kế thừa (inheritance) mà không gặp phải những nhược điểm của nó.
Thử thách
DễHãy xây dựng một hệ thống thông báo thể hiện cách tiếp cận kết hợp của Go thay vì kế thừa. Bạn sẽ tạo các kiểu dùng chung hành vi thông qua giao diện và nhúng struct, thay vì hệ thống phân cấp lớp.
Bạn sẽ sắp xếp code trong ba tệp:
notifier.go: Định nghĩa giao diệnNotifiervới một phương thức duy nhất làNotify(message string) string. Đồng thời tạo structBaseNotifiervới trườngName, để các kiểu khác nhúng vào. Thêm choBaseNotifiermột phương thức tên làFormat(message string) string, phương thức này trả về thông báo có tên của bộ thông báo đặt trong ngoặc vuông ở đầu.channels.go: Tạo hai kiểu kênh thông báo nhúngBaseNotifiervà triển khai giao diệnNotifier:EmailNotifiercó thêm trườngAddressSMSNotifiercó thêm trườngPhone
Notifycủa mỗi kiểu nên dùng phương thứcFormatđược nhúng và đưa thông tin riêng của kênh đó vào đầu ra.main.go: Tạo một hàm tên làSendAlert, nhận bất kỳNotifiernào cùng một thông báo, rồi trả về kết quả gọiNotify. Đọc thông tin thông báo từ đầu vào, tạo cả hai kiểu bộ thông báo và minh họa cách dùng chúng thay thế cho nhau thông qua giao diện.
Đầu vào sẽ gồm các dòng sau:
- Dòng 1: Tên bộ thông báo cho email
- Dòng 2: Địa chỉ email
- Dòng 3: Tên bộ thông báo cho SMS
- Dòng 4: Số điện thoại
- Dòng 5: Thông báo cảnh báo
Phương thức Format của BaseNotifier nên trả về:
[Name] messageCác phương thức Notify nên trả về:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Ví dụ, với các giá trị Alerts, user@mail.com, Urgent, 555-1234 và Server down, đầu ra của bạn sẽ là:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downHãy chú ý cách cả hai kiểu bộ thông báo dùng lại phương thức Format từ BaseNotifier thông qua nhúng, trong khi mỗi kiểu có cách triển khai Notify riêng. Hàm SendAlert hoạt động với bất kỳ Notifier nào mà không cần biết kiểu cụ thể. Đây là cách kết hợp thay cho kế thừa.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert nhận một Notifier bất kỳ và một thông điệp, rồi trả về kết quả khi gọi Notify
// TODO: Triển khai hàm (function) SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Đọc tên bộ thông báo email
scanner.Scan()
emailName := scanner.Text()
// Đọc địa chỉ email
scanner.Scan()
emailAddress := scanner.Text()
// Đọc tên bộ thông báo SMS
scanner.Scan()
smsName := scanner.Text()
// Đọc số điện thoại
scanner.Scan()
phoneNumber := scanner.Text()
// Đọc thông điệp cảnh báo
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Tạo EmailNotifier với emailName và emailAddress
// TODO: Tạo SMSNotifier với smsName và phoneNumber
// TODO: Dùng SendAlert để gửi alertMessage qua cả hai bộ thông báo
// và in ra kết quả
// Ẩn cảnh báo về các biến chưa dùng (xóa các dòng này khi bạn dùng các biến đó)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
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