Đa hình qua 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 41 trên 107.
Tính đa hình cho phép xử lý các kiểu khác nhau một cách thống nhất thông qua một giao diện chung. Trong Go, điều này hoàn toàn được thực hiện thông qua các giao diện, không cần kế thừa hay hệ thống phân cấp lớp.
Khi một hàm nhận kiểu giao diện làm tham số, bạn có thể truyền vào bất kỳ kiểu cụ thể nào triển khai giao diện đó. Hàm không cần biết kiểu cụ thể là gì. Hàm chỉ quan tâm đến hành vi mà giao diện định nghĩa:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
Giờ đây, MakeSound hoạt động với mọi kiểu có phương thức Speak():
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // Gâu!
MakeSound(cat) // Meo!
}
Cùng một lời gọi hàm sẽ tạo ra hành vi khác nhau tùy thuộc vào kiểu thực tế được truyền vào. Đây là tính đa hình đang hoạt động. Hàm MakeSound chỉ cần viết một lần nhưng có thể hoạt động với vô số kiểu, miễn là chúng đáp ứng giao diện Speaker.
Cách tiếp cận này giúp code của bạn linh hoạt và dễ mở rộng. Để thêm một kiểu mới có thể phát ra âm thanh, bạn chỉ cần triển khai giao diện, không cần sửa các hàm hiện có, và mọi thứ sẽ tự động hoạt động.
Thử thách
DễHãy xây dựng một hệ thống mô tả phương tiện để minh họa cách tính đa hình hoạt động. Bạn sẽ tạo nhiều loại phương tiện có chung một hành vi thông qua một interface, sau đó viết một hàm duy nhất có thể làm việc với mọi phương tiện.
Bạn sẽ sắp xếp code trong hai tệp:
vehicles.go: Định nghĩa interfaceDescriber, yêu cầu có phương thứcDescribe() string. Sau đó tạo ba loại phương tiện, mỗi loại triển khai interface này theo cách riêng:Carcó các trườngBrandvàModel. Phương thứcDescribe()trả vềCar: [Brand] [Model]Motorcyclecó các trườngBrandvàEngineCC(int). Phương thứcDescribe()trả vềMotorcycle: [Brand] [EngineCC]ccBicyclecó trườngType, chẳng hạn như "Mountain" hoặc "Road". Phương thứcDescribe()trả vềBicycle: [Type]
main.go: Tạo một hàm tên làPrintDescription, nhận bất kỳDescribernào và in ra kết quả khi gọiDescribe(). Đọc thông tin phương tiện từ đầu vào, tạo một phương tiện thuộc mỗi loại, rồi truyền từng phương tiện vàoPrintDescriptionđể minh họa rằng cùng một hàm có thể làm việc với cả ba loại khác nhau.
Dữ liệu đầu vào sẽ gồm:
- Dòng 1: Hãng xe
- Dòng 2: Mẫu xe
- Dòng 3: Hãng xe máy
- Dòng 4: Dung tích động cơ xe máy tính bằng CC (số nguyên)
- Dòng 5: Loại xe đạp
Ví dụ, với các giá trị Toyota, Camry, Honda, 600 và Mountain, đầu ra sẽ là:
Car: Toyota Camry
Motorcycle: Honda 600cc
Bicycle: MountainLưu ý rằng PrintDescription không cần biết mình nhận một Car, Motorcycle hay Bicycle. Hàm chỉ cần gọi Describe(), rồi mỗi loại sẽ trả về đầu ra riêng. Đó là tính đa hình: một hàm, nhiều hành vi.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// TODO: Tạo hàm (function) PrintDescription nhận bất kỳ Describer nào
// và in ra kết quả khi gọi Describe()
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc thông tin ô tô
carBrand, _ := reader.ReadString('\n')
carBrand = strings.TrimSpace(carBrand)
carModel, _ := reader.ReadString('\n')
carModel = strings.TrimSpace(carModel)
// Đọc thông tin xe máy
motoBrand, _ := reader.ReadString('\n')
motoBrand = strings.TrimSpace(motoBrand)
motoEngineStr, _ := reader.ReadString('\n')
motoEngineStr = strings.TrimSpace(motoEngineStr)
motoEngine, _ := strconv.Atoi(motoEngineStr)
// Đọc thông tin xe đạp
bicycleType, _ := reader.ReadString('\n')
bicycleType = strings.TrimSpace(bicycleType)
// TODO: Tạo Car, Motorcycle và Bicycle bằng các giá trị đầu vào
// TODO: Gọi PrintDescription cho từng phương tiện để minh họa tính đa hình
fmt.Println("TODO: Print vehicle descriptions")
}
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