Triển khai ngầm định
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 26 trên 107.
Trong nhiều ngôn ngữ hướng đối tượng như Java hoặc C#, bạn phải khai báo rõ ràng rằng một kiểu triển khai một giao diện bằng các từ khóa như implements. Go có cách tiếp cận khác: việc triển khai giao diện là ngầm định.
Một kiểu dữ liệu đáp ứng một interface chỉ cần triển khai tất cả phương thức của interface đó. Bạn không cần khai báo mối quan hệ này ở đâu cả. Trình biên dịch tự động xác định điều đó.
type Writer interface {
Write(data string) int
}
type FileWriter struct {
Filename string
}
// FileWriter ngầm triển khai Writer
func (f FileWriter) Write(data string) int {
fmt.Println("Writing to", f.Filename)
return len(data)
}
Lưu ý rằng không có khai báo implements Writer trên FileWriter. Vì FileWriter có phương thức Write(data string) int khớp với chữ ký của giao diện, nên nó tự động thỏa mãn giao diện Writer.
Thiết kế này mang lại một hệ quả mạnh mẽ: bạn có thể định nghĩa giao diện sau khi các kiểu đã tồn tại. Nếu một thư viện bên thứ ba có kiểu với các phương thức bạn cần, bạn có thể tạo một giao diện mà kiểu đó đã đáp ứng mà không cần sửa đổi code gốc.
func Save(w Writer, content string) {
w.Write(content)
}
func main() {
fw := FileWriter{Filename: "data.txt"}
Save(fw, "Hello!") // Chạy được vì FileWriter đáp ứng Writer
}
Việc triển khai ngầm giúp code Go tách rời và linh hoạt. Các kiểu dữ liệu không cần biết trước về các interface, nhờ đó bạn dễ kết hợp những hệ thống từ các thành phần độc lập.
Thử thách
DễHãy xây dựng một hệ thống gửi tin nhắn để minh họa cách các kiểu dữ liệu ngầm định thỏa mãn giao diện trong Go. Bạn sẽ tạo các kiểu người gửi tin nhắn khác nhau cùng thỏa mãn một giao diện, mà không cần khai báo rõ mối quan hệ đó.
Bạn sẽ sắp xếp code trong hai tệp:
messengers.go: Định nghĩa giao diệnMessengervới một phương thức duy nhất làSend(message string) string. Sau đó, tạo hai cấu trúc sẽ ngầm định thỏa mãn giao diện này:EmailMessengercó trườngAddress(chuỗi)SMSMessengercó trườngPhoneNumber(chuỗi)
Sendtrả về một chuỗi mô tả việc gửi tin nhắn. Không cấu trúc nào cần khai báo rõ rằng nó triển khaiMessenger: Go sẽ tự động xác định điều đó dựa trên chữ ký phương thức.main.go: Tạo một hàm có tênDeliverMessagenhận bất kỳMessengernào và một chuỗi tin nhắn, sau đó trả về kết quả gọiSend. Đọc thông tin liên hệ từ đầu vào, tạo cả hai kiểu người gửi tin nhắn và dùng hàm của bạn để gửi tin nhắn qua từng kiểu.
Chương trình sẽ nhận các đầu vào sau:
- Dòng 1: Địa chỉ email
- Dòng 2: Số điện thoại
- Dòng 3: Tin nhắn cần gửi
Các phương thức Send của bạn cần trả về chuỗi theo những định dạng sau:
- EmailMessenger:
Email to [Address]: [message] - SMSMessenger:
SMS to [PhoneNumber]: [message]
Ví dụ, với các giá trị alice@example.com, 555-1234 và Hello!, đầu ra cần là:
Email to alice@example.com: Hello!
SMS to 555-1234: Hello!Điểm mấu chốt là hàm DeliverMessage của bạn nhận bất kỳ Messenger nào, còn cả EmailMessenger và SMSMessenger đều thỏa mãn giao diện đó chỉ bằng cách có một phương thức Send khớp. Không có từ khóa implements nào ở bất cứ đâu: cơ chế triển khai ngầm định của Go xử lý tất cả.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
)
// TODO: Tạo hàm (function) DeliverMessage nhận bất kỳ Messenger nào
// và một chuỗi thông điệp, rồi trả về kết quả khi gọi Send
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc địa chỉ email
email, _ := reader.ReadString('\n')
email = email[:len(email)-1]
// Đọc số điện thoại
phone, _ := reader.ReadString('\n')
phone = phone[:len(phone)-1]
// Đọc thông điệp
message, _ := reader.ReadString('\n')
if len(message) > 0 && message[len(message)-1] == '\n' {
message = message[:len(message)-1]
}
// TODO: Tạo EmailMessenger với địa chỉ email
// TODO: Tạo SMSMessenger với số điện thoại
// TODO: Dùng DeliverMessage để gửi thông điệp qua từng messenger
// và in ra kết quả
}
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