Giao diện
Composition vs Inheritance — Thoát Khỏi Bẫy Kế Thừa
Bạn đã học OOP, hiểu class, dataclass, __init__. Nhưng khi bắt đầu thiết kế hệ thống thực — service layers, repository patterns, API handlers — câu hỏi xuất hiện: khi nào kế thừa, khi nào composition?
Câu trả lời ngắn: composition trước, inheritance chỉ khi thực sự cần. Bài này sẽ giải thích tại sao, và quan trọng hơn — cách áp dụng composition đúng cách trong Python backend.
Tại sao Inheritance Bị Lạm Dụng?
🎯 Mục tiêu
- Nhận diện "Kingdom of Nouns" problem — khi inheritance trở thành thói quen thay vì quyết định thiết kế
- Hiểu fragile base class problem và tại sao deep inheritance chain phá vỡ maintainability
- Phân biệt quan hệ "is-a" thật sự vs quan hệ "has-a" bị ép thành kế thừa
- Nắm vững Protocol, dependency injection, và composition pattern để thiết kế linh hoạt
The Kingdom of Nouns
Đây là pattern bạn sẽ gặp trong rất nhiều codebase Python — đặc biệt khi team mới chuyển từ Java sang:
python
# ❌ The inheritance addiction
class BaseService:
def log(self, msg): ...
def validate(self, data): ...
class UserService(BaseService):
def create_user(self, data): ...
class AdminUserService(UserService):
def ban_user(self, user_id): ...
class SuperAdminUserService(AdminUserService): # 💀 Where does it end?
def delete_everything(self): ...Mỗi lần cần thêm tính năng, team tạo thêm một tầng kế thừa. Cây class sâu 4-5 tầng. Không ai dám đổi BaseService vì sợ break toàn bộ.
Bốn vấn đề của deep inheritance
| Vấn đề | Hậu quả |
|---|---|
| Fragile base class | Thay đổi BaseService.validate → break mọi class con downstream |
| Multiple inheritance + MRO confusion | Diamond problem, thứ tự gọi super() không rõ ràng |
| Tight coupling | Không thể dùng AdminUserService mà không kéo theo BaseService + UserService |
| "is-a" test fails | AdminUserService có thực sự là một UserService? Hay nó chỉ có khả năng quản lý user? |
⚠️ DẤU HIỆU NHẬN BIẾT
Nếu bạn phải dùng từ "và" để mô tả class con — "nó là UserService và có thêm khả năng ban user và quản lý permissions" — thì bạn đang nhồi nhiều responsibility vào một cây kế thừa. Đó là tín hiệu cần chuyển sang composition.
Composition — Has-A thay vì Is-A
Ý tưởng cốt lõi: thay vì kế thừa hành vi từ class cha, hãy nhận hành vi qua thuộc tính.
Cùng bài toán trên, giải bằng composition:
python
from dataclasses import dataclass
# ✅ Composition: mix capabilities freely
@dataclass
class Logger:
prefix: str = "APP"
def log(self, msg: str) -> None:
print(f"[{self.prefix}] {msg}")
@dataclass
class Validator:
def validate_email(self, email: str) -> bool:
return "@" in email and "." in email
@dataclass
class UserService:
logger: Logger
validator: Validator
repo: "UserRepository"
def create_user(self, data: "CreateUserRequest") -> "User":
if not self.validator.validate_email(data.email):
raise ValueError("Invalid email")
user = self.repo.save(User(email=data.email))
self.logger.log(f"Created user {user.id}")
return userTại sao tốt hơn?
| Tiêu chí | Inheritance | Composition |
|---|---|---|
| Testability | Phải mock cả base class chain | Mock từng dependency riêng |
| Thay đổi implementation | Override method, cầu nguyện không break | Swap object: FileLogger → CloudLogger |
| Dependency chain | Ngầm ẩn trong class hierarchy | Hiện rõ trong constructor |
| Reuse | Class con kéo theo mọi thứ từ cha | Chỉ inject thứ mình cần |
Bạn thấy không? UserService không là Logger — nó có một Logger. Quan hệ "has-a" rõ ràng hơn, linh hoạt hơn, và dễ test hơn.
Protocol — Duck Typing Có Type Safety
Python nổi tiếng với duck typing: "nếu nó kêu như vịt, đi như vịt, thì nó là vịt." Nhưng trong codebase lớn, duck typing thuần túy không đủ — bạn cần mypy bắt lỗi trước khi code chạy.
Protocol giải quyết điều này — duck typing có type safety:
python
from typing import Protocol
class Notifier(Protocol):
"""Bất kỳ class nào có method send() khớp signature đều thỏa mãn."""
def send(self, recipient: str, message: str) -> bool: ...
# Không cần kế thừa Notifier!
class EmailNotifier:
def send(self, recipient: str, message: str) -> bool:
# Gửi email qua SMTP
print(f"📧 Email to {recipient}: {message}")
return True
class SlackNotifier:
def send(self, recipient: str, message: str) -> bool:
# Gửi message qua Slack API
print(f"💬 Slack to {recipient}: {message}")
return True
class SMSNotifier:
def send(self, recipient: str, message: str) -> bool:
# Gửi SMS qua Twilio
print(f"📱 SMS to {recipient}: {message}")
return True
def alert_user(notifier: Notifier, user_email: str, msg: str) -> None:
"""mypy validates: notifier phải có method send() đúng signature."""
notifier.send(user_email, msg)
# ✅ Tất cả đều hợp lệ — không cần inheritance
alert_user(EmailNotifier(), "user@example.com", "Server down!")
alert_user(SlackNotifier(), "#alerts", "Deployment failed!")Protocol vs ABC — Khi nào chọn cái nào?
| Tiêu chí | Protocol | ABC (Abstract Base Class) |
|---|---|---|
| Cơ chế | Structural — khớp signature là đủ | Nominal — phải kế thừa tường minh |
| Import dependency | Class con không cần import Protocol | Class con phải import và kế thừa ABC |
| Duck typing | ✅ Giữ nguyên tinh thần Python | ❌ Ép kế thừa, giống Java |
| Runtime check | Cần @runtime_checkable | isinstance() hoạt động mặc định |
| Khi nào dùng | Mặc định — Interface cho external code, plugin system | Khi cần enforce implementation + shared logic |
💡 NGUYÊN TẮC CHỌN
Protocol trước, ABC chỉ khi cần. Nếu bạn chỉ cần define interface (contract) — dùng Protocol. Nếu bạn cần cung cấp default implementation hoặc force subclass implement method — dùng ABC.
Dependency Injection — Không Cần Framework
Dependency Injection (DI) nghe phức tạp, nhưng trong Python nó đơn giản đến bất ngờ: truyền dependency qua constructor thay vì hardcode bên trong.
python
from dataclasses import dataclass
# --- Interfaces (Protocol) ---
class UserRepository(Protocol):
def save(self, user: "User") -> "User": ...
def find_by_id(self, user_id: str) -> "User | None": ...
# --- Implementations ---
@dataclass
class PostgresUserRepository:
db_url: str
def save(self, user: "User") -> "User":
# INSERT INTO users ...
return user
def find_by_id(self, user_id: str) -> "User | None":
# SELECT * FROM users WHERE id = ...
return None
@dataclass
class InMemoryUserRepository:
_store: dict[str, "User"] = field(default_factory=dict)
def save(self, user: "User") -> "User":
self._store[user.id] = user
return user
def find_by_id(self, user_id: str) -> "User | None":
return self._store.get(user_id)Wiring — Production vs Testing
python
# ✅ Production: kết nối thật
service = UserService(
logger=CloudLogger(service="user-api"),
validator=StrictValidator(),
repo=PostgresUserRepository(db_url=config.DATABASE_URL),
)
# ✅ Testing: mock không cần thư viện
service = UserService(
logger=NullLogger(), # Không log gì — test sạch
validator=Validator(), # Validator đơn giản
repo=InMemoryUserRepository(), # Dict thay database
)Không cần Spring, không cần Guice, không cần decorator magic. Chỉ constructor injection — đơn giản nhất, mạnh nhất.
🔗 FRAMEWORK DI XÂY TRÊN CÙNG NGUYÊN TẮC
FastAPI Depends(), Django inject, hay dependency-injector library — tất cả đều là abstraction trên constructor injection. Nắm vững pattern cơ bản này, bạn sẽ hiểu mọi framework DI.
Mixin — Khi Nào Chấp Nhận Được?
Mixin là pattern nằm giữa inheritance và composition. Python cho phép multiple inheritance, nên mixin rất phổ biến. Nhưng không phải mixin nào cũng an toàn.
✅ Mixin chấp nhận được: nhỏ, stateless
python
from dataclasses import dataclass, field
from datetime import datetime
# ✅ Acceptable mixin: nhỏ, không có state riêng
class TimestampMixin:
created_at: datetime = field(default_factory=datetime.utcnow)
updated_at: datetime | None = None
def touch(self) -> None:
self.updated_at = datetime.utcnow()
@dataclass
class User(TimestampMixin):
name: str = ""
email: str = ""
user = User(name="Minh", email="minh@example.com")
print(user.created_at) # Có sẵn nhờ mixin
user.touch() # Update timestamp❌ Mixin nguy hiểm: stateful, có __init__
python
# ❌ Dangerous mixin: stateful, complex
class CacheMixin:
def __init__(self):
self._cache = {} # 💀 __init__ trong mixin = đau đớn
def get_cached(self, key):
return self._cache.get(key)
def set_cached(self, key, value):
self._cache[key] = value
# Khi dùng với class khác:
class UserService(CacheMixin, BaseService):
def __init__(self):
# Gọi super().__init__() sẽ vào CacheMixin hay BaseService?
# MRO quyết định, nhưng bạn có chắc không?
super().__init__() # 💀 FragileChecklist trước khi dùng mixin
| Câu hỏi | Nếu "Không" → |
|---|---|
Mixin có __init__ không? | ❌ Đừng dùng mixin — chuyển sang composition |
| Mixin có state riêng (attributes)? | ⚠️ Cân nhắc kỹ, dễ conflict với class khác |
| Mixin phụ thuộc vào mixin khác? | ❌ Quá phức tạp — dùng composition |
| Mixin < 20 dòng code? | Nếu lớn hơn → tách thành class riêng, inject vào |
Code Smell & Anti-Patterns
🔴 Code Smell: The God Base Class
python
# ❌ Everything inherits from BaseService
class BaseService:
def log(self, msg): ...
def validate(self, data): ...
def cache(self, key, value): ...
def send_email(self, to, body): ...
def publish_event(self, event): ...
# 200 dòng "shared" functionality nữa...
class OrderService(BaseService): # Cần log + validate
pass
class ReportService(BaseService): # Chỉ cần log
# Nhưng được "tặng" validate, cache, send_email, publish_event 💀
passVấn đề: ReportService chỉ cần log() nhưng kéo theo toàn bộ BaseService — vi phạm Interface Segregation Principle.
Fix: Tách thành dependency nhỏ, inject từng thứ cần thiết:
python
# ✅ Mỗi service chỉ nhận thứ mình cần
@dataclass
class OrderService:
logger: Logger
validator: Validator
@dataclass
class ReportService:
logger: Logger # Chỉ cần log, không bị ép nhận validate/cache/email💡 PERFORMANCE NOTE
Composition có overhead nhỏ (thêm 1 attribute lookup). Nhưng trong 99.99% backend code, I/O latency (database, network) lớn hơn gấp 1000 lần. Đừng optimize sai chỗ.
python
# Attribute lookup: ~50ns
self.logger.log(msg)
# Database query: ~1-50ms (gấp 20,000-1,000,000 lần)
self.repo.find_by_id(user_id)
# Network call: ~50-500ms
self.notifier.send(email, msg)Khi nào overhead quan trọng? Hot loop xử lý hàng triệu items — nhưng lúc đó bạn nên dùng NumPy hoặc Rust extension, không phải optimize attribute lookup.
Bài tập luyện tập
Bài 1: Refactor Inheritance → Composition
Đề bài: Refactor đoạn code sau từ 3 tầng kế thừa sang composition pattern.
python
# ❌ Code cần refactor
class BaseProcessor:
def log(self, msg: str) -> None:
print(f"[LOG] {msg}")
def validate(self, data: dict) -> bool:
return "name" in data
class DataProcessor(BaseProcessor):
def process(self, data: dict) -> dict:
if not self.validate(data):
raise ValueError("Invalid data")
self.log(f"Processing {data['name']}")
return {"processed": True, **data}
class SpecialDataProcessor(DataProcessor):
def process(self, data: dict) -> dict:
result = super().process(data)
self.log("Applying special transformation")
result["special"] = True
return resultYêu cầu:
- Tách
LoggervàValidatorthành class riêng DataProcessornhận chúng qua constructor (dùng@dataclass)- Không cần
SpecialDataProcessor— thêmTransformerProtocol
💡 Gợi ý
Nghĩ theo dependency: DataProcessor cần gì?
- Cần log → nhận
Logger - Cần validate → nhận
Validator - Cần transform → nhận
Transformer(Protocol)
Mỗi thứ là một dependency, không phải một class cha.
✅ Lời giải
python
from dataclasses import dataclass
from typing import Protocol
class Transformer(Protocol):
def transform(self, data: dict) -> dict: ...
@dataclass
class Logger:
prefix: str = "LOG"
def log(self, msg: str) -> None:
print(f"[{self.prefix}] {msg}")
@dataclass
class NameValidator:
required_field: str = "name"
def validate(self, data: dict) -> bool:
return self.required_field in data
class SpecialTransformer:
def transform(self, data: dict) -> dict:
data["special"] = True
return data
class NoOpTransformer:
def transform(self, data: dict) -> dict:
return data
@dataclass
class DataProcessor:
logger: Logger
validator: NameValidator
transformer: Transformer
def process(self, data: dict) -> dict:
if not self.validator.validate(data):
raise ValueError("Invalid data")
self.logger.log(f"Processing {data['name']}")
result = {"processed": True, **data}
return self.transformer.transform(result)
# Sử dụng
processor = DataProcessor(
logger=Logger(prefix="DATA"),
validator=NameValidator(),
transformer=SpecialTransformer(), # Hoặc NoOpTransformer()
)
print(processor.process({"name": "test"}))
# [DATA] Processing test
# {'processed': True, 'name': 'test', 'special': True}Bài 2: Quiz — Protocol đúng hay sai?
🧠 Quiz
Câu hỏi: Đoạn nào dùng Protocol đúng?
Snippet A:
python
class Serializer(Protocol):
def to_json(self) -> str: ...
class UserSerializer(Serializer): # Kế thừa Protocol
def to_json(self) -> str:
return '{"user": true}'Snippet B:
python
class Serializer(Protocol):
def to_json(self) -> str: ...
class UserSerializer: # Không kế thừa — structural typing
def to_json(self) -> str:
return '{"user": true}'
def serialize(s: Serializer) -> str:
return s.to_json()
serialize(UserSerializer()) # ✅ mypy happySnippet C:
python
class Serializer(Protocol):
def to_json(self) -> str: ...
class UserSerializer:
def to_xml(self) -> str: # ❌ Sai signature
return '<user/>'
def serialize(s: Serializer) -> str:
return s.to_json()
serialize(UserSerializer()) # mypy error!- [ ] A. Chỉ Snippet A đúng
- [x] B. Snippet B đúng — Protocol không cần kế thừa, chỉ cần khớp signature
- [ ] C. Cả A và B đều đúng
- [ ] D. Cả ba đều đúng
Giải thích
Snippet A hoạt động nhưng không đúng tinh thần Protocol — nếu kế thừa Protocol thì không khác gì ABC. Sức mạnh của Protocol là structural subtyping: class con không cần biết Protocol tồn tại.
Snippet B ✅ là cách dùng chuẩn — UserSerializer không import, không kế thừa Serializer, nhưng khớp signature → mypy validate thành công.
Snippet C ❌ sai rõ ràng — to_xml không match to_json, mypy sẽ báo lỗi.
Bài 3: Spot the Bug
🐛 Spot the Bug
Đoạn code sau có bug tinh vi. Tìm ra nó:
python
from dataclasses import dataclass, field
from datetime import datetime
class TimestampMixin:
created_at: datetime = datetime.utcnow() # 🤔 Có vấn đề gì?
updated_at: datetime | None = None
@dataclass
class Order(TimestampMixin):
product: str = ""
quantity: int = 0
import time
order1 = Order(product="Laptop", quantity=1)
time.sleep(2)
order2 = Order(product="Phone", quantity=2)
print(order1.created_at == order2.created_at) # ???💡 Gợi ý
datetime.utcnow() được gọi khi nào? Khi class được define hay khi instance được tạo?
✅ Đáp án
Bug: datetime.utcnow() được gọi một lần duy nhất khi class TimestampMixin được define — không phải mỗi lần tạo instance. Tất cả order sẽ có cùng created_at!
python
print(order1.created_at == order2.created_at) # True 💀
# Fix: dùng field(default_factory=...)
class TimestampMixin:
created_at: datetime = field(default_factory=datetime.utcnow)
updated_at: datetime | None = NoneĐây là biến thể của mutable default argument — một trong những gotchas phổ biến nhất trong Python.
Production Anti-Patterns
⚠️ Cạm bẫy
Django CBV — 5 tầng kế thừa ẩn
python
# Django Class-Based Views — chuỗi kế thừa thực tế:
# CreateView → BaseCreateView → ModelFormMixin → FormMixin
# → SingleObjectMixin → ContextMixin → ProcessFormView
# → BaseDetailView → View
class OrderCreateView(LoginRequiredMixin, CreateView):
model = Order
fields = ["product", "quantity"]
def get_queryset(self):
# Class nào define get_queryset() gốc?
# LoginRequiredMixin? CreateView? ModelFormMixin? View?
# Bạn phải đọc MRO mới biết 💀
return super().get_queryset().filter(user=self.request.user)Triệu chứng: Để tìm get_queryset() thực sự đang chạy, bạn phải trace qua 5+ class. IDE "Go to Definition" nhảy lung tung. Bug xuất hiện khi override method mà không biết base class đã làm gì.
The fix: Với logic phức tạp, ưu tiên function-based views hoặc flat composition:
python
# ✅ Function-based view: mọi logic ở một chỗ, không inheritance chain
@login_required
def create_order(request):
if request.method == "POST":
form = OrderForm(request.POST)
if form.is_valid():
order = form.save(commit=False)
order.user = request.user
order.save()
return redirect("order-detail", pk=order.pk)
else:
form = OrderForm()
return render(request, "orders/create.html", {"form": form})Nguyên tắc: CBV inheritance ổn cho CRUD đơn giản. Nhưng khi logic phức tạp (custom validation, conditional workflows, multiple models) — function-based view hoặc composition dễ đọc và debug hơn rất nhiều.
Checklist ghi nhớ
✅ Checklist triển khai
Thiết kế class
- [ ] Áp dụng "has-a" test trước "is-a" — mặc định chọn composition
- [ ] Class hierarchy không quá 2-3 tầng sâu
- [ ] Mỗi class chỉ nhận dependency mình thực sự cần (Interface Segregation)
Protocol & Type Safety
- [ ] Dùng Protocol cho interface — không ép class con kế thừa
- [ ] mypy validate structural subtyping — chạy
mypy --stricttrong CI
Dependency Injection
- [ ] Dependency truyền qua constructor — không hardcode bên trong class
- [ ] Test dùng in-memory implementation — không cần mock library
Mixin
- [ ] Mixin không có
__init__— nếu có, chuyển sang composition - [ ] Mixin < 20 dòng, stateless, không phụ thuộc mixin khác