Dependency Injection trong FastAPI: Từ sự cố lúc 2 giờ sáng đến kiến trúc Modular

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Cuộc gọi lúc 2 giờ sáng: Tại sao sự phụ thuộc chặt chẽ (Tight Coupling) lại giết chết hệ thống

Lúc đó là 2:14 sáng khi chuông cảnh báo PagerDuty của tôi bắt đầu kêu inh ỏi. Một microservice thanh toán cốt lõi đã bị sập vì một bên thứ ba cập nhật schema metadata mà không báo trước. Khi tôi đào sâu vào các bản log giữa một loạt lỗi 500, tôi nhận ra logic cho API bên ngoài đó đã được code cứng (hardcoded) ở 14 route handler khác nhau. Để sửa một thay đổi trường duy nhất, tôi đã phải cập nhật thủ công 22 hàm và hy vọng rằng mình không làm hỏng logic cơ sở dữ liệu nằm ngay cạnh chúng.

Tight coupling (phụ thuộc chặt chẽ) là một kẻ sát nhân thầm lặng. Khi logic nghiệp vụ của bạn bị dính chặt trực tiếp vào framework hoặc trình điều khiển cơ sở dữ liệu, bạn sẽ mất khả năng xoay chuyển. Bạn không thể kiểm thử hiệu quả và chắc chắn không thể mở rộng quy mô. Đây là lúc Dependency Injection (DI) không còn là một mẫu thiết kế “nên có” mà trở thành chính sách bảo hiểm của bạn chống lại nợ kỹ thuật (technical debt).

FastAPI coi DI là một thành phần ưu tiên hàng đầu thông qua hệ thống Depends của nó. Tôi đã sử dụng kiến trúc này trong các môi trường production xử lý 5.000 yêu cầu mỗi giây. Kết quả rất rõ ràng: chúng tôi có thể thay đổi database engine hoặc giả lập (mock) các dịch vụ bên ngoài để kiểm thử in vài phút chứ không phải vài ngày, mà không cần chạm vào logic nghiệp vụ cốt lõi.

Thiết lập môi trường sạch

Trước khi sửa lại kiến trúc, chúng ta cần một sandbox chuẩn chỉnh. Chúng ta không chỉ cài đặt FastAPI; chúng ta cần các công cụ để mô phỏng môi trường thực tế nơi DI thực sự phát huy tác dụng. Chúng ta sẽ sử dụng httpx cho các lệnh gọi bên ngoài và pytest để xác minh kiến trúc của mình.

# Tạo môi trường ảo
python -m venv venv
source venv/bin/activate

# Cài đặt các thư viện cần thiết cho production
pip install fastapi uvicorn httpx pytest

Trong một dự án thực tế, bạn sẽ sử dụng pyproject.toml hoặc requirements.txt. Đối với hướng dẫn này, những bước cơ bản này là đủ. Mục tiêu của chúng ta là tránh xa anti-pattern “tất cả trong một file” vốn thường thấy ở nhiều startup giai đoạn đầu.

Cấu hình lớp Dependency Injection

Hầu hết các nhà phát triển bắt đầu bằng cách đưa logic cơ sở dữ liệu trực tiếp vào các path operation của FastAPI. Cách này hoạt động với các ứng dụng “Hello World” nhưng sẽ thất bại khi cần migration. Để xây dựng thứ gì đó bền bỉ, chúng ta tách biệt các mối quan tâm thành ba lớp riêng biệt: Dữ liệu (Data), Logic và Giao diện (Interface).

1. Lớp Logic (Trừu tượng hóa)

Đầu tiên, hãy xác định dịch vụ sẽ làm gì. Nếu chúng ta đang xây dựng một hệ thống quản lý người dùng, chúng ta cần một cách nhất quán để lấy dữ liệu. Sử dụng một class cho phép chúng ta thay đổi các triển khai (implementations) mà không cần thay đổi mã gọi hàm.

# services.py
class UserService:
    def __init__(self, api_key: str):
        self.api_key = api_key

    def get_user_data(self, user_id: int):
        # Trong thực tế, đoạn này có thể gọi một CRM bên ngoài như Salesforce hoặc HubSpot
        return {"id": user_id, "name": "John Doe", "status": "active"}

2. Nhà cung cấp phụ thuộc (Dependency Provider)

Thay vì khởi tạo UserService bên trong một route, hãy tạo một hàm provider chuyên dụng. Đây là nơi bạn xử lý cấu hình, chẳng hạn như lấy secret từ biến môi trường hoặc quản lý connection pool.

# dependencies.py
import os
from .services import UserService

def get_user_service():
    # Lấy từ biến môi trường; mặc định là giá trị tạm thời cho môi trường dev
    api_key = os.getenv("CRM_API_KEY", "dev-key-123")
    return UserService(api_key=api_key)

3. Inject vào Route

Cuối cùng, sử dụng Depends của FastAPI để kết nối mọi thứ lại với nhau. Route không quan tâm UserService được tạo ra như thế nào. Nó chỉ đơn giản yêu cầu một instance và bắt đầu làm việc.

# main.py
from fastapi import FastAPI, Depends
from .dependencies import get_user_service
from .services import UserService

app = FastAPI()

@app.get("/users/{user_id}")
def read_user(user_id: int, service: UserService = Depends(get_user_service)):
    return service.get_user_data(user_id)

Cấu trúc này giúp tiết kiệm hàng giờ refactoring. Nếu bạn chuyển từ một CRM bên ngoài sang cơ sở dữ liệu PostgreSQL cục bộ, bạn chỉ cần thay đổi hàm get_user_service. Các route vẫn được giữ nguyên.

Kiểm thử không còn là nỗi lo

DI làm cho việc kiểm thử trở nên vô cùng đơn giản. Trong sự cố lúc 2 giờ sáng đó, lẽ ra tôi có thể viết một test case giả lập API bị lỗi chỉ trong vài giây. FastAPI cho phép bạn ghi đè (override) các phụ thuộc trên toàn cầu, đây là cứu cánh cho các pipeline CI/CD.

Trong các bộ kiểm thử của mình, tôi sử dụng một phiên bản “Mock” của dịch vụ. Điều này đảm bảo các bài kiểm tra chạy trong vài mili giây vì chúng không bao giờ thực hiện kết nối mạng.

# test_main.py
from fastapi.testclient import TestClient
from .main import app
from .dependencies import get_user_service

client = TestClient(app)

class MockUserService:
    def get_user_data(self, user_id: int):
        return {"id": user_id, "name": "Người dùng Giả lập", "status": "testing"}

# Thay thế dịch vụ thật bằng dịch vụ giả lập (mock)
app.dependency_overrides[get_user_service] = lambda: MockUserService()

def test_read_user():
    response = client.get("/users/1")
    assert response.status_code == 200
    assert response.json()["name"] == "Người dùng Giả lập"

# Reset lại các override để tránh rò rỉ trạng thái giữa các bài test
app.dependency_overrides = {}

Sử dụng app.dependency_overrides cho phép bạn mô phỏng tình trạng timeout của cơ sở dữ liệu hoặc các trường hợp dữ liệu biên (edge-case) mà không cần chạm vào máy chủ production. Đây là dấu hiệu của một ứng dụng bền bỉ.

Giám sát và Bảo trì dài hạn

Khi bạn triển khai DI, việc giám sát trở nên sạch sẽ hơn. Vì các phụ thuộc được tập trung hóa, bạn có thể bao bọc chúng bằng các công cụ đo lường (telemetry) như OpenTelemetry hoặc logging cơ bản. Bạn không cần thêm bộ đếm thời gian (timer) vào mọi route. Bạn chỉ cần thêm chúng một lần trong dependency provider của mình.

Hãy để mắt đến “Đồ thị phụ thuộc” (Dependency Graph) của bạn. FastAPI tự động xử lý các phụ thuộc con (sub-dependencies), điều này rất mạnh mẽ nhưng có thể dẫn đến sự phức tạp. Nếu Service A cần Service B, và B lại cần Service C, điều đó thường ổn. Tuy nhiên, nếu bạn thấy các phụ thuộc vòng (circular dependencies) nơi A cần BB cần A, thì ranh giới dịch vụ của bạn có thể đang quá mờ nhạt.

Các ứng dụng Python ổn định coi các phụ thuộc giống như các module có thể tháo rời. Bạn sẽ có thể rút một cơ sở dữ liệu thật ra và cắm một bản mock vào mà ứng dụng không hề gặp lỗi. Nó giúp việc gỡ lỗi bớt căng thẳng và mã nguồn của bạn chuyên nghiệp hơn đáng kể.

Đừng đợi đến khi hệ thống production gặp sự cố mới bắt đầu tách biệt mã nguồn. Hãy xác định một dịch vụ bên ngoài hoặc một lệnh gọi cơ sở dữ liệu trong ứng dụng FastAPI của bạn và chuyển nó vào Depends() ngay hôm nay. Bản thân bạn trong tương lai sẽ cảm ơn nỗ lực này khi cảnh báo tiếp theo xuất hiện.

Share: