Đ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
- 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
- Hoàn thành Kế thừa và Composition
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)); // 6Python (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ạnh | Overloading | Overriding |
|---|---|---|
| Khi giải quyết | Compile time | Runtime |
| Ở đâu | Cùng class | Parent-child classes |
| Method signature | Phải khác | Phải giống |
| Kiểu trả về | Có thể khác | Phải giống hoặc covariant |
| Mục đích | Nhiều cách gọi cùng operation | Chuyê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 = processorImplement 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ạnh | Abstract Class | Interface |
|---|---|---|
| Methods | Có thể có implementation | Chỉ signatures (chủ yếu) |
| Fields | Có thể có instance fields | Chỉ constants (Java) |
| Constructors | Có | Không |
| Kế thừa | Single | Multiple |
| Mục đích | Shared implementation | Define contract |
| "Is-a" vs "Can-do" | Quan hệ Is-a | Khả 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 LSPInterface 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 overloading | Method overriding |
| Giải quyết lúc compile | Giải quyết lúc runtime |
| Dựa trên parameters | Dựa trên kiểu object thực tế |
| Cùng class | Parent-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
- Lập trình với interfaces, không implementations
- Ưu tiên composition hơn inheritance cho sự linh hoạt
- Giữ interfaces nhỏ (Interface Segregation)
- Tôn trọng LSP — subtypes phải tuân thủ parent contracts
- Dùng đa hình để loại bỏ conditional logic
Bài tập Thực hành
-
Xây dựng Plugin System: Tạo kiến trúc plugin với interface
IPlugincó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. -
Implement Sorting Framework: Thiết kế framework sorting với interface
ISortable. Implement bubble sort, quick sort, và merge sort. -
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
IPlayablechung.
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
- 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
- Kế thừa và Composition - Chiến lược tái sử dụng code
Bài Viết Liên quan
- Java Phase 2: Lập trình Hướng Đối tượng - OOP trong Java
- Python Phase 2: OOP & Tính năng Nâng cao - OOP trong Python
📬 Đă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.