Kế thừa và Composition: Chiến lược Tái sử dụng Code trong OOP

Giới thiệu
Tái sử dụng code là một trong những mục tiêu cơ bản của lập trình hướng đối tượng. Không ai muốn viết cùng một code hai lần. Nhưng cách bạn đạt được tái sử dụng code ảnh hưởng rất lớn đến khả năng bảo trì và linh hoạt của phần mềm.
Trong bài viết này, chúng ta sẽ khám phá hai cơ chế chính để tái sử dụng code trong OOP: kế thừa (inheritance) và composition. Bạn sẽ học khi nào mỗi phương pháp phù hợp, hiểu nguyên tắc nổi tiếng "ưu tiên composition hơn kế thừa," và cách áp dụng trong các tình huống thực tế.
Bạn Sẽ Học Được
✅ Hiểu kế thừa và quan hệ "is-a"
✅ Nắm vững ghi đè phương thức và từ khóa super
✅ Học composition và quan hệ "has-a"
✅ Hiểu tại sao nên ưu tiên composition hơn kế thừa
✅ Nhận biết các bẫy kế thừa và cách tránh
✅ Áp dụng mixins và multiple inheritance
Điều kiện tiên quyết
- Hoàn thành Bốn Trụ Cột của OOP
- Hoàn thành Classes, Objects và Abstraction
- Hoàn thành Encapsulation và Information Hiding
Hiểu về Kế thừa (Inheritance)
Kế thừa là cơ chế tạo class mới (class con/subclass) từ class có sẵn (class cha/superclass), kế thừa các thuộc tính và phương thức của nó.
Quan hệ "Is-A"
Kế thừa mô hình hóa quan hệ "is-a" (là một):
Dogis-aAnimal(Chó là một Động vật)Manageris-aEmployee(Quản lý là một Nhân viên)Caris-aVehicle(Xe hơi là một Phương tiện)
// TypeScript
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): void {
console.log(`${this.name} makes a sound`);
}
}
// Dog IS-A Animal
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name); // Gọi constructor của class cha
this.breed = breed;
}
}
const dog = new Dog("Rex", "German Shepherd");
dog.speak(); // "Rex makes a sound" (phương thức kế thừa)
console.log(dog.breed); // "German Shepherd"# Python
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> None:
print(f"{self.name} makes a sound")
# Dog IS-A Animal
class Dog(Animal):
def __init__(self, name: str, breed: str):
super().__init__(name) # Gọi constructor của class cha
self.breed = breed
dog = Dog("Rex", "German Shepherd")
dog.speak() # "Rex makes a sound"Ghi đè Phương thức (Method Overriding)
Class con có thể ghi đè phương thức của class cha để cung cấp hành vi chuyên biệt:
// TypeScript
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): void {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
speak(): void {
console.log(`${this.name} barks: Woof!`);
}
}
class Cat extends Animal {
speak(): void {
console.log(`${this.name} meows: Meow!`);
}
}
// Đa hình trong thực tế
const animals: Animal[] = [
new Dog("Rex"),
new Cat("Whiskers"),
new Animal("Unknown")
];
animals.forEach(animal => animal.speak());
// Output:
// Rex barks: Woof!
// Whiskers meows: Meow!
// Unknown makes a soundTừ khóa super
Từ khóa super cho phép truy cập các thành viên của class cha từ class con:
// TypeScript
class Employee {
name: string;
salary: number;
constructor(name: string, salary: number) {
this.name = name;
this.salary = salary;
}
calculatePay(): number {
return this.salary;
}
}
class Manager extends Employee {
private bonus: number;
constructor(name: string, salary: number, bonus: number) {
super(name, salary); // PHẢI gọi super() trước
this.bonus = bonus;
}
calculatePay(): number {
return super.calculatePay() + this.bonus; // Gọi phương thức cha
}
}
const manager = new Manager("Alice", 80000, 10000);
console.log(manager.calculatePay()); // 90000Hiểu về Composition
Composition là nguyên tắc thiết kế xây dựng đối tượng phức tạp từ các đối tượng đơn giản hơn. Thay vì kế thừa hành vi, bạn bao gồm (compose) các đối tượng cung cấp hành vi đó.
Quan hệ "Has-A"
Composition mô hình hóa quan hệ "has-a" (có một):
Carhas-aEngine(Xe hơi có một Động cơ)Computerhas-aProcessor(Máy tính có một Bộ xử lý)Teamhas-a list ofPlayers(Đội có danh sách Cầu thủ)
// TypeScript - Ví dụ Composition
class Engine {
private horsepower: number;
constructor(horsepower: number) {
this.horsepower = horsepower;
}
start(): void {
console.log(`Engine with ${this.horsepower}hp started`);
}
stop(): void {
console.log("Engine stopped");
}
}
class Transmission {
private gears: number;
private currentGear: number = 0;
constructor(gears: number) {
this.gears = gears;
}
shiftUp(): void {
if (this.currentGear < this.gears) {
this.currentGear++;
console.log(`Shifted to gear ${this.currentGear}`);
}
}
}
// Car HAS-A Engine và HAS-A Transmission
class Car {
private engine: Engine;
private transmission: Transmission;
private brand: string;
constructor(brand: string, horsepower: number, gears: number) {
this.brand = brand;
this.engine = new Engine(horsepower);
this.transmission = new Transmission(gears);
}
start(): void {
console.log(`Starting ${this.brand}...`);
this.engine.start();
}
drive(): void {
this.transmission.shiftUp();
}
}
const car = new Car("Toyota", 200, 6);
car.start(); // Starting Toyota... Engine with 200hp started
car.drive(); // Shifted to gear 1Kế thừa vs Composition: Khi nào Dùng Cái Nào
Bảng So sánh
| Khía cạnh | Kế thừa | Composition |
|---|---|---|
| Quan hệ | "Is-a" (là một) | "Has-a" (có một) |
| Coupling | Chặt (tight) | Lỏng (loose) |
| Linh hoạt | Thấp (cố định lúc compile) | Cao (có thể thay đổi runtime) |
| Tái sử dụng | Tất cả hoặc không | Chọn những gì cần |
| Testing | Khó hơn | Dễ hơn (mock được) |
| Thay đổi hành vi | Cần subclass mới | Thay đổi component |
Dùng Kế thừa Khi:
- Có quan hệ "is-a" rõ ràng và ít thay đổi
- Cần đa hình (polymorphism)
- Class cha ổn định
- Muốn mở rộng hành vi
Dùng Composition Khi:
- Có quan hệ "has-a"
- Cần linh hoạt - thay đổi hành vi runtime
- Muốn kết hợp nhiều hành vi
- Quan hệ có thể thay đổi theo thời gian
Ưu tiên Composition hơn Kế thừa
Đây là nguyên tắc nổi tiếng từ sách "Design Patterns" của Gang of Four.
Tại sao Ưu tiên Composition?
1. Tránh Coupling Chặt
// Vấn đề: Coupling chặt với kế thừa
class Stack<T> extends Array<T> {
// Stack kế thừa TẤT CẢ phương thức của Array
}
const stack = new Stack<number>();
stack.push(1);
stack.splice(0, 1); // Oops! Điều này phá vỡ quy tắc LIFO!
stack.reverse(); // Điều này cũng vậy!// Giải pháp: Composition bảo vệ encapsulation
class Stack<T> {
private items: T[] = []; // Compose với Array
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
// Chỉ expose những gì Stack cần - không có splice, reverse, etc.
}2. Cho phép Linh hoạt Runtime
// Payment processor có thể thay đổi chiến lược runtime
interface PaymentProcessor {
process(amount: number): void;
}
class CreditCardProcessor implements PaymentProcessor {
process(amount: number): void {
console.log(`Processing $${amount} via credit card`);
}
}
class PayPalProcessor implements PaymentProcessor {
process(amount: number): void {
console.log(`Processing $${amount} via PayPal`);
}
}
class PaymentService {
private processor: PaymentProcessor;
constructor(processor: PaymentProcessor) {
this.processor = processor;
}
// Có thể thay đổi runtime!
setProcessor(processor: PaymentProcessor): void {
this.processor = processor;
}
checkout(amount: number): void {
this.processor.process(amount);
}
}
// Sử dụng
const service = new PaymentService(new CreditCardProcessor());
service.checkout(100); // Processing $100 via credit card
// User thay đổi phương thức thanh toán
service.setProcessor(new PayPalProcessor());
service.checkout(50); // Processing $50 via PayPal3. Testing Dễ hơn
Composition cho phép mock dependencies dễ dàng:
// Với composition - dễ test
interface IEmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class OrderService {
constructor(private emailService: IEmailService) {}
async placeOrder(order: Order): Promise<void> {
await this.emailService.send(
order.customerEmail,
"Order Confirmation",
`Your order ${order.id} has been placed`
);
}
}
// Trong tests - dễ mock
class MockEmailService implements IEmailService {
sentEmails: Array<{to: string, subject: string, body: string}> = [];
async send(to: string, subject: string, body: string): Promise<void> {
this.sentEmails.push({ to, subject, body });
}
}Các Bẫy Kế thừa Phổ biến
1. Vấn đề Fragile Base Class
Thay đổi class cha có thể bất ngờ phá vỡ class con.
2. Vấn đề Square-Rectangle (Vi phạm LSP)
// Có vấn đề: Square extends Rectangle
class Rectangle {
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
}
class Square extends Rectangle {
// Phải override để duy trì invariant của square
setWidth(width: number): void {
this.width = width;
this.height = width; // Giữ nó vuông
}
}
// Code này hoạt động với Rectangle nhưng không với Square!Giải pháp: Dùng composition hoặc phân tách hierarchy.
3. Phá vỡ Encapsulation
Kế thừa có thể expose chi tiết nội bộ cho subclasses qua protected members.
Mixins: Kết hợp Kế thừa và Composition
Mixins là cách thêm chức năng vào classes mà không cần strict inheritance.
TypeScript Mixins
type Constructor<T = {}> = new (...args: any[]) => T;
// Mixin: Timestampable
function Timestampable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt: Date = new Date();
updatedAt: Date = new Date();
touch(): void {
this.updatedAt = new Date();
}
};
}
// Mixin: Serializable
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
toJSON(): string {
return JSON.stringify(this);
}
};
}
// Kết hợp mixins
class User extends Serializable(Timestampable(Entity)) {
email: string;
constructor(id: string, email: string) {
super(id);
this.email = email;
}
}Python Mixins
# Mixin classes
class TimestampMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.created_at = datetime.now()
self.updated_at = datetime.now()
def touch(self):
self.updated_at = datetime.now()
class SerializableMixin:
def to_json(self) -> str:
return json.dumps(self.__dict__, default=str)
# Kết hợp mixins (mixin trước, rồi đến base class)
class User(TimestampMixin, SerializableMixin, Entity):
def __init__(self, id: str, email: str):
super().__init__(id)
self.email = emailTóm tắt và Điểm Chính
Kế thừa
- Mô hình hóa quan hệ "is-a"
- Cho phép đa hình — xử lý subclasses như parent type
- Dùng
superđể gọi phương thức và constructor cha - Tạo coupling chặt giữa parent và child
- Tốt nhất cho hierarchies ổn định với chuyên biệt hóa rõ ràng
Composition
- Mô hình hóa quan hệ "has-a"
- Tạo coupling lỏng giữa objects
- Dùng delegation để tái sử dụng hành vi
- Cho phép linh hoạt runtime — thay đổi hành vi động
- Dễ test hơn — mock dependencies dễ dàng
Ưu tiên Composition hơn Kế thừa
- Tránh vấn đề fragile base class
- Bảo vệ encapsulation
- Cho phép thay đổi hành vi runtime
- Testing dễ hơn
- Linh hoạt và dễ bảo trì hơn
Bài tập Thực hành
-
Refactor sang Composition: Lấy một inheritance hierarchy và refactor nó dùng composition.
-
Thiết kế Game Character System: Tạo hệ thống character linh hoạt với khả năng di chuyển khác nhau (walk, fly, swim) và khả năng tấn công (sword, magic, archery). Dùng composition để abilities có thể thay đổi runtime.
-
Xây dựng Document Formatter: Thiết kế hệ thống formatting với các tùy chọn (bold, italic, underline, color) kết hợp theo bất kỳ cách nào.
Tiếp theo
Bây giờ bạn đã hiểu kế thừa và composition, bạn sẵn sàng học về Đa hình và Interfaces trong bài viết tiếp theo:
Tiếp tục hành trình OOP: Polymorphism và Interfaces
Tài nguyên Bổ sung
Các Bài Viết Trước trong Series
- OOP & Design Patterns Roadmap - Lộ trình học tập hoàn chỉnh
- Bốn Trụ Cột của OOP - Nền tảng OOP
- Classes, Objects và Abstraction - Các khối xây dựng của OOP
- Encapsulation và Information Hiding - Bảo vệ tính toàn vẹn dữ liệu
Bài Viết Liên quan
- Go: Ưu tiên Composition hơn Kế thừa - Cách Go triển khai composition mà không cần inheritance
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.