Quy tắc thỏa mãn interface
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 43 trên 107.
Dù Go dùng kiểu định kiểu vịt để ngầm đáp ứng giao diện, vẫn có những quy tắc cụ thể xác định một kiểu có thực sự đáp ứng giao diện hay không. Hiểu rõ các quy tắc này giúp bạn tránh những bug khó nhận ra.
Quy tắc quan trọng nhất liên quan đến bộ nhận con trỏ (pointer receiver). Nếu một phương thức được định nghĩa với bộ nhận con trỏ, chỉ con trỏ đến kiểu đó mới đáp ứng giao diện, chứ không phải bản thân giá trị:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // bộ nhận kiểu con trỏ
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Vì Save() có receiver là con trỏ, chỉ *Document mới thỏa mãn Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // Chạy được: con trỏ đáp ứng giao diện (interface)
// Process(doc) // lỗi biên dịch: giá trị không đáp ứng giao diện
}
Tuy nhiên, cách ngược lại linh hoạt hơn. Nếu một phương thức có bộ nhận giá trị, cả kiểu giá trị lẫn kiểu con trỏ đều thỏa mãn giao diện. Go tự động bỏ tham chiếu con trỏ khi gọi các phương thức có bộ nhận giá trị:
func (d Document) Info() string { // receiver giá trị
return d.Name
}
// Cả Document và *Document đều đáp ứng interface yêu cầu Info()
Sự bất đối xứng này tồn tại vì Go luôn có thể lấy giá trị từ một con trỏ (bằng cách giải tham chiếu), nhưng không phải lúc nào cũng lấy được con trỏ từ một giá trị (giá trị đó có thể không thể lấy địa chỉ). Ghi nhớ quy tắc này giúp bạn tránh gặp lỗi trình biên dịch khó hiểu khi làm việc với các interface.
Thử thách
DễHãy xây dựng một hệ thống cấu hình để minh họa cách bộ nhận giá trị (value receiver) và bộ nhận con trỏ (pointer receiver) ảnh hưởng đến việc một kiểu dữ liệu có thỏa mãn giao diện (interface) hay không. Bạn sẽ tạo các kiểu mà lựa chọn bộ nhận quyết định liệu giá trị, con trỏ hay cả hai có thể dùng với một giao diện hay không.
Bạn sẽ sắp xếp code trong ba tệp:
config.go: Định nghĩa giao diệnConfigurable, yêu cầu hai phương thức:GetValue() stringvàSetValue(string). Sau đó tạo hai kiểu cấu hình:ReadOnlyConfigcó trườngValue: dùng bộ nhận giá trị choGetValue()(trả về Value) và bộ nhận con trỏ choSetValue()(cập nhật Value)Settingcó trườngData: dùng bộ nhận giá trị cho cả hai phương thức (GetValuetrả về Data,SetValuechỉ in ra "Cannot modify" mà không thay đổi gì)
processor.go: Tạo hàm có tênProcessConfig, nhận mộtConfigurablevà một chuỗi giá trị mới. Hàm này sẽ in ra giá trị hiện tại bằngGetValue(), gọiSetValue()với giá trị mới, rồi in lại giá trị để cho thấy mọi thay đổi.main.go: Đọc thông tin cấu hình từ đầu vào và minh họa các quy tắc thỏa mãn giao diện:- Tạo một
ReadOnlyConfigvà truyền một con trỏ vàoProcessConfig(cần thiết vìSetValuecó bộ nhận con trỏ) - Tạo một
Settingvà truyền trực tiếp giá trị vàoProcessConfig(hoạt động vì cả hai phương thức đều có bộ nhận giá trị)
- Tạo một
Đầu vào sẽ gồm:
- Dòng 1: Giá trị ban đầu của ReadOnlyConfig
- Dòng 2: Giá trị mới cần đặt cho ReadOnlyConfig
- Dòng 3: Giá trị ban đầu của Setting
- Dòng 4: Giá trị mới định đặt cho Setting
Hàm ProcessConfig của bạn cần in ra theo định dạng sau:
Current: [value]
Current: [value after SetValue]Ví dụ, với các giá trị debug, production, localhost và remote, đầu ra sẽ là:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostLưu ý rằng giá trị của ReadOnlyConfig thực sự thay đổi (vì ta đã truyền một con trỏ), còn Setting vẫn giữ nguyên (phương thức SetValue có bộ nhận giá trị không thể sửa đổi giá trị ban đầu). Điểm mấu chốt là chỉ riêng các giá trị ReadOnlyConfig không thỏa mãn Configurable, chỉ các con trỏ mới thỏa mãn, còn các giá trị Setting có thể dùng trực tiếp vì mọi phương thức của nó đều dùng bộ nhận giá trị.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Đọc các đầu vào
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: Tạo ReadOnlyConfig với giá trị rocInitial
// TODO: Truyền CON TRỎ vào ProcessConfig (cần thiết vì SetValue có bộ nhận là con trỏ)
// TODO: Tạo Setting với giá trị settingInitial
// TODO: Truyền trực tiếp GIÁ TRỊ vào ProcessConfig (vì cả hai phương thức đều có bộ nhận là giá trị)
// Dùng các biến này để tránh lỗi biến không được sử dụng
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
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