Composition và kế thừa
Thuộc phần Lập trình hướng đối tượng trong hành trình Dart của Coddy. Bài học 82 trên 110.
Bạn đã học rằng kế thừa tạo ra mối quan hệ "là một", Dog là một Animal. Nhưng còn một cách tiếp cận mạnh mẽ khác: tổ hợp, tạo ra mối quan hệ "có một". Thay vì kế thừa hành vi, một lớp chứa các instance của những lớp khác và giao việc cho chúng.
Hãy xem xét lớp Car. Với tính kế thừa, bạn có thể thử mở rộng lớp Engine, nhưng ô tô không phải là động cơ, mà nó có một động cơ:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Quan hệ hợp thành (composition): Car có một Engine
void start() {
_engine.start(); // Giao cho động cơ xử lý
print('Car is ready to drive');
}
}
void main() {
var car = Car();
car.start();
}Ưu điểm chính của cách kết hợp là tính linh hoạt. Với kế thừa, bạn bị ràng buộc vào một lớp cha duy nhất. Với cách kết hợp, bạn có thể ghép nhiều thành phần và thậm chí thay đổi chúng khi chương trình đang chạy:
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}Một nguyên tắc phổ biến là "ưu tiên composition hơn inheritance". Hãy dùng inheritance khi thực sự có mối quan hệ "là một" và bạn cần tính đa hình. Hãy dùng composition khi bạn muốn tái sử dụng hành vi mà không tạo liên kết chặt chẽ, hoặc khi một đối tượng cần các khả năng từ nhiều nguồn.
Thử thách
DễHãy xây dựng một hệ thống máy tính bằng cách kết hợp (composition)! Thay vì cố gắng để lớp máy tính kế thừa từ các bộ phận của nó, bạn sẽ tạo các lớp thành phần riêng biệt rồi kết hợp chúng lại với nhau. Máy tính có CPU và có bộ nhớ, chứ không phải là CPU hay bộ nhớ.
Bạn sẽ sắp xếp code vào ba tệp:
components.dart: Tạo hai lớp thành phần đại diện cho các bộ phận của máy tính:- Một lớp
CPUcóString brandvàdouble speedGHz. Thêm phương thứcprocess()trả về một chuỗi:[brand] processing at [speedGHz]GHz - Một lớp
Memorycóint sizeGB. Thêm phương thứcload()trả về một chuỗi:Loading [sizeGB]GB of data
- Một lớp
computer.dart: Tạo lớpComputerdùng phép kết hợp để gộp các thành phần lại với nhau. Máy tính của bạn cần:- Có trường
String name - Chứa một đối tượng
CPUvà một đối tượngMemory(phép kết hợp: máy tính CÓ các bộ phận này) - Nhận mọi giá trị cần thiết qua hàm khởi tạo để tự tạo cả hai thành phần bên trong
- Có phương thức
boot()gọi đến cả hai thành phần và in ra ba dòng: tên máy tính theo sau làstarting..., rồi đến kết quả của phương thức process của CPU, sau đó là kết quả của phương thức load của Memory - Có phương thức
specs()trả về một chuỗi mô tả hệ thống:[name]: [brand] [speedGHz]GHz, [sizeGB]GB RAM
- Có trường
main.dart: Nhập các tệp của bạn và minh họa cách phép kết hợp hoạt động:- Tạo một
Computercó tênWorkstation, với CPUIntelchạy ở tốc độ3.5GHz và bộ nhớ16GB - Gọi phương thức
boot() - In ra kết quả của
specs()
- Tạo một
Lưu ý lớp Computer không mở rộng CPU hay Memory. Lớp này chứa chúng và giao việc cho chúng thực hiện. Đây là mối quan hệ “có” mà phép kết hợp tạo ra!
Kết quả mong đợi:
Workstation starting...
Intel processing at 3.5GHz
Loading 16GB of data
Workstation: Intel 3.5GHz, 16GB RAMTự làm thử
import 'computer.dart';
void main() {
// TODO: Tạo một Computer tên "Workstation" với:
// - CPU Intel chạy ở tốc độ 3.5 GHz
// - Bộ nhớ 16 GB
// TODO: Gọi phương thức boot()
// TODO: In ra kết quả của specs()
}
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 về lập trình hướng đối tượng
Tệp bên ngoàiThư viện và câu lệnh importGiới thiệu về lập trình hướng đối tượngLớp và đối tượngTừ khóa thisPhương thứcBiến thể hiệnKiến thức cơ bản về hàm khởi tạoÔn tập: Máy tính đơn giản4An toàn với giá trị null
Giới thiệu về an toàn với giá trị nullCó thể null và không thể nullToán tử ? và !Từ khóa late và an toàn với giá trị nullToán tử xử lý nullAn toàn với giá trị null trong lớpÔn tập: hệ thống hồ sơ người dùng7Lớp trừu tượng và interface
Lớp trừu tượngPhương thức trừu tượngInterface trong DartInterface ngầm địnhTriển khai và kế thừaNhiều interfaceÔn tập: Máy tính hình10Bộ sưu tập và kiểu tổng quát
Tổng quan về List, Set và MapBộ sưu tập an toàn về kiểuLớp tổng quátPhương thức tổng quátRàng buộc kiểu tổng quátIterable và IteratorÔn tập: Lưu trữ tổng quát13Khái niệm OOP nâng cao
Composition và kế thừaPhương thức mở rộngLớp có thể gọiLớp sealed (Dart 3)Records (Dart 3)Mẫu và đối sánh (3.0)Enum có phương thức2Hàm khởi tạo trong Dart
Hàm khởi tạo mặc địnhHàm khởi tạo có tênDanh sách khởi tạoHàm khởi tạo hằng sốHàm khởi tạo nhà máyHàm khởi tạo chuyển hướngÔn tập: Trình tạo hình dạng5Đóng gói
Thành viên công khai và riêng tưQuy ước tiền tố _Tính riêng tư ở cấp thư việnTìm hiểu sâu về getter và setterChe giấu thông tinÔn tập: hồ sơ học sinh8Mixin
Giới thiệu về MixinTạo MixinSử dụng nhiều MixinTừ khóa on trong MixinMixin và kế thừaMixin và giao diệnÔn tập: hệ thống động vật3Thuộc tính của lớp
Thành viên instance và thành viên tĩnhTrường final và constBiến latePhương thức và trường tĩnhGetter và setterÔn tập: trình quản lý tài khoản ngân hàng6Kế thừa
Kế thừa cơ bảnTừ khóa superGhi đè phương thứcChú thích @overrideTừ khóa lớp finalHàm khởi tạo và kế thừaÔn tập: phân cấp nhân viên9Đa hình
Khái niệm cơ bản về đa hìnhĐa hình qua giao diệnKiểm tra kiểu lúc chạyToán tử is và asTừ khóa hiệp biếnÔn tập: Bộ xử lý thanh toánTự luyện tập: Trình biên dịch Dart online