Khi code bắt đầu “bốc mùi” sau 3 tháng
Mình từng làm một project Python xử lý dữ liệu đơn hàng — ban đầu chỉ có 3 module, viết thoải mái, chạy ngon. Nhưng 3 tháng sau, project lên đến 20+ module và mỗi lần thêm feature mới là một lần… hít thở sâu. Lý do: mọi class đều tự tạo ra dependency của mình ngay bên trong, kiểu như này:
class OrderService:
def __init__(self):
self.db = DatabaseConnection(host="localhost", port=5432) # Hard-coded!
self.email = EmailSender(smtp_server="smtp.gmail.com") # Hard-coded!
self.logger = Logger(level="INFO") # Hard-coded!
Kết quả: không test được (vì test nào cũng phải có database thật chạy), không swap được (muốn đổi cấu hình staging thì sửa 15 chỗ), và senior review PR… lặng thinh rồi comment “needs refactoring”.
Vấn đề thực sự nằm ở đâu?
Code vẫn chạy đúng — đó không phải lỗi logic. Vấn đề là tight coupling: OrderService không chỉ biết cách xử lý đơn hàng, nó còn phải biết cách tạo database connection, cách kết nối SMTP, cách cấu hình logger. Một class ôm quá nhiều trách nhiệm.
Cái bẫy ở đây: với project nhỏ hay script xử lý 100K records chạy một lần, pattern này hoàn toàn ổn và không ai thấy vấn đề. Vấn đề chỉ lộ ra khi project lớn lên, cần test đúng nghĩa, hoặc có người thứ hai join vào team. Cụ thể:
- Unit test: Muốn test
OrderServicephải có database chạy sẵn — chậm, dễ fail do môi trường - Nhiều môi trường: Dev/staging/prod dùng config khác nhau → phải sửa code hoặc rải env var khắp nơi
- Swap implementation: Đổi từ PostgreSQL sang MySQL → tìm và sửa tất cả chỗ khởi tạo
DatabaseConnection - Circular import: Module A import module B, B import A → Python ném lỗi khi load
Dependency Injection là gì — giải thích cho người mới
Dependency Injection (DI) đơn giản là: thay vì class tự tạo dependency, ta truyền dependency từ bên ngoài vào.
# Không dùng DI (bad)
class OrderService:
def __init__(self):
self.db = DatabaseConnection() # Tự khởi tạo — tightly coupled
# Dùng DI (good)
class OrderService:
def __init__(self, db: DatabaseConnection, email: EmailSender):
self.db = db # Nhận từ bên ngoài
self.email = email # Nhận từ bên ngoài
Bây giờ OrderService chỉ cần biết: “tao cần một cái db và một cái email sender“. Ai cung cấp, cung cấp loại gì — không phải việc của nó.
Ba cách giải quyết, từ thô đến tinh
Cách 1: Manual DI — Tự truyền tay
Tự inject từng dependency khi tạo object:
db = DatabaseConnection(host=os.getenv("DB_HOST"), port=5432)
email = EmailSender(smtp_server=os.getenv("SMTP_SERVER"))
service = OrderService(db=db, email=email)
Ưu điểm: đơn giản, không cần thêm library. Nhược điểm: project lớn lên thì đoạn “wiring” này trở thành 50 dòng rối rắm ở main.py.
Cách 2: Factory / Service Locator
Tạo một registry để quản lý việc tạo object, nhưng đây thực ra là Service Locator pattern — bị coi là anti-pattern vì class vẫn phụ thuộc vào factory, chỉ là ẩn đi thôi. Khó test hơn chứ không dễ hơn.
Cách 3: Thư viện Dependency Injector
Thư viện dependency-injector cung cấp hệ thống Container để khai báo và wire dependencies một cách tường minh, type-safe, và dễ override khi test.
Thực hành với Dependency Injector
Cài đặt
pip install dependency-injector
Cấu trúc project ví dụ
myapp/
├── containers.py # DI Container — khai báo tất cả dependencies
├── services/
│ ├── order_service.py
│ └── notification_service.py
├── repositories/
│ └── order_repository.py
└── main.py
Bước 1 — Viết các class thuần, không biết gì về DI
# repositories/order_repository.py
class OrderRepository:
def __init__(self, db_url: str):
self.db_url = db_url
def get_order(self, order_id: int) -> dict:
# query database...
return {"id": order_id, "status": "pending"}
# services/notification_service.py
class NotificationService:
def __init__(self, smtp_host: str, smtp_port: int):
self.smtp_host = smtp_host
self.smtp_port = smtp_port
def send_email(self, to: str, subject: str, body: str):
print(f"[EMAIL] To: {to} | {subject}")
# services/order_service.py
class OrderService:
def __init__(self, repo: OrderRepository, notifier: NotificationService):
self.repo = repo
self.notifier = notifier
def process_order(self, order_id: int, customer_email: str) -> dict:
order = self.repo.get_order(order_id)
self.notifier.send_email(
to=customer_email,
subject=f"Order #{order_id} confirmed",
body="Your order has been processed.",
)
return order
Bước 2 — Tạo Container khai báo tất cả dependencies
# containers.py
from dependency_injector import containers, providers
from repositories.order_repository import OrderRepository
from services.notification_service import NotificationService
from services.order_service import OrderService
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
# Repository — Singleton: tạo 1 lần, dùng mãi
order_repository = providers.Singleton(
OrderRepository,
db_url=config.database.url,
)
# Notification service — Singleton
notification_service = providers.Singleton(
NotificationService,
smtp_host=config.smtp.host,
smtp_port=config.smtp.port,
)
# Order service — Factory: tạo mới mỗi lần gọi nếu cần
order_service = providers.Factory(
OrderService,
repo=order_repository,
notifier=notification_service,
)
Bước 3 — Sử dụng trong main.py
# main.py
from containers import Container
def main():
container = Container()
container.config.from_dict({
"database": {"url": "postgresql://user:pass@localhost/mydb"},
"smtp": {"host": "smtp.gmail.com", "port": 587},
})
# Dependencies được inject tự động từ container
order_service = container.order_service()
result = order_service.process_order(
order_id=12345,
customer_email="[email protected]",
)
print(f"Processed order: {result}")
if __name__ == "__main__":
main()
Singleton vs Factory — Chọn cái nào?
providers.Singleton: Tạo một lần, dùng mãi — dùng cho database connection, config, cache client. Loại này chiếm ~80% use case thực tế.providers.Factory: Tạo mới mỗi lần gọi — dùng cho request handler, job processorproviders.Resource: Có lifecycle rõ ràng — init khi cần, cleanup khi done (file handles, connection pools)
Lợi ích thấy ngay khi viết test
Thật ra, lý do mình dùng DI không phải vì kiến trúc đẹp — mà vì test. Để test OrderService, không cần database thật, không cần SMTP server:
# tests/test_order_service.py
from unittest.mock import MagicMock
from containers import Container
def test_process_order_sends_notification():
container = Container()
mock_repo = MagicMock()
mock_repo.get_order.return_value = {"id": 1, "status": "pending"}
mock_notifier = MagicMock()
# Override dependencies bằng mock — clean, không side effect
with container.order_repository.override(mock_repo):
with container.notification_service.override(mock_notifier):
service = container.order_service()
service.process_order(order_id=1, customer_email="[email protected]")
mock_notifier.send_email.assert_called_once_with(
to="[email protected]",
subject="Order #1 confirmed",
body="Your order has been processed.",
)
Test chạy trong ~5ms thay vì 2–3 giây phải đợi database thật. Không bao giờ fail vì môi trường, không cần setup Docker chỉ để chạy pytest. Đây là thứ mình ước mình biết sớm hơn khi còn viết kiểu hard-code.
Tích hợp với FastAPI
Với FastAPI, việc tích hợp chỉ cần thêm decorator @inject là xong:
# api/routes.py
from fastapi import APIRouter, Depends
from dependency_injector.wiring import inject, Provide
from containers import Container
from services.order_service import OrderService
router = APIRouter()
@router.post("/orders/{order_id}/process")
@inject
async def process_order(
order_id: int,
customer_email: str,
order_service: OrderService = Depends(Provide[Container.order_service]),
):
result = order_service.process_order(order_id, customer_email)
return {"status": "ok", "order": result}
FastAPI tự gọi container để lấy order_service với đầy đủ dependencies — không cần truyền tay từng request.
Khi nào nên dùng, khi nào không cần?
Mình không suggest dùng DI cho mọi project. Rule of thumb của mình sau vài năm làm việc:
- Nên dùng: Project có 5+ service, cần viết unit test nghiêm túc, team từ 2 người trở lên, cần chạy nhiều môi trường (dev/staging/prod)
- Không cần thiết: Script nhỏ dưới 200 dòng, tool dùng một lần, prototype kiểm tra ý tưởng nhanh
Over-engineering một script 50 dòng với DI Container là lãng phí thời gian. Nhưng với service backend chạy production nhiều tháng, không có DI là nợ kỹ thuật tự chôn mình.
Nói gọn lại
DI không phải magic — chỉ là nguyên tắc đơn giản: đừng để class tự tạo dependency của mình. Thư viện dependency-injector làm điều đó có hệ thống. Khai báo trong Container, wire tự động, override sạch khi test.
Project Python đang khó test, mỗi lần thêm feature là lo lắng? Thêm DI vào trước khi nợ kỹ thuật lan ra toàn codebase — từ kinh nghiệm của mình, đây là bước refactor có ROI cao nhất.

