Skip to content

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 classThay đổi BaseService.validate → break mọi class con downstream
Multiple inheritance + MRO confusionDiamond problem, thứ tự gọi super() không rõ ràng
Tight couplingKhông thể dùng AdminUserService mà không kéo theo BaseService + UserService
"is-a" test failsAdminUserService có thực sự một UserService? Hay nó chỉ 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 có thêm khả năng ban user 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 user

Tại sao tốt hơn?

Tiêu chíInheritanceComposition
TestabilityPhải mock cả base class chainMock từng dependency riêng
Thay đổi implementationOverride method, cầu nguyện không breakSwap object: FileLoggerCloudLogger
Dependency chainNgầm ẩn trong class hierarchyHiện rõ trong constructor
ReuseClass con kéo theo mọi thứ từ chaChỉ inject thứ mình cần

Bạn thấy không? UserService không Logger — nó 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 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íProtocolABC (Abstract Base Class)
Cơ chếStructural — khớp signature là đủNominal — phải kế thừa tường minh
Import dependencyClass con không cần import ProtocolClass 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 checkCần @runtime_checkableisinstance() hoạt động mặc định
Khi nào dùngMặc định — Interface cho external code, plugin systemKhi 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__()  # 💀 Fragile

Checklist trước khi dùng mixin

Câu hỏiNế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 💀
    pass

Vấ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 result

Yêu cầu:

  1. Tách LoggerValidator thành class riêng
  2. DataProcessor nhận chúng qua constructor (dùng @dataclass)
  3. Không cần SpecialDataProcessor — thêm Transformer Protocol
💡 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 happy

Snippet 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 --strict trong 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