Back to blog

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

oopinheritancecompositiondesign-patternstypescriptpythonjava
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


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):

  • Dog is-a Animal (Chó là một Động vật)
  • Manager is-a Employee (Quản lý là một Nhân viên)
  • Car is-a Vehicle (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 sound

Từ 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()); // 90000

Hiể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):

  • Car has-a Engine (Xe hơi có một Động cơ)
  • Computer has-a Processor (Máy tính có một Bộ xử lý)
  • Team has-a list of Players (Độ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 1

Kế thừa vs Composition: Khi nào Dùng Cái Nào

Bảng So sánh

Khía cạnhKế thừaComposition
Quan hệ"Is-a" (là một)"Has-a" (có một)
CouplingChặt (tight)Lỏng (loose)
Linh hoạtThấp (cố định lúc compile)Cao (có thể thay đổi runtime)
Tái sử dụngTất cả hoặc khôngChọn những gì cần
TestingKhó hơnDễ hơn (mock được)
Thay đổi hành viCần subclass mớiThay đổi component

Dùng Kế thừa Khi:

  1. Có quan hệ "is-a" rõ ràng và ít thay đổi
  2. Cần đa hình (polymorphism)
  3. Class cha ổn định
  4. Muốn mở rộng hành vi

Dùng Composition Khi:

  1. Có quan hệ "has-a"
  2. Cần linh hoạt - thay đổi hành vi runtime
  3. Muốn kết hợp nhiều hành vi
  4. 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 PayPal

3. 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 = email

Tó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

  1. Refactor sang Composition: Lấy một inheritance hierarchy và refactor nó dùng composition.

  2. 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.

  3. 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

Bài Viết Liên quan

📬 Đă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.