Back to blog

Đa hình và Interfaces: Một Interface, Nhiều Hình thức

ooppolymorphisminterfacesdesign-patternstypescriptpythonjava
Đa hình và Interfaces: Một Interface, Nhiều Hình thức

Giới thiệu

Từ polymorphism (đa hình) xuất phát từ tiếng Hy Lạp, có nghĩa là "nhiều hình thức." Trong lập trình hướng đối tượng, đây là một trong những khái niệm mạnh mẽ nhất—khả năng để các đối tượng khác nhau phản hồi cùng một thông điệp theo những cách khác nhau.

Khi kết hợp với interfaces, đa hình cho phép bạn viết code linh hoạt, có thể mở rộng, phụ thuộc vào abstraction thay vì implementation cụ thể.

Bạn Sẽ Học Được

✅ Hiểu đa hình và khái niệm "nhiều hình thức"
✅ Nắm vững đa hình compile-time (static): method overloading
✅ Nắm vững đa hình runtime (dynamic): method overriding
✅ Thiết kế với interfaces và abstract contracts
✅ Hiểu duck typing trong ngôn ngữ động
✅ Áp dụng Liskov Substitution Principle (LSP)

Điều kiện tiên quyết


Hiểu về Đa hình (Polymorphism)

Đa hình cho phép các đối tượng có kiểu khác nhau được xử lý như các đối tượng của một kiểu chung. Cùng một method call có thể tạo ra hành vi khác nhau tùy thuộc vào kiểu đối tượng thực tế.

Khái niệm "Nhiều Hình thức"

Hãy nghĩ về điều khiển từ xa đa năng. Khi bạn nhấn "Power," TV bật lên. Nhấn cùng nút đó với thiết bị khác được chọn, và loa bật lên. Cùng một nút (interface), hành vi khác nhau (implementation).

// TypeScript - Ví dụ đa hình
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!`);
  }
}
 
class Duck extends Animal {
  speak(): void {
    console.log(`${this.name} quacks: Quack!`);
  }
}
 
// Đa hình trong thực tế - cùng method, hành vi khác nhau
function makeAnimalSpeak(animal: Animal): void {
  animal.speak(); // speak() nào được gọi phụ thuộc vào kiểu thực tế
}
 
const animals: Animal[] = [
  new Dog("Rex"),
  new Cat("Whiskers"),
  new Duck("Donald")
];
 
animals.forEach(animal => makeAnimalSpeak(animal));
// Output:
// Rex barks: Woof!
// Whiskers meows: Meow!
// Donald quacks: Quack!

Hàm makeAnimalSpeak không biết hoặc không quan tâm về kiểu animal cụ thể. Nó chỉ gọi speak(), và đa hình đảm bảo implementation đúng được chạy.


Hai Loại Đa hình

Đa hình có hai dạng, phân biệt bởi khi nào method được gọi được xác định:

1. Đa hình Compile-Time (Static)

Được giải quyết tại compile time. Compiler xác định method nào được gọi dựa trên:

  • Method signature (overloading)
  • Kiểu tham số
  • Số lượng tham số

Còn được gọi là: Method overloading, static binding, early binding

2. Đa hình Runtime (Dynamic)

Được giải quyết tại runtime. Method thực tế được gọi phụ thuộc vào:

  • Kiểu đối tượng thực tế (không phải kiểu khai báo)
  • Method overriding trong subclasses

Còn được gọi là: Method overriding, dynamic binding, late binding


Đa hình Compile-Time: Method Overloading

Method overloading cho phép nhiều methods cùng tên nhưng khác tham số trong cùng một class.

Ví dụ Java (Hỗ trợ Overloading đầy đủ)

// Java - Method overloading
public class Calculator {
    // Cùng tên method, khác kiểu tham số
    public int add(int a, int b) {
        return a + b;
    }
 
    public double add(double a, double b) {
        return a + b;
    }
 
    public int add(int a, int b, int c) {
        return a + b + c;
    }
 
    public String add(String a, String b) {
        return a + b; // Nối chuỗi
    }
}
 
// Sử dụng
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3));           // 5 (int version)
System.out.println(calc.add(2.5, 3.5));       // 6.0 (double version)
System.out.println(calc.add(1, 2, 3));        // 6 (ba tham số)
System.out.println(calc.add("Hello, ", "World")); // "Hello, World"

TypeScript Overloading (Dựa trên Signature)

// TypeScript - Overload signatures
class Calculator {
  // Overload signatures
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: number, b: number, c: number): number;
 
  // Single implementation
  add(a: number | string, b: number | string, c?: number): number | string {
    if (typeof a === "string" && typeof b === "string") {
      return a + b;
    }
    if (typeof a === "number" && typeof b === "number") {
      return c !== undefined ? a + b + c : a + b;
    }
    throw new Error("Invalid arguments");
  }
}
 
const calc = new Calculator();
console.log(calc.add(2, 3));              // 5
console.log(calc.add("Hello, ", "World")); // "Hello, World"
console.log(calc.add(1, 2, 3));           // 6

Python (Không có Overloading truyền thống)

Python không hỗ trợ method overloading theo cách truyền thống. Thay vào đó sử dụng default parameters hoặc @singledispatch:

# Python - Thay thế cho overloading
from functools import singledispatch
 
# Sử dụng singledispatch cho type-based dispatch
@singledispatch
def add(a, b):
    raise NotImplementedError(f"Unsupported types: {type(a)}, {type(b)}")
 
@add.register(int)
def _(a: int, b: int) -> int:
    return a + b
 
@add.register(str)
def _(a: str, b: str) -> str:
    return a + b
 
@add.register(float)
def _(a: float, b: float) -> float:
    return a + b
 
# Sử dụng
print(add(2, 3))              # 5
print(add("Hello, ", "World")) # "Hello, World"
print(add(2.5, 3.5))          # 6.0

Đa hình Runtime: Method Overriding

Method overriding xảy ra khi subclass cung cấp implementation riêng cho method được định nghĩa trong parent class.

Method Overriding Cơ bản

// TypeScript - Method overriding
class Shape {
  getArea(): number {
    return 0;
  }
 
  describe(): string {
    return `A shape with area ${this.getArea()}`;
  }
}
 
class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }
 
  // Override getArea
  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}
 
class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number
  ) {
    super();
  }
 
  // Override getArea
  getArea(): number {
    return this.width * this.height;
  }
}
 
// Đa hình runtime - getArea() đúng được gọi dựa trên kiểu thực tế
function calculateTotalArea(shapes: Shape[]): number {
  return shapes.reduce((total, shape) => total + shape.getArea(), 0);
}
 
const shapes: Shape[] = [
  new Circle(5),           // Area: 78.54
  new Rectangle(4, 6)      // Area: 24
];
 
console.log(calculateTotalArea(shapes)); // 102.54...
# Python - Method overriding
import math
 
class Shape:
    def get_area(self) -> float:
        return 0
 
    def describe(self) -> str:
        return f"A shape with area {self.get_area()}"
 
class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius
 
    def get_area(self) -> float:
        return math.pi * self.radius ** 2
 
class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height
 
    def get_area(self) -> float:
        return self.width * self.height
 
# Đa hình runtime
def calculate_total_area(shapes: list[Shape]) -> float:
    return sum(shape.get_area() for shape in shapes)

Khác biệt Chính: Overloading vs Overriding

Khía cạnhOverloadingOverriding
Khi giải quyếtCompile timeRuntime
Ở đâuCùng classParent-child classes
Method signaturePhải khácPhải giống
Kiểu trả vềCó thể khácPhải giống hoặc covariant
Mục đíchNhiều cách gọi cùng operationChuyên biệt hóa hành vi

Interfaces: Contracts cho Đa hình

Interfaces định nghĩa một contract—tập hợp các methods mà các implementing classes phải cung cấp. Chúng là hình thức abstraction thuần túy nhất.

Định nghĩa và Implement Interfaces

// TypeScript - Interface definition
interface IPaymentProcessor {
  processPayment(amount: number): boolean;
  refund(transactionId: string): boolean;
  getTransactionHistory(): string[];
}
 
// Implementing classes phải hoàn thành contract
class CreditCardProcessor implements IPaymentProcessor {
  private transactions: string[] = [];
 
  processPayment(amount: number): boolean {
    console.log(`Processing $${amount} via Credit Card`);
    const txId = `CC-${Date.now()}`;
    this.transactions.push(txId);
    return true;
  }
 
  refund(transactionId: string): boolean {
    console.log(`Refunding transaction ${transactionId}`);
    return true;
  }
 
  getTransactionHistory(): string[] {
    return [...this.transactions];
  }
}
 
class PayPalProcessor implements IPaymentProcessor {
  private transactions: string[] = [];
 
  processPayment(amount: number): boolean {
    console.log(`Processing $${amount} via PayPal`);
    const txId = `PP-${Date.now()}`;
    this.transactions.push(txId);
    return true;
  }
 
  refund(transactionId: string): boolean {
    console.log(`Refunding PayPal transaction ${transactionId}`);
    return true;
  }
 
  getTransactionHistory(): string[] {
    return [...this.transactions];
  }
}
 
// Client code phụ thuộc vào interface, không phải implementation
class CheckoutService {
  constructor(private paymentProcessor: IPaymentProcessor) {}
 
  checkout(amount: number): boolean {
    console.log(`Checkout for $${amount}`);
    return this.paymentProcessor.processPayment(amount);
  }
 
  // Có thể thay đổi processor tại runtime
  setPaymentProcessor(processor: IPaymentProcessor): void {
    this.paymentProcessor = processor;
  }
}
 
// Sử dụng - đa hình thông qua interfaces
const checkout = new CheckoutService(new CreditCardProcessor());
checkout.checkout(100); // Processing $100 via Credit Card
 
checkout.setPaymentProcessor(new PayPalProcessor());
checkout.checkout(50); // Processing $50 via PayPal
# Python - Interface sử dụng ABC (Abstract Base Class)
from abc import ABC, abstractmethod
 
class IPaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        pass
 
    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        pass
 
    @abstractmethod
    def get_transaction_history(self) -> list[str]:
        pass
 
class CreditCardProcessor(IPaymentProcessor):
    def __init__(self):
        self._transactions: list[str] = []
 
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via Credit Card")
        tx_id = f"CC-{id(self)}"
        self._transactions.append(tx_id)
        return True
 
    def refund(self, transaction_id: str) -> bool:
        print(f"Refunding transaction {transaction_id}")
        return True
 
    def get_transaction_history(self) -> list[str]:
        return self._transactions.copy()
 
# Client code phụ thuộc vào interface
class CheckoutService:
    def __init__(self, payment_processor: IPaymentProcessor):
        self._processor = payment_processor
 
    def checkout(self, amount: float) -> bool:
        print(f"Checkout for ${amount}")
        return self._processor.process_payment(amount)
 
    def set_payment_processor(self, processor: IPaymentProcessor):
        self._processor = processor

Implement Nhiều Interfaces

Không giống classes (single inheritance trong hầu hết ngôn ngữ), một class có thể implement nhiều interfaces:

// TypeScript - Multiple interfaces
interface IReadable {
  read(): string;
}
 
interface IWritable {
  write(data: string): void;
}
 
interface IClosable {
  close(): void;
  isOpen(): boolean;
}
 
// Implement nhiều interfaces
class FileHandler implements IReadable, IWritable, IClosable {
  private content: string = "";
  private open: boolean = true;
 
  read(): string {
    if (!this.open) throw new Error("File is closed");
    return this.content;
  }
 
  write(data: string): void {
    if (!this.open) throw new Error("File is closed");
    this.content += data;
  }
 
  close(): void {
    this.open = false;
  }
 
  isOpen(): boolean {
    return this.open;
  }
}
 
// Functions có thể yêu cầu các interfaces cụ thể
function copyContent(source: IReadable, destination: IWritable): void {
  const content = source.read();
  destination.write(content);
}

Abstract Classes vs Interfaces

Cả hai đều cung cấp abstraction, nhưng phục vụ các mục đích khác nhau:

Bảng So sánh

Khía cạnhAbstract ClassInterface
MethodsCó thể có implementationChỉ signatures (chủ yếu)
FieldsCó thể có instance fieldsChỉ constants (Java)
ConstructorsCóKhông
Kế thừaSingleMultiple
Mục đíchShared implementationDefine contract
"Is-a" vs "Can-do"Quan hệ Is-aKhả năng Can-do

Khi nào Dùng Abstract Classes

  • Khi bạn có shared implementation giữa các subclasses
  • Khi cần constructors hoặc instance fields
  • Khi quan hệ thực sự là "is-a"

Khi nào Dùng Interfaces

  • Khi muốn định nghĩa contract không có implementation
  • Khi cần multiple inheritance của types
  • Khi các classes không liên quan cần chia sẻ khả năng

Duck Typing: "Nếu Nó Kêu Như Vịt..."

Duck typing là một dạng đa hình trong ngôn ngữ động, nơi sự phù hợp của đối tượng được xác định bởi methods và properties, không phải class.

"Nếu nó đi như vịt và kêu như vịt, thì nó chắc là vịt."

Python Duck Typing

# Python - Duck typing (không cần interface!)
class Duck:
    def speak(self):
        print("Quack!")
 
    def walk(self):
        print("Waddle waddle")
 
class Person:
    def speak(self):
        print("Hello!")
 
    def walk(self):
        print("Step step")
 
class Robot:
    def speak(self):
        print("Beep boop!")
 
    def walk(self):
        print("Clank clank")
 
# Không cần base class hay interface chung
def make_it_walk_and_talk(thing):
    thing.walk()
    thing.speak()
 
# Tất cả hoạt động vì chúng có walk() và speak()
make_it_walk_and_talk(Duck())    # Waddle waddle, Quack!
make_it_walk_and_talk(Person())  # Step step, Hello!
make_it_walk_and_talk(Robot())   # Clank clank, Beep boop!

TypeScript Structural Typing

TypeScript sử dụng structural typing — types tương thích nếu cấu trúc của chúng khớp:

// TypeScript - Structural typing (như duck typing với types)
interface Walkable {
  walk(): void;
}
 
interface Talkable {
  speak(): void;
}
 
// Không cần implements tường minh nếu cấu trúc khớp
class Duck {
  speak(): void {
    console.log("Quack!");
  }
 
  walk(): void {
    console.log("Waddle waddle");
  }
}
 
class Robot {
  speak(): void {
    console.log("Beep boop!");
  }
 
  walk(): void {
    console.log("Clank clank");
  }
}
 
// Cả hai hoạt động dù Robot không implements interfaces tường minh
function makeItWalkAndTalk(thing: Walkable & Talkable): void {
  thing.walk();
  thing.speak();
}
 
makeItWalkAndTalk(new Duck());  // Hoạt động!
makeItWalkAndTalk(new Robot()); // Hoạt động!

Liskov Substitution Principle (LSP)

Liskov Substitution Principle phát biểu:

Objects của superclass phải có thể thay thế bằng objects của subclasses mà không ảnh hưởng đến tính đúng đắn của chương trình.

Đây là "L" trong SOLID và là nền tảng cho đa hình đúng cách.

Vi phạm LSP: Square và Rectangle

Ví dụ kinh điển về vi phạm LSP:

// Vi phạm LSP - Square extends Rectangle
class Rectangle {
  protected width: number;
  protected height: number;
 
  setWidth(width: number): void {
    this.width = width;
  }
 
  setHeight(height: number): void {
    this.height = height;
  }
 
  getArea(): number {
    return this.width * this.height;
  }
}
 
class Square extends Rectangle {
  // Phải override để duy trì invariant của square
  setWidth(width: number): void {
    this.width = width;
    this.height = width; // Phải giữ nó vuông!
  }
 
  setHeight(height: number): void {
    this.height = height;
    this.width = height; // Phải giữ nó vuông!
  }
}
 
// Hàm này hoạt động với Rectangle nhưng không với Square
function testLSP(rect: Rectangle): void {
  rect.setWidth(5);
  rect.setHeight(4);
  const area = rect.getArea();
 
  // Expected: 5 * 4 = 20
  console.log(`Expected area: 20, Actual: ${area}`);
}
 
testLSP(new Rectangle(1, 1)); // Expected: 20, Actual: 20 ✓
testLSP(new Square(1));       // Expected: 20, Actual: 16 ✗ (4*4)

Thiết kế Tuân thủ LSP

// Tuân thủ LSP - Dùng interface thay vì inheritance
interface Shape {
  getArea(): number;
}
 
class Rectangle implements Shape {
  constructor(
    private width: number,
    private height: number
  ) {}
 
  setWidth(width: number): void {
    this.width = width;
  }
 
  setHeight(height: number): void {
    this.height = height;
  }
 
  getArea(): number {
    return this.width * this.height;
  }
}
 
class Square implements Shape {
  constructor(private size: number) {}
 
  setSize(size: number): void {
    this.size = size;
  }
 
  getArea(): number {
    return this.size * this.size;
  }
}
 
// Không có quan hệ inheritance - không thể vi phạm LSP

Interface Segregation

Interface Segregation Principle (ISP) phát biểu rằng clients không nên phụ thuộc vào interfaces họ không sử dụng.

Vấn đề: Fat Interface

// Vấn đề: Fat interface
interface IWorker {
  work(): void;
  eat(): void;
  sleep(): void;
}
 
class RobotWorker implements IWorker {
  work(): void { console.log("Processing tasks..."); }
  eat(): void { /* Robots không ăn! */ }
  sleep(): void { /* Robots không ngủ! */ }
}

Giải pháp: Segregated Interfaces

// Giải pháp: Segregated interfaces
interface IWorkable {
  work(): void;
}
 
interface IFeedable {
  eat(): void;
}
 
interface ISleepable {
  sleep(): void;
}
 
// Human implements tất cả
class HumanWorker implements IWorkable, IFeedable, ISleepable {
  work(): void { console.log("Working..."); }
  eat(): void { console.log("Eating lunch..."); }
  sleep(): void { console.log("Sleeping..."); }
}
 
// Robot chỉ implements những gì cần
class RobotWorker implements IWorkable {
  work(): void { console.log("Processing tasks..."); }
}

Tóm tắt và Điểm Chính

Đa hình

  • Cho phép objects mang "nhiều hình thức"
  • Cùng interface, implementations khác nhau
  • Cho phép code linh hoạt, có thể mở rộng

Hai Loại

Compile-Time (Static)Runtime (Dynamic)
Method overloadingMethod overriding
Giải quyết lúc compileGiải quyết lúc runtime
Dựa trên parametersDựa trên kiểu object thực tế
Cùng classParent-child classes

Interfaces

  • Định nghĩa contracts không có implementation
  • Cho phép multiple inheritance của types
  • Nền tảng cho dependency injection
  • Cho phép thay đổi implementations tại runtime

Duck Typing

  • Tương thích object dựa trên cấu trúc, không phải type
  • "Nếu nó đi như vịt và kêu như vịt..."
  • Linh hoạt hơn nhưng ít an toàn hơn
  • Phổ biến trong Python, hỗ trợ trong TypeScript

Liskov Substitution Principle

  • Subclasses phải thay thế được cho parent classes
  • Tránh vi phạm contracts của parent class
  • Square/Rectangle là ví dụ vi phạm kinh điển
  • Ưu tiên composition khi inheritance không phù hợp

Best Practices

  1. Lập trình với interfaces, không implementations
  2. Ưu tiên composition hơn inheritance cho sự linh hoạt
  3. Giữ interfaces nhỏ (Interface Segregation)
  4. Tôn trọng LSP — subtypes phải tuân thủ parent contracts
  5. Dùng đa hình để loại bỏ conditional logic

Bài tập Thực hành

  1. Xây dựng Plugin System: Tạo kiến trúc plugin với interface IPlugin có init(), execute(), và cleanup(). Application chính có thể load và chạy bất kỳ plugin nào một cách đa hình.

  2. Implement Sorting Framework: Thiết kế framework sorting với interface ISortable. Implement bubble sort, quick sort, và merge sort.

  3. Tạo Media Player: Xây dựng media player có thể phát các media types khác nhau (Audio, Video, Stream) thông qua interface IPlayable chung.


Tiếp theo

Bây giờ bạn đã hiểu đa hình và interfaces, bạn sẵn sàng đi sâu vào SOLID Principles trong bài viết tiếp theo:

Tiếp tục hành trình OOP: SOLID Principles Explained


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.