Ưu tiên composition hơn kế thừa
Thuộc phần Lập trình hướng đối tượng trong hành trình C# của Coddy. Bài học 41 trên 70.
Kế thừa tạo ra sự phụ thuộc chặt chẽ giữa các lớp. Khi Car kế thừa từ Vehicle, mọi thay đổi đối với Vehicle đều có thể khiến Car gặp lỗi. Kết hợp mang đến một lựa chọn linh hoạt hơn: thay vì kế thừa hành vi, một lớp chứa các đối tượng cung cấp hành vi đó.
Hãy hình dung một nhân vật trong trò chơi có thể di chuyển và tấn công. Với kế thừa (inheritance), bạn có thể tạo ra một hệ thống phân cấp phức tạp. Với cách kết hợp các thành phần (composition), bạn chỉ cần cung cấp cho nhân vật những thành phần cần thiết:
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}Điểm khác biệt chính nằm ở mối quan hệ. Kế thừa biểu thị quan hệ "là một" (Dog là một Animal), còn kết hợp biểu thị quan hệ "có" (Character có hành vi di chuyển). Kết hợp cho phép bạn thay đổi hành vi khi chương trình đang chạy, tự do kết hợp các khả năng và tránh vấn đề lớp cơ sở dễ bị lỗi.
Dùng kế thừa khi có mối quan hệ phân cấp thực sự. Chọn kết hợp khi bạn cần sự linh hoạt, khi các hành vi có thể được chia sẻ giữa những lớp không liên quan hoặc khi bạn muốn thay đổi hành vi một cách linh hoạt.
Thử thách
DễHãy xây dựng một hệ thống thông báo để minh họa sức mạnh của việc kết hợp thành phần thay vì kế thừa. Thay vì tạo một hệ thống phân cấp phức tạp gồm các lớp thông báo, bạn sẽ tạo NotificationSender bằng cách gắn vào đó các thành phần hành vi có thể thay thế cho nhau.
Bạn sẽ tổ chức code trong ba tệp:
Behaviors.cs: Tạo hai lớp hành vi trong không gian tênNotificationsđể xử lý các khía cạnh khác nhau của việc gửi thông báo:FormattingBehavior: có phương thứcFormat(string message)trả về thông báo được bao quanh bởi[ALERT]ở đầu và[END]ở cuốiDeliveryBehavior: có phương thứcDeliver(string channel, string message)trả về chuỗi theo định dạngSending via {channel}: {message}
NotificationSender.cs: Tạo lớpNotificationSendertrong không gian tênNotifications, dùng cách kết hợp thành phần để phối hợp các hành vi. Lớp gửi thông báo của bạn cần:- Có các thực thể của cả
FormattingBehaviorvàDeliveryBehaviordưới dạng các trường riêng tư - Có thuộc tính
Channel(chuỗi) lưu tên kênh gửi - Có hàm khởi tạo nhận tên kênh
- Có phương thức
Send(string message)trước tiên định dạng thông báo bằng hành vi định dạng, sau đó gửi thông báo bằng hành vi gửi và trả về kết quả cuối cùng
- Có các thực thể của cả
Program.cs: Trong tệp chính, hãy tạoNotificationSendervới kênh lấy từ đầu vào, sau đó gửi thông báo lấy từ đầu vào. In kết quả để cho thấy các hành vi được kết hợp hoạt động cùng nhau như thế nào.
Bạn sẽ nhận hai đầu vào:
- Tên kênh gửi, chẳng hạn như
Email,SMS - Thông báo cần gửi
In kết quả khi gọi Send() trên lớp gửi thông báo của bạn.
Ví dụ, nếu đầu vào là Email và Server is down, đầu ra sẽ là:
Sending via Email: [ALERT] Server is down [END]Lưu ý rằng NotificationSender không kế thừa từ lớp nào cả, nó chỉ “có” các hành vi định dạng và gửi thông báo. Cách kết hợp thành phần này giúp bạn dễ dàng thay thế một trong hai hành vi mà không cần thay đổi lớp gửi thông báo, hoặc tái sử dụng các hành vi này trong những lớp hoàn toàn khác!
Tự làm thử
using System;
using Notifications;
class Program
{
public static void Main(string[] args)
{
// Đọc đầu vào
string channel = Console.ReadLine();
string message = Console.ReadLine();
// TODO: Tạo NotificationSender với channel
// TODO: Gửi message và in ra kết quả
}
}
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
1Các khái niệm cơ bản về lập trình hướng đối tượng
Tệp bên ngoàiKhông gian tên và chỉ thịGiới thiệu về lớp và đối tượngTừ khóa thisPhương thức và tham sốTrường và thuộc tínhHàm khởi tạoKhởi tạo đối tượngÔn tập: Máy tính đơn giản4Kế thừa
Cú pháp kế thừa cơ bản (:)Từ khóa 'base'Từ khóa virtual và overrideLớp sealedLớp cơ sở 'object'Ôn tập: hệ phân cấp nhân viên7Tính năng nâng cao
Nạp chồng toán tửBộ lập chỉ mục (this[])Ghi đè ToString()Phương thức mở rộngÔn tập: Danh sách tùy chỉnh10Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếSingleton an toàn với luồngMẫu FactoryMẫu Observer (sự kiện)Mẫu Strategy2Thuộc tính và thành viên tĩnh
Thuộc tính tự độngThuộc tính chỉ đọc hoặc chỉ ghiTrường và phương thức tĩnhLớp tĩnhThành viên có thân biểu thức5Đa hình và interface
Đa hình lúc biên dịch và lúc chạyInterface và lớp trừu tượngNhiều interfaceInterface tường minhUpcasting và downcastingÔn tập: bộ tính toán hình học8Các khái niệm OOP nâng cao
Ưu tiên composition hơn kế thừaGenerics (lớp và phương thức)Delegate và sự kiệnAttribute và reflectionGiao diện IDisposable và câu lệnh usingKiến thức cơ bản về dependency injection11Các mẫu thiết kế phần 2
Mẫu lệnhMẫu bộ chuyển đổiMẫu trang tríMẫu phương thức khuôn mẫuMẫu trạng tháiMẫu tổ hợpTự luyện tập: Trình biên dịch C# online