الحقن بالاعتماد: طريقة بايثون
أنماط حقن التبعيات في بايثون لكتابة كود نظيف وقابل للاختبار
الحقول المُستخدمة (DI) هي نمط تصميم أساسي يعزز كتابة الكود النظيف، القابل للاختبار، والمُحافظ عليه في تطبيقات بايثون.
سواء كنت تبني واجهات برمجية REST مع FastAPI، أو تُنفِّذ اختبارات وحدة، أو تعمل مع وظائف AWS Lambda، فإن فهم الحقول المُستخدمة سيُحسِّن جودة كودك بشكل كبير.

ما هي الحقول المُستخدمة؟
الحقول المُستخدمة هي نمط تصميم حيث تقبل المكونات اعتماداتها من مصادر خارجية بدلًا من إنشائها داخليًا. هذه الطريقة تفكك المكونات، مما يجعل كودك أكثر قابلية للصيانة، والاختبار، والمرونة.
في بايثون، تُعتبر الحقول المُستخدمة قوية بشكل خاص بسبب طبيعة اللغة الديناميكية ودعمها للبروتوكولات، والصفات الأساسية، والتصنيف الدودي. المرونة التي توفرها بايثون تعني أنك تستطيع تنفيذ أنماط الحقول المُستخدمة دون الحاجة إلى الإطارات الثقيلة، على الرغم من توفر الإطارات عندما تكون مطلوبة.
لماذا استخدام الحقول المُستخدمة في بايثون؟
تحسين قابلية الاختبار: من خلال حقول المُستخدمة، يمكنك بسهولة استبدال التنفيذات الفعلية بمُحاكاتها أو نماذج الاختبار. هذا يسمح لك بكتابة اختبارات وحدة سريعة ومُستقلة لا تتطلب خدمات خارجية مثل قواعد البيانات أو الواجهات.
عند كتابة اختبارات وحدة شاملة، تجعل الحقول المُستخدمة من السهل أن تُبادل الاعتماديات الفعلية بنماذج اختبار.
تحسين الصيانة: تصبح الاعتماديات واضحة في كودك. عند النظر إلى المُنشئ، ترى فورًا ما تحتاجه المكونة. هذا يجعل قاعدة الكود أسهل لفهمها وتعديلها.
الربط المرن: تعتمد المكونات على المجرات (البروتوكولات أو ABCs) بدلًا من التنفيذات المحددة. هذا يعني أنك تستطيع تغيير التنفيذات دون التأثير على الكود المعتمد.
المرونة: يمكنك تكوين تنفيذات مختلفة لكل بيئات (التطوير، الاختبار، الإنتاج) دون تغيير منطق العمل. هذا مفيد بشكل خاص عند نشر تطبيقات بايثون على منصات مختلفة، سواء كانت AWS Lambda أو الخوادم التقليدية.
حقول المُستخدمة عبر المُنشئ: الطريقة البايثونية
أفضل طريقة واسطة لتنفيذ حقول المُستخدمة في بايثون هي عبر حقول المُنشئ—تقبل الاعتماديات كمعلمات في طريقة __init__.
مثال بسيط
إليك مثال بسيط يوضح حقول المُستخدمة عبر المُنشئ:
from typing import Protocol
from abc import ABC, abstractmethod
# تعريف بروتوكول للمخزن
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# الخدمة تعتمد على بروتوكول المخزن
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, user_id: int) -> 'User | None':
return self.repo.find_by_id(user_id)
هذا النمط يجعل من الواضح أن UserService يحتاج إلى UserRepository. لا يمكنك إنشاء UserService دون توفير مخزن، مما يمنع الأخطاء عند غياب الاعتماديات.
عدة اعتماديات
عندما يكون لدى المكون عدة اعتماديات، أضفها كمعلمات للمُنشئ:
class EmailService(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
class Logger(Protocol):
def info(self, msg: str) -> None:
...
def error(self, msg: str, err: Exception) -> None:
...
class OrderService:
def __init__(
self,
repo: OrderRepository,
email_svc: EmailService,
logger: Logger,
payment_svc: PaymentService,
):
self.repo = repo
self.email_svc = email_svc
self.logger = logger
self.payment_svc = payment_svc
استخدام البروتوكولات والصفات الأساسية
إحدى المبادئ الرئيسية عند تنفيذ الحقول المُستخدمة هي مبدأ انعكاس الاعتماد (DIP): يجب ألا تعتمد الوحدات العليا على الوحدات الدنيا؛ بل يجب أن تعتمد كل منهما على المجرات.
في بايثون، يمكنك تعريف المجرات باستخدام البروتوكولات (التصنيف الهيكلي) أو الصفات الأساسية (ABCs) (التصنيف الاسمي).
البروتوكولات (Python 3.8+)
تستخدم البروتوكولات التصنيف الهيكلي—إذا كان الكائن يحتوي على الأساليب المطلوبة، فإنه يحقق البروتوكول:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# أي فئة تحتوي على طريقة process_payment تحقق هذا البروتوكول
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# منطق بطاقة الائتمان
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# منطق باي بال
return True
# الخدمة تقبل أي PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
الصفات الأساسية
تستخدم الصفات الأساسية التصنيف الاسمي—يجب أن ترث الفئات صراحة من الصفات الأساسية:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
return True
متى تستخدم البروتوكولات مقابل الصفات الأساسية: استخدم البروتوكولات عندما ترغب في التصنيف الهيكلي والمرونة. استخدم الصفات الأساسية عندما تحتاج إلى تقييد التراث أو تقديم تنفيذات افتراضية.
مثال عملي: تأسيس قاعدة بيانات
عند العمل مع قواعد بيانات في تطبيقات بايثون، ستحتاج غالبًا إلى تأسيس عمليات قاعدة البيانات. إليك كيف تساعدك الحقول المُستخدمة:
from typing import Protocol, Optional
from contextlib import contextmanager
class Database(Protocol):
@contextmanager
def transaction(self):
...
def execute(self, query: str, params: dict) -> None:
...
def fetch_one(self, query: str, params: dict) -> Optional[dict]:
...
# المخزن يعتمد على المجرة
class UserRepository:
def __init__(self, db: Database):
self.db = db
def find_by_id(self, user_id: int) -> Optional['User']:
result = self.db.fetch_one(
"SELECT * FROM users WHERE id = :id",
{"id": user_id}
)
if result:
return User(**result)
return None
هذا النمط يسمح لك بتغيير تنفيذات قاعدة البيانات (PostgreSQL، SQLite، MongoDB) دون تغيير كود المخزن.
نمط الجذر التكويني
الجذر التكويني هو المكان الذي تُكوِّن فيه جميع الاعتماديات عند نقطة دخول التطبيق (عادة main.py أو مصنع التطبيق). هذا يركز تكوين الاعتماديات ويجعل الرسم البياني للاعتماديات واضحًا.
def create_app() -> FastAPI:
app = FastAPI()
# تهيئة الاعتماديات الأساسية
db = init_database()
logger = init_logger()
# تهيئة المخازن
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# تهيئة الخدمة مع الاعتماديات
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# تهيئة معالجات HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# ربط الطرق
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
هذا النهج يجعل من الواضح كيفية بناء تطبيقك ومكان أصل الاعتماديات. إنه مفيد بشكل خاص عند بناء تطبيقات تتبع مبادئ البنية النظيفة، حيث تحتاج إلى تنسيق طبقات متعددة من الاعتماديات.
الإطارات للحقول المُستخدمة
لتطبيقات أكبر مع رسم بياني معقد للاعتماديات، قد يصبح إدارة الاعتماديات يدويًا مرهقًا. لدى بايثون عدة إطارات للحقول المُستخدمة يمكن أن تساعد:
Dependency Injector
Dependency Injector هي إطارة شهيرة توفر نهجًا قائمًا على الحاويات للحقول المُستخدمة.
التركيب:
pip install dependency-injector
مثال:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# التكوين
config = providers.Configuration()
# قاعدة البيانات
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# المخازن
user_repository = providers.Factory(
UserRepository,
db=db
)
# الخدمة
user_service = providers.Factory(
UserService,
repo=user_repository
)
# الاستخدام
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector هي مكتبة خفيفة مستوحاة من Guice من Google، تركز على البساطة.
التركيب:
pip install injector
مثال:
from injector import Injector, inject, Module, provider
class DatabaseModule(Module):
@provider
def provide_db(self) -> Database:
return Database(connection_string="...")
class UserModule(Module):
@inject
def __init__(self, repo: UserRepository):
self.repo = repo
injector = Injector([DatabaseModule()])
user_service = injector.get(UserService)
متى تستخدم الإطارات
استخدم إطارة عندما:
- يكون رسم بياني الاعتماديات معقدًا مع العديد من المكونات المعتمدة
- لديك تنفيذات متعددة لنفس الواجهة تحتاج إلى اختيارها بناءً على التكوين
- ترغب في حل الاعتماديات تلقائيًا
- تبني تطبيقًا كبيرًا حيث يصبح التوصيل اليدوي خطيرًا
استخدم الحقول المُستخدمة يدويًا عندما:
- تطبيقك صغير أو متوسط الحجم
- رسم بياني الاعتماديات بسيط وسهل المتابعة
- ترغب في الحفاظ على الاعتماديات قليلة ومحددة
- تفضل الكود الواضح على السحر الإطاري
اختبار مع الحقول المُستخدمة
أحد الفوائد الرئيسية للحقول المُستخدمة هو تحسين قابلية الاختبار. إليك كيف تجعل الحقول المُستخدمة اختبارات أسهل:
مثال على اختبار الوحدة
from unittest.mock import Mock
import pytest
# تنفيذ مُحاكي للاختبار
class MockUserRepository:
def __init__(self):
self.users = {}
self.error = None
def find_by_id(self, user_id: int) -> Optional['User']:
if self.error:
raise self.error
return self.users.get(user_id)
def save(self, user: 'User') -> 'User':
if self.error:
raise self.error
self.users[user.id] = user
return user
# اختبار باستخدام المُحاكي
def test_user_service_get_user():
mock_repo = MockUserRepository()
mock_repo.users[1] = User(id=1, name="John", email="john@example.com")
service = UserService(mock_repo)
user = service.get_user(1)
assert user is not None
assert user.name == "John"
هذا الاختبار يعمل بسرعة، لا يتطلب قاعدة بيانات، ويختبر منطق العمل الخاص بك بشكل مفصول. عند العمل مع اختبارات الوحدة في بايثون، تجعل الحقول المُستخدمة من السهل إنشاء نماذج اختبار وتأكيد التفاعلات.
استخدام fixtures pytest
تعمل fixtures pytest بشكل ممتاز مع الحقول المُستخدمة:
@pytest.fixture
def mock_user_repository():
return MockUserRepository()
@pytest.fixture
def user_service(mock_user_repository):
return UserService(mock_user_repository)
def test_user_service_get_user(user_service, mock_user_repository):
user = User(id=1, name="John", email="john@example.com")
mock_user_repository.users[1] = user
result = user_service.get_user(1)
assert result.name == "John"
الأنماط الشائعة والممارسات المثلى
1. استخدام تقسيم الواجهات
احتفظ بالبروتوكولات والواجهات صغيرة ومُركزة على ما يحتاجه العميل فعليًا:
# جيد: العميل يحتاج فقط إلى قراءة المستخدمين
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# واجهة منفصلة للكتابة
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. تحقق من الاعتماديات في المُنشئ
يجب أن يتحقق المُنشئ من الاعتماديات ويُثير أخطاء واضحة إذا فشل التهيئة:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("user repository cannot be None")
self.repo = repo
3. استخدام العلامات النوعية
تُساعد العلامات النوعية في جعل الاعتماديات واضحة وتُساعد في دعم IDE والفحص النوعي الثابت:
from typing import Protocol, Optional
class UserService:
def __init__(
self,
repo: UserRepository,
logger: Logger,
email_service: EmailService,
) -> None:
self.repo = repo
self.logger = logger
self.email_service = email_service
4. تجنب الحقول المُستخدمة الزائدة
لا تحقِّل الاعتماديات التي تُعتبر تفاصيل تنفيذ داخلية. إذا أنشأ المكون وحدات مساعدة الخاصة به، فهذا مناسب:
# جيد: لا يحتاج المُساعِد الداخلي إلى الحقول المُستخدمة
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# ذاكرة ت高速 - لا تحتاج إلى الحقول المُستخدمة
self._cache: dict[int, User] = {}
5. وثّق الاعتماديات
استخدم الوثائق لشرح سبب الحاجة إلى الاعتماديات وجميع القيود:
class UserService:
"""UserService يتعامل مع منطق العمل المرتبط بالمستخدمين.
Args:
repo: UserRepository للاستعلام عن البيانات. يجب أن يكون آمنًا من التحديد
إذا تم استخدامه في السياقات المتزامنة.
logger: Logger لتعقب الأخطاء وتصحيحها.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
الحقول المُستخدمة مع FastAPI
تُوفر FastAPI دعمًا داخليًا للحقول المُستخدمة عبر آلية Depends:
from fastapi import FastAPI, Depends
from typing import Annotated
app = FastAPI()
def get_user_repository() -> UserRepository:
db = get_database()
return UserRepository(db)
def get_user_service(
repo: Annotated[UserRepository, Depends(get_user_repository)]
) -> UserService:
return UserService(repo)
@app.get("/users/{user_id}")
def get_user(
user_id: int,
service: Annotated[UserService, Depends(get_user_service)]
):
user = service.get_user(user_id)
if not user:
raise HTTPException(status_code=404)
return user
نظام الحقول المُستخدمة في FastAPI يتعامل مع رسم بياني الاعتماديات تلقائيًا، مما يجعل من السهل بناء واجهات API نظيفة ومُحافظة.
متى لا تستخدم الحقول المُستخدمة
الحقول المُستخدمة هي أداة قوية، ولكنها ليست ضرورية دائمًا:
تخطي DI عند:
- القيم البسيطة أو الفئات الخاصة بالبيانات
- الوظائف المساعدة الداخلية أو الأدوات
- النصوص أو الأدوات البسيطة
- عندما يكون التهيئة المباشرة أكثر وضوحًا وبساطة
مثال على متى لا تستخدم DI:
# فئة بيانات بسيطة - لا حاجة للحقول المُستخدمة
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# أداة بسيطة - لا حاجة للحقول المُستخدمة
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
التكامل مع بيئة بايثون
تعمل الحقول المُستخدمة بسلاسة مع أنماط بايثون الأخرى والأدوات. عند بناء تطبيقات تستخدم حزم بايثون أو إطارات اختبار الوحدة، يمكنك حقِّل هذه الخدمات في منطق العمل الخاص بك:
class ReportService:
def __init__(
self,
pdf_generator: PDFGenerator,
repo: ReportRepository,
logger: Logger,
):
self.pdf_generator = pdf_generator
self.repo = repo
self.logger = logger
def generate_report(self, report_id: int) -> bytes:
report_data = self.repo.get_by_id(report_id)
pdf = self.pdf_generator.generate(report_data)
self.logger.info(f"تم إنشاء التقرير {report_id}")
return pdf
هذا يسمح لك بتغيير تنفيذات أو استخدام نماذج اختبار أثناء الاختبار.
الحقول المُستخدمة المُزامنة
تعمل تركيبات async/await في بايثون بشكل جيد مع الحقول المُستخدمة:
from typing import Protocol
import asyncio
class AsyncUserRepository(Protocol):
async def find_by_id(self, user_id: int) -> Optional['User']:
...
async def save(self, user: 'User') -> 'User':
...
class AsyncUserService:
def __init__(self, repo: AsyncUserRepository):
self.repo = repo
async def get_user(self, user_id: int) -> Optional['User']:
return await self.repo.find_by_id(user_id)
async def get_users_batch(self, user_ids: list[int]) -> list['User']:
tasks = [self.repo.find_by_id(uid) for uid in user_ids]
results = await asyncio.gather(*tasks)
return [u for u in results if u is not None]
الخاتمة
الحقول المُستخدمة هي ركيزة أساسية لكتابة كود بايثون قابل للصيانة والاختبار. من خلال اتباع الأنماط المذكورة في هذه المقالة—حقول المُنشئ، التصميم القائم على البروتوكولات، ونمط الجذر التكويني—ستبني تطبيقات أسهل لفهمها، اختبارها، وتعديلها.
ابدأ بحقول المُنشئ اليدوية لتطبيقات صغيرة إلى متوسطة الحجم، وفكر في الإطارات مثل dependency-injector أو injector عند نمو رسم بياني الاعتماديات. تذكّر أن الهدف هو الوضوح والاختبار، وليس التعقيد من أجله نفسه.
للحصول على موارد إضافية لتطوير بايثون، تحقق من دليل بايثون القصير للرجوع السريع إلى تركيبات بايثون والأنماط الشائعة.
روابط مفيدة
- دليل بايثون القصير
- اختبارات الوحدة في بايثون
- أنماط تصميم بايثون للبنية النظيفة
- الإخراج المُهيكل - LLMs على Ollama مع Qwen3 - بايثون وGo
- مقارنة الإخراج المُهيكل عبر مزودي LLM شائعة - OpenAI، Gemini، Anthropic، Mistral وAWS Bedrock
- بناء وظيفة AWS Lambda مزدوجة مع بايثون وTerraform
موارد خارجية
- الحقول المُستخدمة في بايثون - Real Python
- كيف تحسّن الحقول المُستخدمة في بايثون بنية الكود - Volito Digital
- دورة تدريبية حول الحقول المُستخدمة في بايثون - DataCamp
- Dependency Injector - الوثائق الرسمية
- Injector - الإطارة الخفيفة للحقول المُستخدمة
- الاعتماديات في FastAPI - الوثائق الرسمية
- مبادئ SOLID في بايثون - لغة أنماط البرمجيات
- البروتوكولات والتصنيف الهيكلي في بايثون - PEP 544