Tiêm phụ thuộc
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 45 trên 107.
Dependency Injection là kỹ thuật trong đó một struct nhận các phần phụ thuộc từ bên ngoài thay vì tự tạo chúng bên trong. Trong Go, interface giúp mẫu này trở nên tự nhiên và mạnh mẽ.
Thay vì cố định một cách triển khai cụ thể bên trong struct, bạn hãy dùng một interface. Nhờ vậy, bạn có thể thay đổi cách triển khai mà không cần sửa code của struct:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Bây giờ, hãy tạo một struct phụ thuộc vào interface, chứ không phải kiểu cụ thể:
type OrderService struct {
notifier Notifier // thành phần phụ thuộc được truyền vào qua giao diện
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
OrderService không biết và cũng không quan tâm nó đang dùng email hay SMS. Bạn truyền phần phụ thuộc vào khi tạo dịch vụ:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Đã đặt hàng: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Đã đặt hàng: Phone
}
Cách tiếp cận này giúp code của bạn linh hoạt và dễ kiểm thử hơn. Khi kiểm thử, bạn có thể đưa vào một notifier giả không thực sự gửi tin nhắn. Khi chạy trong môi trường thực tế, bạn đưa vào phần triển khai thật. Struct vẫn giữ nguyên trong cả hai trường hợp.
Thử thách
DễHãy xây dựng một hệ thống ghi nhật ký để cho thấy sức mạnh của việc tiêm phụ thuộc. Bạn sẽ tạo một dịch vụ có thể ghi nhật ký đến các đích khác nhau mà không cần biết hay quan tâm nhật ký thực sự được gửi đến đâu.
Bạn sẽ sắp xếp code trong ba tệp:
logger.go: Định nghĩa một giao diệnLoggeryêu cầu phương thứcLog(message string) string. Sau đó, tạo hai cách triển khai trình ghi nhật ký khác nhau:ConsoleLoggercó trườngPrefix. Phương thứcLogtrả về[CONSOLE] [Prefix]: [message]FileLoggercó trườngFilename. Phương thứcLogtrả về[FILE:[Filename]] [message]
service.go: Tạo một cấu trúcAppServicephụ thuộc vào giao diệnLogger(không phải một kiểu cụ thể). Thêm hàm khởi tạoNewAppServicenhận mộtLoggervà trả về con trỏ đếnAppService. Thêm choAppServicephương thứcDoWork(task string) string, phương thức này dùng trình ghi nhật ký được tiêm vào để ghi thông báoProcessing: [task]và trả về bất cứ giá trị nào mà trình ghi nhật ký trả về.main.go: Đọc cấu hình từ đầu vào, tạo cả hai kiểu trình ghi nhật ký, tiêm từng kiểu vào một thực thểAppServiceriêng, rồi gọiDoWorktrên mỗi dịch vụ với tác vụ được cung cấp. In kết quả từ mỗi lần gọi dịch vụ.
Đầu vào sẽ được cung cấp như sau:
- Dòng 1: Tiền tố của trình ghi nhật ký console
- Dòng 2: Tên tệp của trình ghi nhật ký file
- Dòng 3: Tác vụ cần xử lý
Ví dụ, với INFO, app.log và user authentication, đầu ra của bạn sẽ là:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationHãy chú ý rằng AppService không biết nó đang ghi nhật ký ra console hay vào tệp. Nó chỉ gọi Log trên trình ghi nhật ký được tiêm vào. Sự linh hoạt này chính là cốt lõi của việc tiêm phụ thuộc: cùng một code dịch vụ có thể hoạt động với các cách triển khai ghi nhật ký hoàn toàn khác nhau.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc tiền tố của bộ ghi nhật ký console
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// Đọc tên tệp của bộ ghi nhật ký tệp
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// Đọc tác vụ cần xử lý
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: Tạo ConsoleLogger với tiền tố
// TODO: Tạo FileLogger với tên tệp
// TODO: Tạo AppService và truyền ConsoleLogger vào
// TODO: Tạo một AppService khác và truyền FileLogger vào
// TODO: Gọi DoWork trên từng dịch vụ với tác vụ rồi in ra kết quả
fmt.Println("result1")
fmt.Println("result2")
}
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