Menu
CoddyTech

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.

challenge icon

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 CPU có String brand và double speedGHz. Thêm phương thức process() trả về một chuỗi: [brand] processing at [speedGHz]GHz
    • Một lớp Memory có int sizeGB. Thêm phương thức load() trả về một chuỗi: Loading [sizeGB]GB of data
  • computer.dart: Tạo lớp Computer dù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 CPU và một đối tượng Memory (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
  • 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 Computer có tên Workstation, với CPU Intel chạy ở tốc độ 3.5 GHz và bộ nhớ 16 GB
    • Gọi phương thức boot()
    • In ra kết quả của specs()

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 RAM

Tự 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()
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch Dart online