Interface như một hợp đồng
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 27 trên 107.
Xem các interface như những hợp đồng giúp bạn thiết kế chương trình Go tốt hơn. Khi một hàm nhận kiểu interface, điều đó có nghĩa là: "Mình không quan tâm bạn đưa kiểu cụ thể nào, miễn là kiểu đó đáp ứng hợp đồng này."
Cách suy nghĩ dựa trên hợp đồng này giúp bạn viết các hàm tập trung vào hành vi thay vì các kiểu dữ liệu cụ thể. Hãy xem một hàm cần xử lý dữ liệu từ nhiều nguồn khác nhau:
type DataSource interface {
FetchData() string
}
func ProcessData(src DataSource) {
data := src.FetchData()
fmt.Println("Processing:", data)
}Hàm ProcessData không biết và cũng không quan tâm liệu nó nhận được kết nối cơ sở dữ liệu, ứng dụng khách API hay trình đọc tệp. Hàm chỉ yêu cầu đầu vào có thể gọi FetchData(). Bất kỳ kiểu nào tuân theo hợp đồng này đều dùng được.
type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }
type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }
func main() {
db := Database{ConnectionString: "localhost"}
api := APIClient{Endpoint: "https://api.example.com"}
ProcessData(db) // Chạy được!
ProcessData(api) // Cũng chạy được!
}Cách tiếp cận hợp đồng này cũng giúp code của bạn dễ kiểm thử hơn. Khi kiểm thử, bạn có thể tạo một kiểu giả lập (mock) đáp ứng giao diện (interface) mà không cần cơ sở dữ liệu hay kết nối mạng thật. Hàm (function) đang được kiểm thử không nhận ra sự khác biệt vì hợp đồng đã được đáp ứng.
Thử thách
DễHãy xây dựng một hệ thống thông báo để minh họa cách các interface đóng vai trò như những hợp đồng. Bạn sẽ tạo một interface Notifier quy định việc gửi thông báo cần những gì, sau đó triển khai nhiều kênh thông báo cùng tuân theo hợp đồng này.
Bạn sẽ sắp xếp code trong hai tệp:
notifiers.go: Định nghĩa một interfaceNotifiercó một phương thức duy nhất làNotify(message string) string. Sau đó, tạo ba struct khác nhau đáp ứng hợp đồng này:ConsoleNotifiercó trườngPrefixFileNotifiercó trườngFilenameWebhookNotifiercó trườngURL
Notifyđể trả về một chuỗi mô tả cách gửi thông báo.main.go: Tạo một hàm có tênBroadcastAlertnhận một lát cắt gồm các giá trịNotifiervà một chuỗi thông báo. Hàm này cần gọiNotifytrên từng notifier và in ra kết quả. Đọc cấu hình từ đầu vào, tạo cả ba kiểu notifier rồi gửi thông báo qua tất cả các notifier.
Đầu vào sẽ gồm các dòng sau:
- Dòng 1: Tiền tố của Console
- Dòng 2: Tên tệp
- Dòng 3: URL của webhook
- Dòng 4: Nội dung thông báo
Các phương thức Notify cần trả về chuỗi theo những định dạng sau:
- ConsoleNotifier:
[Prefix] [message] - FileNotifier:
Writing to [Filename]: [message] - WebhookNotifier:
POST [URL]: [message]
Ví dụ, với đầu vào ALERT:, log.txt, https://hooks.example.com và Server down!, kết quả của bạn sẽ là:
ALERT: Server down!
Writing to log.txt: Server down!
POST https://hooks.example.com: Server down!Điểm mạnh của thiết kế này là hàm BroadcastAlert không cần biết hoặc quan tâm đến các kiểu notifier cụ thể. Hàm chỉ yêu cầu mỗi notifier tuân thủ hợp đồng Notifier. Sau này, bạn có thể thêm SlackNotifier hoặc EmailNotifier mà không cần thay đổi hàm gửi thông báo.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
)
// BroadcastAlert gửi thông điệp qua tất cả các bộ thông báo
// TODO: Triển khai hàm này để:
// - Nhận một slice các giá trị Notifier và một chuỗi thông điệp
// - Gọi Notify trên từng bộ thông báo và in kết quả
func BroadcastAlert(notifiers []Notifier, message string) {
// TODO: Lặp qua notifiers và in kết quả của từng thông báo
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Đọc tiền tố cho bảng điều khiển
scanner.Scan()
prefix := scanner.Text()
// Đọc tên tệp
scanner.Scan()
filename := scanner.Text()
// Đọc URL của webhook
scanner.Scan()
url := scanner.Text()
// Đọc thông điệp cảnh báo
scanner.Scan()
message := scanner.Text()
// TODO: Tạo instance của cả ba kiểu bộ thông báo
// - ConsoleNotifier với tiền tố
// - FileNotifier với tên tệp
// - WebhookNotifier với URL
// TODO: Tạo một lát cắt gồm cả ba giá trị Notifier
// TODO: Gọi BroadcastAlert với các giá trị thông báo và message
_ = prefix
_ = filename
_ = url
_ = message
fmt.Println("TODO: Implement the solution")
}
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