Dependency injection là một mẫu thiết kế trong đó các đối tượng hoặc dịch vụ (tức là các dependency) được cung cấp cho một lớp từ bên ngoài, thay vì lớp đó tự tạo bên trong. Điều này giúp tách rời các thành phần.
Việc sử dụng dependency injection trong Python mang lại nhiều lợi ích, chẳng hạn như:
Tính mô-đun: Mã của bạn được chia thành các phần nhỏ hơn, có thể tái sử dụng.
Dễ kiểm thử: Dễ dàng kiểm thử vì bạn có thể thay thế phần thực bằng các mock.
Dễ bảo trì: Bạn có thể cập nhật hoặc chỉnh sửa từng phần mã mà không làm hỏng phần còn lại của hệ thống.
Trong hướng dẫn này, tôi sẽ minh họa cách sử dụng dependency injection trong Python thông qua các ví dụ đơn giản, thực tế.
TL;DR
Dependency injection là một mẫu thiết kế trong đó một lớp nhận các dependency từ bên ngoài thay vì tự tạo. Nó giúp mã của bạn mô-đun hơn.
Bạn có thể inject dependency thông qua constructor, phương thức setter, hoặc trực tiếp vào phương thức, và Python hỗ trợ việc này thủ công hoặc thông qua các framework như dependency-injector, injector, và punq.
Các web framework phổ biến xử lý khác nhau: FastAPI có hỗ trợ sẵn qua Depends(). Flask và Django cần thư viện như dependency-injector.
Tìm hiểu Dependency Injection trong Python
Để hiểu cách triển khai dependency injection, điều cốt yếu là bạn nắm vững các nguyên tắc chính chi phối nó.
Dependency injection dựa trên nguyên tắc Inversion of Control (IoC), nghĩa là thay vì một lớp tự tạo và quản lý các dependency, nó nhận chúng từ nguồn bên ngoài. Điều này giúp tập trung vào một nhiệm vụ duy nhất và làm mã của bạn gọn gàng.
Inject dependency trong Python bao gồm những cách phổ biến sau, ngoài ra còn có các cách khác:
Constructor injection: Truyền dependency qua constructor của lớp.
Setter injection: Thiết lập dependency bằng phương thức setter sau khi đối tượng được tạo.
Method injection: Truyền dependency trực tiếp vào phương thức cần dùng.
Vì sao nên dùng Dependency Injection trong Python?
Là một nhà phát triển, hiểu và triển khai dependency injection có thể tạo ra khác biệt lớn trong thiết kế mã của bạn. Hãy xem một số lợi ích của dependency injection.
Tính mô-đun, tái sử dụng và linh hoạt
Dependency injection cho phép bạn chia nhỏ mã thành các thành phần tập trung hơn, giúp tăng tính linh hoạt và mô-đun.
Mỗi phần hoặc thành phần xử lý một tác vụ cụ thể và dựa vào các dependency bên ngoài, giúp dễ dàng tái sử dụng ở những phần khác trong ứng dụng của bạn hoặc của người khác
Ví dụ, nếu bạn có một lớp gửi tin nhắn. Thay vì xây dựng logic email hoặc SMS trực tiếp trong dịch vụ của bạn, bạn có thể inject một lớp gửi email hoặc SMS.
Khi đó, lớp gửi tin nhắn của bạn không cần quan tâm tin nhắn được gửi như thế nào, mà chỉ sử dụng bộ gửi được cung cấp. Điều này ngụ ý rằng bạn có thể dùng bộ gửi tin nhắn với các bộ gửi email hoặc SMS khác nhau.
Dễ bảo trì và kiểm thử
Khi dùng dependency injection, bạn có thể dễ dàng thay thế dependency thật bằng dữ liệu giả (mock) khi kiểm thử. Điều này giúp viết unit test đơn giản hơn mà không phụ thuộc vào API hoặc cơ sở dữ liệu.
Giả sử, như một bài tập suy nghĩ, bạn có một lớp lưu dữ liệu vào cơ sở dữ liệu. Khi kiểm thử, bạn có thể inject một cơ sở dữ liệu giả thay vì cơ sở dữ liệu thật để tránh ràng buộc bài kiểm thử với cơ sở dữ liệu sản xuất.
Dependency injection cho phép bạn chỉnh sửa các phần cụ thể của mã mà không làm vỡ toàn bộ hệ thống. Điều này đặc biệt hữu ích khi cần chuyển nhà cung cấp thanh toán trong ứng dụng, ví dụ từ Stripe sang PayPal, mà không sửa đổi phần mã còn lại.
Đảo ngược kiểm soát và dependency có thể hoán đổi
Ý tưởng về đảo ngược kiểm soát là: dependency injection sẽ chuyển trách nhiệm tạo và quản lý dependency sang một nguồn bên ngoài.
Điều này có nghĩa bạn có thể dễ dàng hoán đổi một triển khai cho triển khai khác; ví dụ, nếu bạn có lớp ReportGenerator định dạng và xuất báo cáo.
Thay vì lớp tự quyết định định dạng xuất, bạn có thể inject một lớp xuất, như PDFExporter, ExcelExporter, hoặc CSVExporter, vào nó.
Lớp ReportGenerator không quan tâm việc xuất diễn ra thế nào, nên để đổi định dạng, bạn chỉ cần hoán đổi bộ xuất mà bạn inject, không cần chỉnh sửa ReportGenerator.
Triển khai Dependency Injection trong Python
Giờ bạn đã hiểu dependency injection là gì và vì sao hữu ích, hãy xem cách triển khai nó trong Python. Bắt đầu với phương pháp đơn giản nhất: dependency injection thủ công.
Dependency injection thủ công
Với dependency injection thủ công, bạn tự tạo các dependency và truyền chúng vào lớp hoặc hàm cần dùng.
Phương pháp này hoạt động tốt với ứng dụng nhỏ, nhưng khi ứng dụng mở rộng, bạn cần dùng một framework DI để tránh lỗi.
Dưới đây là ví dụ mã không dùng dependency injection
classEmailService:defsend_email(self, message):print(f"Sending email: {message}")classUserNotifier:def__init__(self):
self.email_service = EmailService()# Creates its own dependencydefnotify(self, message):
self.email_service.send_email(message)
notifier = UserNotifier()
notifier.notify("Welcome!")Được hỗ trợ bởi
Trong ví dụ trên, UserNotifier gắn chặt với EmailService. Bạn không thể dễ dàng hoán đổi hoặc mock EmailService để kiểm thử.
Dưới đây là một phiên bản khác, nhưng với dependency injection thủ công.
Singleton đảm bảo chỉ có một instance của EmailService
Factory tạo một UserNotifier mới với EmailService được inject tự động.
Cơ chế wiring
Bạn có thể dùng decorator như @inject để giảm nhu cầu truyền dependency thủ công.
Dưới đây là một phiên bản khác của ví dụ trước:
from dependency_injector import containers, providers
# Define servicesclassEmailService:defsend_email(self, message):print(f"Sending email: {message}")classUserNotifier:def__init__(self, email_service: EmailService):
self.email_service = email_service
defnotify(self, message):
self.email_service.send_email(message)# Create containerclassContainer(containers.DeclarativeContainer):
email_service = providers.Singleton(EmailService)
user_notifier = providers.Factory(UserNotifier, email_service=email_service)from dependency_injector.wiring import inject, Provide
@injectdefmain(notifier: UserNotifier = Provide[Container.user_notifier]):
notifier.notify("Hello!")if __name__ =="__main__":
container = Container()
container.wire(modules=[__name__])# This wires the current module
main()Được hỗ trợ bởi
Trong phiên bản hiện tại;
Không cần thêm dependency thủ công, decorator @inject tự động inject dependency của hàm hoặc lớp.
Lớp Provide cho dependency-injector biết cần inject dependency nào.
Cách tiếp cận này loại bỏ nhu cầu wiring dependency thủ công từ container.
Nó hữu ích trong các ứng dụng lớn nơi dịch vụ có thể phụ thuộc vào nhiều thành phần (do đó cần khởi tạo tự động).
Sử dụng Dependency Injection trong các Framework Python phổ biến
Hầu hết các framework Python hiện đại giúp bạn làm việc với dependency injection dễ dàng, cho phép viết mã gọn gàng và dễ kiểm thử hơn.
Trong phần này, bạn sẽ học cách áp dụng dependency injection trong Flask, Django và FastAPI.
Dependency injection trong Flask
Flask không có hỗ trợ sẵn cho dependency injection, nhưng bạn có thể triển khai bằng thư viện dependency-injector theo các bước sau.
Bước 1: Định nghĩa dịch vụ của bạn
classGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Được hỗ trợ bởi
Bước 2: Thiết lập container dependency injection
from dependency_injector import containers, providers
classContainer(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)Được hỗ trợ bởi
Bước 3: Tạo ứng dụng Flask và inject dependency.
from flask import Flask, request
from dependency_injector.wiring import inject, Provide
container = Container()
app = Flask(__name__)@app.route("/greet")@injectdefgreet(greeting_service: GreetingService = Provide[Container.greeting_service]):
name = request.args.get("name","World")return greeting_service.get_greeting(name)
container.wire(modules=[__name__])if __name__ =="__main__":
app.run(debug=True)Được hỗ trợ bởi
Từ đoạn mã trên:
Hàm name = request.args.get(“name”, “World”) trích xuất name từ query string và mặc định là ”World” nếu không được cung cấp.
return greeting_service.get_greeting(name) gọi phương thức trên dịch vụ được inject và trả về thông điệp chào.
container.wire(modules=[__name__]) yêu cầu dependency injector quét module này để tìm các decorator @inject và marker Provide[...] nhằm kết nối chúng với provider thực tế.
Bước 4: Kiểm thử ứng dụng
Chạy ứng dụng trên terminal bằng lệnh python <name_of_file>.py, và truy cập URL http://localhost:5000/greet?name=Jacob.
Bạn sẽ thấy như sau.
Dependency injection trong Django
Tương tự Flask, Django không cung cấp dependency injection tích hợp sẵn. Tuy nhiên, bạn có thể dùng class-based view, middleware và cấu trúc app để tích hợp DI thủ công hoặc dùng thư viện dependency-injector.
Dưới đây là quy trình từng bước để triển khai dependency injection trong Django.
Bước 1: Định nghĩa dịch vụ
Tạo một dịch vụ đơn giản chứa business logic trong file mới services.py dưới thư mục app của bạn.
# myapp/services.pyclassGreetingService:defget_greeting(self, name):returnf"Hello, {name}!"Được hỗ trợ bởi
Bước 2: Thiết lập container dependency injector
Bên trong thư mục ứng dụng của bạn, tạo một container bằng thư viện dependency-injector trong file mới containers.py.
Khởi động máy chủ bằng lệnh uvicorn main:app –reload và truy cập URL http://localhost:8000/greet?name=FastAPI. Bạn sẽ thấy phản hồi sau.
So sánh các Framework Dependency Injector cho Python
Có nhiều framework Dependency cho Python, mỗi cái có thế mạnh, cú pháp và trường hợp sử dụng riêng. Lựa chọn framework phù hợp phụ thuộc vào quy mô và cấu trúc dự án của bạn.
Dưới đây là so sánh một số framework dependency injection phổ biến cho Python.
Cung cấp dependency injection cho view và middleware
Phụ thuộc vào injector
Các mẫu triển khai nâng cao
Khi codebase của bạn trở nên lớn, dependency injection trở nên khó duy trì.
Bạn có thể dùng DI nâng cao để xử lý các tác vụ như tài nguyên theo phạm vi request, dọn dẹp, và tách biệt mối quan tâm.
Dependency theo phạm vi (scoped)
Scoped dependency cho phép bạn kiểm soát thời gian tồn tại của một dependency, ví dụ khi bạn muốn một đối tượng mới cho mỗi request, một đối tượng dùng chung trong vòng đời ứng dụng, hoặc tự động dọn dẹp tài nguyên như kết nối cơ sở dữ liệu.
Hầu hết các framework DI hỗ trợ các scope như:
Singleton: Một instance cho toàn bộ ứng dụng.
Factory: Tạo instance mới mỗi lần được inject.
Thread-local hoặc theo request: Một instance cho mỗi thread/request.
Resource: Được quản lý với logic khởi tạo và hủy (setup/teardown).
Dưới đây là ví dụ về scoped dependency với container lồng nhau dùng dependency-injector.
from dependency_injector import containers, providers, resources
# A simple resource that needs setup and cleanupclassMyResource:def__enter__(self):print("Setting up the resource")return self
def__exit__(self, exc_type, exc_val, exc_tb):print("Cleaning up the resource")# Create a container to manage the resourceclassMyContainer(containers.DeclarativeContainer):
resource = providers.Resource(MyResource)# Using the resource with a context manager
container = MyContainer()with container.resource()as r:print("Using the resource")Được hỗ trợ bởi
Trong ví dụ trên, MyResource mô phỏng một lớp cần setup và teardown, như mở/đóng file hoặc cơ sở dữ liệu.
container cung cấp resource bằng providers.Resource, và khi bạn dùng with container.resource(), nó vào và thoát ngữ cảnh đúng cách.
Mẫu này đảm bảo các tài nguyên như handle file, kết nối cơ sở dữ liệu, hoặc phiên mạng được quản lý sạch sẽ.
Hãy hình dung một endpoint API cần một phiên cơ sở dữ liệu cho request hiện tại hoặc một logger có ngữ cảnh theo request.
Với scoped dependency, framework tạo một container theo phạm vi request khi request bắt đầu và resolve mọi dependency từ scope đó.
Khi request kết thúc, mọi việc dọn dẹp cần thiết, như đóng phiên cơ sở dữ liệu, sẽ diễn ra tự động. Điều này đảm bảo trạng thái không bị chia sẻ giữa các request và tránh rò rỉ tài nguyên.
Injection bất đồng bộ (asynchronous)
Để xử lý hiệu quả các tác vụ I/O-bound, bạn cần thực thi bất đồng bộ. Hệ thống DI trong các framework như FastAPI và dependency-injector hỗ trợ injection bất đồng bộ dùng async/await.
Asynchronous injection là việc inject các dependency là hàm async def hoặc quản lý async resources như kết nối DB async, API bên ngoài, hoặc tác vụ nền.
Thay vì resolve một cách đồng bộ, hệ thống DI sẽ chờ chúng được resolve trước khi inject.
Dưới đây là ví dụ về dependency async trong FastAPI:
from fastapi import FastAPI, Depends
app = FastAPI()# Async dependencyasyncdefget_user():# Simulate async I/Oawait some_async_db_query()return{"name":"Philip Jones"}@app.get("/profile")asyncdefread_profile(user:dict= Depends(get_user)):return{"user": user}Được hỗ trợ bởi
FastAPI tự động xử lý việc resolve bất đồng bộ của get_user và inject kết quả.
dependency-injector cũng hỗ trợ quản lý vòng đời async bằng provider AsyncResource.
from dependency_injector import containers, providers, resources
classAsyncDB:asyncdefconnect(self):print("Connected")return self
asyncdefdisconnect(self):print("Disconnected")classAsyncDBResource(resources.AsyncResource):asyncdefinit(self)-> AsyncDB:
db = AsyncDB()returnawait db.connect()asyncdefshutdown(self, db: AsyncDB):await db.disconnect()classContainer(containers.DeclarativeContainer):
db = providers.Resource(AsyncDBResource)# Usageasyncdefmain():
container = Container()await container.init_resources()
db =await container.db()# Get the AsyncDB instance# Use db here...await container.shutdown_resources()Được hỗ trợ bởi
Trong đoạn mã trên:
resources.AsyncResource được dùng cho logic setup/teardown async.
await container.init_resources() khởi tạo tất cả resource và gọi init.
await container.shutdown_resources() dọn dẹp tất cả resource.
await container.db() lấy instance resource thực tế, là AsyncDB.
Sử dụng async DI giúp ứng dụng vận hành hiệu quả bằng cách tránh I/O đồng bộ trong các route bất đồng bộ, và loại bỏ nhu cầu tự await dependency trong logic của bạn.
Hệ quả bảo mật
Mặc dù DI cải thiện tính mô-đun và khả năng kiểm thử, nó có thể mang đến rủi ro bảo mật nếu không được quản lý cẩn thận.
Một rủi ro đáng chú ý là dependency confusion, khi các gói độc hại giả mạo gói hợp pháp. Ví dụ, kẻ tấn công đã tải lên các gói lên PyPI với tên được dùng trong hệ thống nội bộ của doanh nghiệp để lừa ứng dụng sử dụng phiên bản độc hại.
Để an toàn, hãy đảm bảo bạn không bao giờ inject các thành phần không tin cậy hoặc do người dùng kiểm soát và xác thực nguồn cấu hình, như biến môi trường, trước khi inject.
Sử dụng file requirements.txt hoặc poetry.lock để khóa phiên bản chính xác, và tự động quét bảo mật khi thay đổi dependency. Ngoài ra, đảm bảo thường xuyên kiểm toán các dependency để phát hiện lỗ hổng.
Tập trung hóa việc kiểm soát dependency cũng rất quan trọng, vì nó đảm bảo tính nhất quán giữa các ứng dụng và giúp áp dụng bản vá bảo mật hoặc cập nhật phiên bản dễ dàng hơn. Nó giúp tránh các "shadow dependency" được inject theo nhiều cách khác nhau trong codebase.
Một số công cụ có thể phát hiện rủi ro và cải thiện tư thế bảo mật của bạn:
Snyk: Quét các lỗ hổng đã biết trong gói Python và đề xuất cách khắc phục.
Safety: Kiểm tra các gói không an toàn và lỗ hổng trong mã.
Bandit: Trình phân tích mã tĩnh cho Python tập trung vào vấn đề bảo mật.
Ứng dụng thực tế
Dependency injection không chỉ là lý thuyết; nó được dùng trong nhiều hệ thống sản xuất để quản lý độ phức tạp, cải thiện tính linh hoạt và đơn giản hóa kiểm thử.
Cấu hình dịch vụ web
Trong các ứng dụng web hiện đại, DI đóng vai trò quan trọng trong quản lý các dịch vụ chuẩn như xác thực, logging và truy cập cơ sở dữ liệu.
Các framework như FastAPI đóng vai trò thiết yếu trong việc resolve route và dependency.
Ví dụ dưới đây minh họa một triển khai xác thực tập trung trong FastAPI.
from fastapi import Depends, HTTPException
defget_current_user(token:str= Depends(oauth2_scheme)):
user = verify_token(token)ifnot user:raise HTTPException(status_code=401)return user
@app.get("/protected")defprotected_route(user:dict= Depends(get_current_user)):return{"message":f"Hello {user['name']}"}Được hỗ trợ bởi
Trong đoạn mã trên, get_current_user() được inject làm dependency của route, nhưng logic kiểm tra token được tập trung và có thể tái sử dụng.
Bạn cũng có thể làm tương tự cho truy cập cơ sở dữ liệu, để các phiên làm việc được tự động quản lý và dọn dẹp.
Dưới đây là ví dụ cách dùng DI để tập trung hóa truy cập cơ sở dữ liệu.
from sqlalchemy.ext.asyncio import AsyncSession
asyncdefget_db_session()-> AsyncSession:asyncwith async_session()as session:yield session
@app.get("/items")asyncdefread_items(db: AsyncSession = Depends(get_db_session)):returnawait db.execute("SELECT * FROM items")Được hỗ trợ bởi
Trong đoạn mã trên, get_db_session là một nhà cung cấp dependency. Khi route read_items được gọi, FastAPI tự động inject tham số db, cho phép route truy cập cơ sở dữ liệu mà không cần tự thiết lập hay dọn dẹp phiên.
Phát triển hướng kiểm thử (TDD)
Dependency injection giúp bạn dễ dàng thay đổi các dependency mà thành phần nhận, điều này rất quan trọng khi kiểm thử.
Bạn có thể thay dịch vụ thật bằng mock trong khi kiểm thử, khiến mã của bạn mô-đun hơn và dễ kiểm thử hơn.
FastAPI và dependency-injector hỗ trợ ghi đè dependency tại thời điểm kiểm thử.
Dưới đây là ví dụ trong FastAPI.
from fastapi.testclient import TestClient
from main import app, get_db_session
defoverride_db():return TestDBSession()
app.dependency_overrides[get_db_session]= override_db
client = TestClient(app)
response = client.get("/items")assert response.status_code ==200Được hỗ trợ bởi
Trong ví dụ trên, get_db_session được ghi đè bằng cơ sở dữ liệu giả trong quá trình kiểm thử; do đó không cần vá (patch) hoặc thay đổi mã sản xuất.
Tương tự trong depedency-injector, bạn có thể ghi đè cơ sở dữ liệu thật bằng cơ sở dữ liệu giả để kiểm thử, như ví dụ dưới đây.
from containers import Container
deftest_service_behavior():
container = Container()
container.db.override(providers.Factory(TestDB))
service = container.service()assert service.get_data()=="mocked"Được hỗ trợ bởi
Phương thức override() thay thế dependency thật bằng mock, một cách sạch sẽ, có kiểm soát và có thể đảo ngược.
Dưới đây là một số chiến lược bạn nên dùng để ghi đè dependency:
Constructor injection: Đảm bảo bạn truyền mock trực tiếp vào constructor.
Ghi đè trong framework: Dùng công cụ ghi đè tích hợp như FastAPI.dependency_overrides trong FastAPI hoặc dependency_injector.override() trong dependency-injector.
Fixture: Dùng framework kiểm thử như pytest để tạo mock dùng lại và inject qua fixture.
Thực hành tốt và các phản mẫu (anti-pattern)
Mặc dù DI nhằm giúp mã mô-đun và dễ kiểm thử, nó có thể dẫn đến lỗi hoặc tạo độ phức tạp trong mã.
Khuyến nghị thực hành
Dưới đây là một số thực hành tốt bạn nên tuân thủ khi tạo dependency:
Lập trình hướng giao diện, không hướng triển khai: Định nghĩa dependency bằng abstract base class hoặc interface thay vì các triển khai phức tạp. Điều này giúp mã dễ kiểm thử và khuyến khích dùng mock trong unit test.
Ưu tiên composition hơn kế thừa: Thay vì xây dựng hệ phân cấp lớp phức tạp, hãy cấu thành đối tượng bằng các dependency được inject để giữ thành phần nhỏ gọn và tập trung.
Cấu hình dependency tập trung: Sử dụng một container hoặc module duy nhất để định nghĩa và quản lý dependency, giúp việc ghi đè và kiểm thử dễ dàng hơn.
Tránh dependency vòng tròn: Thiết kế cẩn thận đồ thị dependency để ngăn tham chiếu vòng tròn bằng cách sử dụng factory function hoặc provider để trì hoãn khởi tạo.
Những lỗi thường gặp
Dưới đây là một số lỗi thường gặp bạn nên tránh khi dùng DI.
Phản mẫu service locator: Tránh dùng container toàn cục để lấy dependency động, vì có thể che giấu dependency và làm mã khó hiểu, khó kiểm thử.
Over-injection: Tránh inject quá nhiều dịch vụ vào một lớp hoặc hàm, để không dẫn đến constructor cồng kềnh và logic khó kiểm thử.
Quản lý scope kém và rò rỉ trạng thái: Đảm bảo luôn dùng đúng scope để tránh chia sẻ trạng thái giữa người dùng hoặc bài kiểm thử.
Kiểm thử Dependency Injection trong Python
Dependency injection giúp dễ dàng inject các đối tượng mock vào thành phần của bạn, từ đó đơn giản hóa unit test. Điều này cho phép bạn tách các thành phần cốt lõi khỏi các thành phần dành riêng cho kiểm thử.
Với DI, các bài test có tính quyết định, không có tác dụng phụ và chạy nhanh hơn.
Dưới đây là ví dụ cho thấy cách bạn có thể dùng DI để inject các phiên bản mock, stub, hoặc fake của những dependency trong bài kiểm thử.
classTestEmailService:defsend_email(self, to, message):
self.sent_to = to
deftest_notifier_sends_email():
test_email = TestEmailService()
notifier = Notifier(test_email)
notifier.alert("[email protected]")assert test_email.sent_to =="[email protected]"Được hỗ trợ bởi
Hãy dùng DI thường xuyên và tránh monkey patching để dễ bảo trì và tránh thay đổi phá vỡ khi tên nội bộ thay đổi.
Kết luận
Mặc dù ban đầu bạn có thể chưa cần DI cho các dự án nhỏ, nhưng khi dự án lớn lên và mở rộng, bạn sẽ hưởng lợi từ cấu trúc và tính mô-đun mà DI mang lại.
Nhìn về phía trước, tương lai của DI trong Python rất hứa hẹn, và chúng ta có thể kỳ vọng tiếp tục có hệ thống kiểu (type system) được cải thiện và quản lý vòng đời async tốt hơn.
Thành thạo DI là điều thiết yếu để trở thành một lập trình viên Python thuần thục. Nhưng chưa dừng lại ở đó; còn nhiều khái niệm nâng cao khác có thể nâng tầm kỹ năng Python của bạn.
Để giúp bạn tăng tốc lộ trình học tập, dưới đây là một số tài nguyên bạn nên tham khảo.
Giảng viên khoa học dữ liệu giàu kinh nghiệm và Nhà thống kê sinh học, có chuyên môn về Python, R và học máy.
FAQs
Dependency Injection là gì?⌄
Dependency injection là một mẫu thiết kế giúp bạn viết mã sạch hơn bằng cách tạo các dependency trong một lớp, có thể được truyền từ bên ngoài, khiến mã của bạn dễ quản lý và chỉnh sửa hơn.
Lợi ích chính của Dependency Injection là gì?⌄
Dependency injection giúp mã của bạn mô-đun, dễ kiểm thử và dễ bảo trì.
Làm thế nào để triển khai dependency injection trong Python?⌄
Bạn có thể triển khai DI trong Python thủ công hoặc bằng các thư viện như dependency_injector.
Asynchronous Injection là gì?⌄
Asynchronous injection là việc inject các dependency là hàm async def hoặc quản lý async resources như kết nối DB async, API bên ngoài, hoặc tác vụ nền.
Các rủi ro bảo mật của dependency injection là gì và tôi xử lý chúng như thế nào?⌄
Một rủi ro đáng kể là dependency confusion, khi các gói độc hại giả mạo gói hợp pháp. Để ngăn chặn, bạn có thể dùng các công cụ như Snyk, Xygeni, Safety hoặc Bandit để quét mã và gói của bạn nhằm phát hiện lỗ hổng.
Dependency injection là một mẫu thiết kế trong đó các đối tượng hoặc dịch vụ (tức là các dependency) được cung cấp cho một lớp từ bên ngoài, thay vì lớp đó tự tạo bên trong. Điều này giúp tách rời các thành phần.
Việc sử dụng dependency injection trong Python mang lại nhiều lợi ích, chẳng hạn như:
Trong hướng dẫn này, tôi sẽ minh họa cách sử dụng dependency injection trong Python thông qua các ví dụ đơn giản, thực tế.
TL;DR
Dependency injection là một mẫu thiết kế trong đó một lớp nhận các dependency từ bên ngoài thay vì tự tạo. Nó giúp mã của bạn mô-đun hơn.
Bạn có thể inject dependency thông qua constructor, phương thức setter, hoặc trực tiếp vào phương thức, và Python hỗ trợ việc này thủ công hoặc thông qua các framework như
dependency-injector,injector, vàpunq.Các web framework phổ biến xử lý khác nhau: FastAPI có hỗ trợ sẵn qua
Depends(). Flask và Django cần thư viện nhưdependency-injector.Tìm hiểu Dependency Injection trong Python
Để hiểu cách triển khai dependency injection, điều cốt yếu là bạn nắm vững các nguyên tắc chính chi phối nó.
Dependency injection dựa trên nguyên tắc Inversion of Control (IoC), nghĩa là thay vì một lớp tự tạo và quản lý các dependency, nó nhận chúng từ nguồn bên ngoài. Điều này giúp tập trung vào một nhiệm vụ duy nhất và làm mã của bạn gọn gàng.
Inject dependency trong Python bao gồm những cách phổ biến sau, ngoài ra còn có các cách khác:
Vì sao nên dùng Dependency Injection trong Python?
Là một nhà phát triển, hiểu và triển khai dependency injection có thể tạo ra khác biệt lớn trong thiết kế mã của bạn. Hãy xem một số lợi ích của dependency injection.
Tính mô-đun, tái sử dụng và linh hoạt
Dependency injection cho phép bạn chia nhỏ mã thành các thành phần tập trung hơn, giúp tăng tính linh hoạt và mô-đun.
Mỗi phần hoặc thành phần xử lý một tác vụ cụ thể và dựa vào các dependency bên ngoài, giúp dễ dàng tái sử dụng ở những phần khác trong ứng dụng của bạn hoặc của người khác
Ví dụ, nếu bạn có một lớp gửi tin nhắn. Thay vì xây dựng logic email hoặc SMS trực tiếp trong dịch vụ của bạn, bạn có thể inject một lớp gửi email hoặc SMS.
Khi đó, lớp gửi tin nhắn của bạn không cần quan tâm tin nhắn được gửi như thế nào, mà chỉ sử dụng bộ gửi được cung cấp. Điều này ngụ ý rằng bạn có thể dùng bộ gửi tin nhắn với các bộ gửi email hoặc SMS khác nhau.
Dễ bảo trì và kiểm thử
Khi dùng dependency injection, bạn có thể dễ dàng thay thế dependency thật bằng dữ liệu giả (mock) khi kiểm thử. Điều này giúp viết unit test đơn giản hơn mà không phụ thuộc vào API hoặc cơ sở dữ liệu.
Giả sử, như một bài tập suy nghĩ, bạn có một lớp lưu dữ liệu vào cơ sở dữ liệu. Khi kiểm thử, bạn có thể inject một cơ sở dữ liệu giả thay vì cơ sở dữ liệu thật để tránh ràng buộc bài kiểm thử với cơ sở dữ liệu sản xuất.
Dependency injection cho phép bạn chỉnh sửa các phần cụ thể của mã mà không làm vỡ toàn bộ hệ thống. Điều này đặc biệt hữu ích khi cần chuyển nhà cung cấp thanh toán trong ứng dụng, ví dụ từ Stripe sang PayPal, mà không sửa đổi phần mã còn lại.
Đảo ngược kiểm soát và dependency có thể hoán đổi
Ý tưởng về đảo ngược kiểm soát là: dependency injection sẽ chuyển trách nhiệm tạo và quản lý dependency sang một nguồn bên ngoài.
Điều này có nghĩa bạn có thể dễ dàng hoán đổi một triển khai cho triển khai khác; ví dụ, nếu bạn có lớp
ReportGeneratorđịnh dạng và xuất báo cáo.Thay vì lớp tự quyết định định dạng xuất, bạn có thể inject một lớp xuất, như
PDFExporter,ExcelExporter, hoặcCSVExporter, vào nó.Lớp
ReportGeneratorkhông quan tâm việc xuất diễn ra thế nào, nên để đổi định dạng, bạn chỉ cần hoán đổi bộ xuất mà bạn inject, không cần chỉnh sửaReportGenerator.Triển khai Dependency Injection trong Python
Giờ bạn đã hiểu dependency injection là gì và vì sao hữu ích, hãy xem cách triển khai nó trong Python. Bắt đầu với phương pháp đơn giản nhất: dependency injection thủ công.
Dependency injection thủ công
Với dependency injection thủ công, bạn tự tạo các dependency và truyền chúng vào lớp hoặc hàm cần dùng.
Phương pháp này hoạt động tốt với ứng dụng nhỏ, nhưng khi ứng dụng mở rộng, bạn cần dùng một framework DI để tránh lỗi.
Dưới đây là ví dụ mã không dùng dependency injection
Trong ví dụ trên,
UserNotifiergắn chặt vớiEmailService. Bạn không thể dễ dàng hoán đổi hoặc mockEmailServiceđể kiểm thử.Dưới đây là một phiên bản khác, nhưng với dependency injection thủ công.
Phiên bản trên linh hoạt và cho phép bạn inject các dịch vụ nhắn tin khác nhau; bạn cũng có thể thay
EmailServicebằng mock trong kiểm thử.Sử dụng framework dependency-injector
Khi ứng dụng phát triển, việc quản lý dependency trở nên phức tạp và lộn xộn.
Framework
dependency-injectorcung cấp một cách tiếp cận có cấu trúc.Kiến trúc container-provider
Framework
dependency-injectorhoạt động dựa trên kiến trúc container-provider.Trong ví dụ trên:
Containerđịnh nghĩa cách tạo các instanceSingletonđảm bảo chỉ có một instance củaEmailServiceFactorytạo mộtUserNotifiermới vớiEmailServiceđược inject tự động.Cơ chế wiring
Bạn có thể dùng decorator như
@injectđể giảm nhu cầu truyền dependency thủ công.Dưới đây là một phiên bản khác của ví dụ trước:
Trong phiên bản hiện tại;
Không cần thêm dependency thủ công, decorator
@injecttự động inject dependency của hàm hoặc lớp.Lớp
Providechodependency-injectorbiết cần inject dependency nào.Cách tiếp cận này loại bỏ nhu cầu wiring dependency thủ công từ container.
Nó hữu ích trong các ứng dụng lớn nơi dịch vụ có thể phụ thuộc vào nhiều thành phần (do đó cần khởi tạo tự động).
Sử dụng Dependency Injection trong các Framework Python phổ biến
Hầu hết các framework Python hiện đại giúp bạn làm việc với dependency injection dễ dàng, cho phép viết mã gọn gàng và dễ kiểm thử hơn.
Trong phần này, bạn sẽ học cách áp dụng dependency injection trong Flask, Django và FastAPI.
Dependency injection trong Flask
Flask không có hỗ trợ sẵn cho dependency injection, nhưng bạn có thể triển khai bằng thư viện
dependency-injectortheo các bước sau.Bước 1: Định nghĩa dịch vụ của bạn
Bước 2: Thiết lập container dependency injection
Bước 3: Tạo ứng dụng Flask và inject dependency.
Từ đoạn mã trên:
Hàm
name = request.args.get(“name”, “World”)trích xuấtnametừ query string và mặc định là”World”nếu không được cung cấp.return greeting_service.get_greeting(name)gọi phương thức trên dịch vụ được inject và trả về thông điệp chào.container.wire(modules=[__name__])yêu cầu dependency injector quét module này để tìm các decorator@injectvà markerProvide[...]nhằm kết nối chúng với provider thực tế.Bước 4: Kiểm thử ứng dụng
Chạy ứng dụng trên terminal bằng lệnh
python <name_of_file>.py, và truy cập URLhttp://localhost:5000/greet?name=Jacob.Bạn sẽ thấy như sau.
Dependency injection trong Django
Tương tự Flask, Django không cung cấp dependency injection tích hợp sẵn. Tuy nhiên, bạn có thể dùng class-based view, middleware và cấu trúc app để tích hợp DI thủ công hoặc dùng thư viện
dependency-injector.Dưới đây là quy trình từng bước để triển khai dependency injection trong Django.
Bước 1: Định nghĩa dịch vụ
Tạo một dịch vụ đơn giản chứa business logic trong file mới
services.pydưới thư mục app của bạn.Bước 2: Thiết lập container dependency injector
Bên trong thư mục ứng dụng của bạn, tạo một container bằng thư viện
dependency-injectortrong file mớicontainers.py.Bước 3: Tạo View với dependency được inject
Dùng decorator
@injecttrong view để nhận dependency trongviews.py.Bước 4: Cập nhật urls.py
Trong thư mục dự án, vào file
urls.pyvà kết nối dependency injector vào hệ thống định tuyến URL của Django.Bước 5: Wire container trong apps.py
Vào
apps.pyvà wire container vào ứng dụng.Sau đó vào cấu hình app trong
INSTALLED_APPSvà thêm ứng dụng của bạn.Bước 6: Kiểm thử View
Khởi động máy chủ Django và truy cập view qua URL
http://localhost:8000/greet/?name=Django.Bạn sẽ thấy như sau.
Dependency Injection trong FastAPI
FastAPI có hỗ trợ sẵn cho dependency injection thông qua marker đặc biệt
Depends()để inject bất cứ thứ gì một hàm trả về.Dưới đây là hướng dẫn từng bước về dependency injection trong FastAPI.
Bước 1: Định nghĩa một dịch vụ
Bước 2: Tạo hàm dependency
Bước 3: Thiết lập ứng dụng FastAPI và dùng Dependency Injection
Bước 4: Chạy và kiểm thử ứng dụng
Khởi động máy chủ bằng lệnh
uvicorn main:app –reloadvà truy cập URLhttp://localhost:8000/greet?name=FastAPI. Bạn sẽ thấy phản hồi sau.So sánh các Framework Dependency Injector cho Python
Có nhiều framework Dependency cho Python, mỗi cái có thế mạnh, cú pháp và trường hợp sử dụng riêng. Lựa chọn framework phù hợp phụ thuộc vào quy mô và cấu trúc dự án của bạn.
Dưới đây là so sánh một số framework dependency injection phổ biến cho Python.
Framework
Phù hợp nhất cho
Thế mạnh
Hạn chế
dependency-injectorỨng dụng phức tạp trong môi trường sản xuất
Đầy đủ tính năng, nhanh và có thể cấu hình
Dài dòng
injectorĐơn giản và phù hợp cho ứng dụng tối giản
Cú pháp gọn gàng
Ít nâng cao hơn các lựa chọn khác.
pinjectLogic binding nâng cao
Tự động wiring
Chậm hơn do introspection
punqỨng dụng đơn giản
Tối giản và nhanh
Tính năng hạn chế so với các lựa chọn khác
FastAPI
Depends()Ứng dụng FastAPI
Hỗ trợ sẵn cho async và dễ kiểm thử
Không thể tái sử dụng ngoài FastAPI
Flask-injectorỨng dụng Flask
Dễ tích hợp với ứng dụng Flask
Phụ thuộc vào
injectordjango-injectorDự án Django
Cung cấp dependency injection cho view và middleware
Phụ thuộc vào
injectorCác mẫu triển khai nâng cao
Khi codebase của bạn trở nên lớn, dependency injection trở nên khó duy trì.
Bạn có thể dùng DI nâng cao để xử lý các tác vụ như tài nguyên theo phạm vi request, dọn dẹp, và tách biệt mối quan tâm.
Dependency theo phạm vi (scoped)
Scoped dependency cho phép bạn kiểm soát thời gian tồn tại của một dependency, ví dụ khi bạn muốn một đối tượng mới cho mỗi request, một đối tượng dùng chung trong vòng đời ứng dụng, hoặc tự động dọn dẹp tài nguyên như kết nối cơ sở dữ liệu.
Hầu hết các framework DI hỗ trợ các scope như:
Dưới đây là ví dụ về scoped dependency với container lồng nhau dùng
dependency-injector.Trong ví dụ trên,
MyResourcemô phỏng một lớp cần setup và teardown, như mở/đóng file hoặc cơ sở dữ liệu.containercung cấp resource bằngproviders.Resource, và khi bạn dùngwith container.resource(), nó vào và thoát ngữ cảnh đúng cách.Mẫu này đảm bảo các tài nguyên như handle file, kết nối cơ sở dữ liệu, hoặc phiên mạng được quản lý sạch sẽ.
Hãy hình dung một endpoint API cần một phiên cơ sở dữ liệu cho request hiện tại hoặc một logger có ngữ cảnh theo request.
Với scoped dependency, framework tạo một container theo phạm vi request khi request bắt đầu và resolve mọi dependency từ scope đó.
Khi request kết thúc, mọi việc dọn dẹp cần thiết, như đóng phiên cơ sở dữ liệu, sẽ diễn ra tự động. Điều này đảm bảo trạng thái không bị chia sẻ giữa các request và tránh rò rỉ tài nguyên.
Injection bất đồng bộ (asynchronous)
Để xử lý hiệu quả các tác vụ I/O-bound, bạn cần thực thi bất đồng bộ. Hệ thống DI trong các framework như FastAPI và
dependency-injectorhỗ trợ injection bất đồng bộ dùngasync/await.Asynchronous injection là việc inject các dependency là hàm
async defhoặc quản lýasync resourcesnhư kết nối DB async, API bên ngoài, hoặc tác vụ nền.Thay vì resolve một cách đồng bộ, hệ thống DI sẽ chờ chúng được resolve trước khi inject.
Dưới đây là ví dụ về dependency async trong FastAPI:
FastAPI tự động xử lý việc resolve bất đồng bộ của
get_uservà inject kết quả.dependency-injectorcũng hỗ trợ quản lý vòng đời async bằng providerAsyncResource.Trong đoạn mã trên:
resources.AsyncResourceđược dùng cho logic setup/teardown async.await container.init_resources()khởi tạo tất cả resource và gọiinit.await container.shutdown_resources()dọn dẹp tất cả resource.await container.db()lấy instance resource thực tế, làAsyncDB.Sử dụng async DI giúp ứng dụng vận hành hiệu quả bằng cách tránh I/O đồng bộ trong các route bất đồng bộ, và loại bỏ nhu cầu tự await dependency trong logic của bạn.
Hệ quả bảo mật
Mặc dù DI cải thiện tính mô-đun và khả năng kiểm thử, nó có thể mang đến rủi ro bảo mật nếu không được quản lý cẩn thận.
Một rủi ro đáng chú ý là dependency confusion, khi các gói độc hại giả mạo gói hợp pháp. Ví dụ, kẻ tấn công đã tải lên các gói lên PyPI với tên được dùng trong hệ thống nội bộ của doanh nghiệp để lừa ứng dụng sử dụng phiên bản độc hại.
Để an toàn, hãy đảm bảo bạn không bao giờ inject các thành phần không tin cậy hoặc do người dùng kiểm soát và xác thực nguồn cấu hình, như biến môi trường, trước khi inject.
Sử dụng file
requirements.txthoặcpoetry.lockđể khóa phiên bản chính xác, và tự động quét bảo mật khi thay đổi dependency. Ngoài ra, đảm bảo thường xuyên kiểm toán các dependency để phát hiện lỗ hổng.Tập trung hóa việc kiểm soát dependency cũng rất quan trọng, vì nó đảm bảo tính nhất quán giữa các ứng dụng và giúp áp dụng bản vá bảo mật hoặc cập nhật phiên bản dễ dàng hơn. Nó giúp tránh các "shadow dependency" được inject theo nhiều cách khác nhau trong codebase.
Một số công cụ có thể phát hiện rủi ro và cải thiện tư thế bảo mật của bạn:
Ứng dụng thực tế
Dependency injection không chỉ là lý thuyết; nó được dùng trong nhiều hệ thống sản xuất để quản lý độ phức tạp, cải thiện tính linh hoạt và đơn giản hóa kiểm thử.
Cấu hình dịch vụ web
Trong các ứng dụng web hiện đại, DI đóng vai trò quan trọng trong quản lý các dịch vụ chuẩn như xác thực, logging và truy cập cơ sở dữ liệu.
Các framework như FastAPI đóng vai trò thiết yếu trong việc resolve route và dependency.
Ví dụ dưới đây minh họa một triển khai xác thực tập trung trong FastAPI.
Trong đoạn mã trên,
get_current_user()được inject làm dependency của route, nhưng logic kiểm tra token được tập trung và có thể tái sử dụng.Bạn cũng có thể làm tương tự cho truy cập cơ sở dữ liệu, để các phiên làm việc được tự động quản lý và dọn dẹp.
Dưới đây là ví dụ cách dùng DI để tập trung hóa truy cập cơ sở dữ liệu.
Trong đoạn mã trên,
get_db_sessionlà một nhà cung cấp dependency. Khi routeread_itemsđược gọi, FastAPI tự động inject tham sốdb, cho phép route truy cập cơ sở dữ liệu mà không cần tự thiết lập hay dọn dẹp phiên.Phát triển hướng kiểm thử (TDD)
Dependency injection giúp bạn dễ dàng thay đổi các dependency mà thành phần nhận, điều này rất quan trọng khi kiểm thử.
Bạn có thể thay dịch vụ thật bằng mock trong khi kiểm thử, khiến mã của bạn mô-đun hơn và dễ kiểm thử hơn.
FastAPI và
dependency-injectorhỗ trợ ghi đè dependency tại thời điểm kiểm thử.Dưới đây là ví dụ trong FastAPI.
Trong ví dụ trên,
get_db_sessionđược ghi đè bằng cơ sở dữ liệu giả trong quá trình kiểm thử; do đó không cần vá (patch) hoặc thay đổi mã sản xuất.Tương tự trong
depedency-injector, bạn có thể ghi đè cơ sở dữ liệu thật bằng cơ sở dữ liệu giả để kiểm thử, như ví dụ dưới đây.Phương thức
override()thay thế dependency thật bằng mock, một cách sạch sẽ, có kiểm soát và có thể đảo ngược.Dưới đây là một số chiến lược bạn nên dùng để ghi đè dependency:
FastAPI.dependency_overridestrong FastAPI hoặcdependency_injector.override()trongdependency-injector.pytestđể tạo mock dùng lại và inject qua fixture.Thực hành tốt và các phản mẫu (anti-pattern)
Mặc dù DI nhằm giúp mã mô-đun và dễ kiểm thử, nó có thể dẫn đến lỗi hoặc tạo độ phức tạp trong mã.
Khuyến nghị thực hành
Dưới đây là một số thực hành tốt bạn nên tuân thủ khi tạo dependency:
Những lỗi thường gặp
Dưới đây là một số lỗi thường gặp bạn nên tránh khi dùng DI.
Kiểm thử Dependency Injection trong Python
Dependency injection giúp dễ dàng inject các đối tượng mock vào thành phần của bạn, từ đó đơn giản hóa unit test. Điều này cho phép bạn tách các thành phần cốt lõi khỏi các thành phần dành riêng cho kiểm thử.
Với DI, các bài test có tính quyết định, không có tác dụng phụ và chạy nhanh hơn.
Dưới đây là ví dụ cho thấy cách bạn có thể dùng DI để inject các phiên bản mock, stub, hoặc fake của những dependency trong bài kiểm thử.
Trong bài kiểm thử, thay
EmailServicebằng mock:Hãy dùng DI thường xuyên và tránh monkey patching để dễ bảo trì và tránh thay đổi phá vỡ khi tên nội bộ thay đổi.
Kết luận
Mặc dù ban đầu bạn có thể chưa cần DI cho các dự án nhỏ, nhưng khi dự án lớn lên và mở rộng, bạn sẽ hưởng lợi từ cấu trúc và tính mô-đun mà DI mang lại.
Nhìn về phía trước, tương lai của DI trong Python rất hứa hẹn, và chúng ta có thể kỳ vọng tiếp tục có hệ thống kiểu (type system) được cải thiện và quản lý vòng đời async tốt hơn.
Thành thạo DI là điều thiết yếu để trở thành một lập trình viên Python thuần thục. Nhưng chưa dừng lại ở đó; còn nhiều khái niệm nâng cao khác có thể nâng tầm kỹ năng Python của bạn.
Để giúp bạn tăng tốc lộ trình học tập, dưới đây là một số tài nguyên bạn nên tham khảo.
Khóa học:
Bài viết blog: