ملزومات معماری پایتون: ساخت اپلیکیشن مقیاس پذیر و تمیز برای نوجوانان

Summarize this content to 400 words in Persian Lang
ساختار یک پروژه با اصول کد SOLID، KISS، DRY و پاک، همراه با الگوهای طراحی کارآمد. به زبان ساده با مثال های واقعی.
با این راهنمای مبتدی، به اصول معماری برنامه های کاربردی مقیاس پذیر و تمیز در پایتون بپردازید. در اینجا، مفاهیم اساسی مانند برنامه نویسی شی گرا (OOP)، اصول جامد، تزریق وابستگی (DI) و تایپ را بررسی می کنیم. این مقاله طراحی شده است تا شما را از صفر به قهرمان برساند و استانداردهای پیچیده معماری را به بینشهای عملی تبدیل کند که ماژولار بودن، اتصال کم و انسجام بالا را افزایش میدهد. از طریق مثالهای عملی، یاد خواهید گرفت که چگونه معماریهای چندلایه را توسعه دهید که ماژولار بودن و استقلال را تضمین میکند و شما را برای پروژههای موفق و قابل نگهداری آماده میکند.
برخی از تئوری کد پاک
قبل از پرداختن به معماری، مایلم به چند سوال متداول پاسخ دهم:
مزایای مشخص کردن انواع در پایتون چیست؟
دلایل تقسیم یک اپلیکیشن به لایه ها چیست؟
مزایای استفاده از OOP چیست؟
اشکالات استفاده از متغیرهای سراسری یا تک تن چیست؟
اگر از قبل پاسخها را میدانید، میتوانید از بخشهای تئوری رد شوید و مستقیماً به بخش «ساخت برنامه» بروید.
همیشه انواع را حاشیه نویسی کنید
حاشیه نویسی همه انواع در پایتون با افزایش وضوح، استحکام و قابلیت نگهداری کد شما را به طور قابل توجهی افزایش می دهد:
نوع Safety: حاشیه نویسی تایپ کمک می کند تا ناهماهنگی های نوع را زودتر تشخیص دهید، اشکالات را کاهش می دهد و اطمینان می دهد که کد شما مطابق انتظار عمل می کند.
کد خود مستندسازی: نکات تایپ خوانایی کد شما را افزایش می دهد و به عنوان مستندات داخلی عمل می کند و انواع داده های ورودی و خروجی مورد انتظار توابع را روشن می کند.
بهبود کیفیت کد: استفاده از نکات نوع طراحی و معماری بهتر را تشویق می کند، برنامه ریزی و اجرای متفکرانه ساختارهای داده و رابط ها را ترویج می کند.
پشتیبانی ابزار پیشرفته: ابزارهایی مانند خواب آلود استفاده از حاشیه نویسی نوع برای بررسی نوع استاتیک، شناسایی خطاهای احتمالی قبل از زمان اجرا، در نتیجه روند توسعه را بهبود می بخشد.
پشتیبانی از کتابخانه های مدرن: کتابخانه هایی مانند FastAPI و پیدانتیک از حاشیه نویسی های نوع اهرمی برای ارائه عملکردهایی مانند اعتبار سنجی خودکار داده ها، تولید طرحواره و مستندات جامع API استفاده کنید.
مزایای کلاس های داده تایپ شده نسبت به ساختارهای داده ساده: کلاس های داده تایپ شده خوانایی، مدیریت ساختار داده ها و ایمنی نوع را در مقایسه با دیکت ها و تاپل ها بهبود می بخشد. آنها به جای کلیدهای رشته ای از ویژگی ها استفاده می کنند که خطاهای ناشی از اشتباهات تایپی را به حداقل می رساند و تکمیل خودکار کد را بهبود می بخشد. کلاس های داده همچنین تعاریف واضحی از ساختارهای داده ارائه می دهند، مقادیر پیش فرض را پشتیبانی می کنند و نگهداری کد و اشکال زدایی را ساده می کنند.
چرا باید برنامه را به لایه ها تقسیم کنیم؟
تقسیم یک برنامه به لایه ها، قابلیت نگهداری، مقیاس پذیری و انعطاف پذیری را افزایش می دهد. دلایل اصلی این استراتژی عبارتند از:
تفکیک نگرانی ها
هر لایه روی یک جنبه خاص تمرکز دارد که توسعه، اشکال زدایی و نگهداری را ساده می کند. #### قابلیت استفاده مجدد
لایه ها را می توان در بخش های مختلف برنامه یا در پروژه های دیگر مورد استفاده مجدد قرار داد. تکرار کد حذف شده است. #### مقیاس پذیری
به لایه های مختلف اجازه می دهد تا بسته به نیاز، مستقل از یکدیگر مقیاس شوند. #### قابلیت سرویس دهی
تعمیر و نگهداری را با بومی سازی توابع مشترک در لایه های جداگانه ساده می کند. #### بهبود همکاری
تیم ها می توانند به طور مستقل روی لایه های مختلف کار کنند. #### انعطاف پذیری و سازگاری
تغییرات در تکنولوژی یا طراحی را می توان در لایه های خاصی پیاده سازی کرد. فقط لایه های آسیب دیده باید تطبیق داده شوند، بقیه بدون تاثیر باقی می مانند. #### تست پذیری
هر لایه را می توان به طور مستقل آزمایش کرد، آزمایش واحد و اشکال زدایی را ساده می کند.
اتخاذ یک معماری لایهای مزایای قابل توجهی در سرعت توسعه، مدیریت عملیاتی و نگهداری طولانیمدت دارد و سیستمها را قویتر، مدیریتپذیرتر و سازگارتر با تغییرات میکند.
ثابت های جهانی در مقابل پارامترهای تزریقی
هنگام توسعه نرم افزار، انتخاب بین استفاده از ثابت های جهانی و استفاده از تزریق وابستگی (DI) می تواند به طور قابل توجهی بر انعطاف پذیری، قابلیت نگهداری و مقیاس پذیری برنامه ها تأثیر بگذارد. این تجزیه و تحلیل به اشکالات ثابت های جهانی می پردازد و آنها را با مزایای ارائه شده توسط تزریق وابستگی مقایسه می کند.
ثابت های جهانی
پیکربندی ثابت: ثابت های جهانی ایستا هستند و نمی توانند به صورت پویا با محیط ها یا نیازمندی های مختلف بدون تغییر پایگاه کد سازگار شوند. این سختی، کاربرد آنها را در سناریوهای عملیاتی مختلف محدود می کند.
محدوده تست محدود: آزمایش با ثابت های جهانی چالش برانگیز می شود زیرا آنها به راحتی نادیده گرفته نمی شوند. توسعه دهندگان ممکن است نیاز به تغییر وضعیت جهانی یا به کارگیری راه حل های پیچیده برای تطبیق سناریوهای آزمایشی مختلف داشته باشند و در نتیجه خطر خطاها را افزایش دهند.
ماژولاریت کاهش یافته: تکیه بر ثابتهای جهانی، ماژولار بودن را کاهش میدهد زیرا مؤلفهها به مقادیر خاصی که در سطح جهانی تنظیم میشوند وابسته میشوند. این وابستگی قابلیت استفاده مجدد مولفه ها را در پروژه ها یا زمینه های مختلف کاهش می دهد.
اتصال کامل: ثابت های جهانی رفتارها و پیکربندی های خاص را مستقیماً در پایگاه کد ادغام می کنند و تطبیق یا تکامل برنامه بدون تغییرات گسترده را دشوار می کند.
وابستگی های پنهان: مانند متغیرهای سراسری، ثابتهای جهانی وابستگیهای درون یک برنامه کاربردی را پنهان میکنند. مشخص نیست که کدام بخش از سیستم به این ثابتها متکی است و درک و نگهداری کد را پیچیده میکند.
چالش های آلودگی فضای نام و مقیاس پذیری: ثابت های جهانی می توانند فضای نام را به هم ریخته و مقیاس بندی برنامه ها را پیچیده کنند. آنها طرحی را ترویج می کنند که مدولار نیست و مانع توزیع کارآمد در فرآیندهای مختلف می شود.
مشکلات تعمیر و نگهداری و بازسازی: با گذشت زمان، استفاده از ثابت های جهانی می تواند منجر به چالش های تعمیر و نگهداری شود. ایجاد مجدد چنین پایگاه کدی خطرناک است زیرا تغییرات در ثابت ها ممکن است به طور ناخواسته بر بخش های متفاوت برنامه تأثیر بگذارد.
تکثیر حالت در سطح ماژول: در پایتون، اگر وارد کردن در مسیرهای مختلف (مثلاً مطلق در مقابل نسبی) انجام شود، ممکن است کدهای سطح ماژول چندین بار اجرا شوند. این میتواند منجر به تکراری شدن نمونههای جهانی و ردیابی باگهای نگهداری سخت شود و پایداری و پیشبینیپذیری برنامه را پیچیدهتر کند.
پارامترهای تزریق شده
انعطاف پذیری و پیکربندی پویا: تزریق وابستگی امکان پیکربندی پویا اجزا را فراهم میکند و برنامهها را بدون نیاز به تغییر کد با شرایط مختلف سازگار میکند.
قابلیت تست پیشرفته: DI با فعال کردن تزریق اشیاء ساختگی یا پیکربندیهای جایگزین در طول آزمایش، قابلیت آزمایش را بهبود میبخشد، به طور موثر اجزا را از وابستگیهای خارجی جدا میکند و نتایج تست قابل اطمینانتری را تضمین میکند.
ماژولاریت و قابلیت استفاده مجدد افزایش یافته است: کامپوننتها مدولارتر و قابل استفادهتر میشوند زیرا برای عملکرد با هر پارامتر تزریقی که با رابطهای مورد انتظار مطابقت دارد طراحی شدهاند. این تفکیک نگرانی ها قابلیت حمل اجزا را در بخش های مختلف یک برنامه کاربردی یا حتی پروژه های مختلف افزایش می دهد.
اتصال سست: پارامترهای تزریق شده با جدا کردن منطق سیستم از پیکربندی آن، اتصال شل را تقویت می کنند. این رویکرد به روز رسانی و تغییرات آسان تر برنامه را تسهیل می کند.
اعلامیه وابستگی صریح: با DI، کامپوننت ها به وضوح وابستگی های خود را معمولاً از طریق پارامترهای سازنده یا تنظیم کننده ها اعلام می کنند. این وضوح درک، نگهداری و گسترش سیستم را آسانتر میکند.
مقیاس پذیری و مدیریت پیچیدگی: با رشد برنامهها، DI با بومیسازی نگرانیها و جدا کردن پیکربندی از کاربرد، به مدیریت پیچیدگی کمک میکند و به مقیاسبندی و نگهداری مؤثر سیستمهای بزرگ کمک میکند.
رویه ای در مقابل OOP
استفاده از برنامه نویسی شی گرا (OOP) و تزریق وابستگی (DI) می تواند به طور قابل توجهی کیفیت کد و قابلیت نگهداری را در مقایسه با رویکرد رویه ای با متغیرها و توابع جهانی افزایش دهد. در اینجا یک مقایسه ساده وجود دارد که این مزایا را نشان می دهد:
رویکرد رویه ای: متغیرها و توابع جهانی
# Global configuration
database_config = {
‘host’: ‘localhost’,
‘port’: 3306,
‘user’: ‘user’,
‘password’: ‘pass’
}
def connect_to_database():
print(f”Connecting to database on {database_config[‘host’]}…”)
# Assume connection is made
return “database_connection”
def fetch_user(database_connection, user_id):
print(f”Fetching user {user_id} using {database_connection}”)
# Fetch user logic
return {‘id’: user_id, ‘name’: ‘John Doe’}
# Usage
db_connection = connect_to_database()
user = fetch_user(db_connection, 1)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تکرار کد: database_config باید به صورت سراسری در چندین توابع منتقل شود یا به آن دسترسی پیدا کرد.
سختی تست: تمسخر اتصال یا پیکربندی پایگاه داده شامل دستکاری وضعیت جهانی است که مستعد خطا است.
اتصال کامل: توابع به طور مستقیم به وضعیت جهانی و پیاده سازی های خاص بستگی دارد.
OOP + رویکرد
from typing import Dict, Optional
from abc import ABC, abstractmethod
class DatabaseConnection(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def fetch_user(self, user_id: int) -> Dict:
pass
class MySQLConnection(DatabaseConnection):
def __init__(self, config: Dict[str, str]):
self.config = config
def connect(self):
print(f”Connecting to MySQL database on {self.config[‘host’]}…”)
# Assume connection is made
def fetch_user(self, user_id: int) -> Dict:
print(f”Fetching user {user_id} from MySQL”)
return {‘id’: user_id, ‘name’: ‘John Doe’}
class UserService:
def __init__(self, db_connection: DatabaseConnection):
self.db_connection = db_connection
def get_user(self, user_id: int) -> Dict:
return self.db_connection.fetch_user(user_id)
# Configuration and DI
config = {
‘host’: ‘localhost’,
‘port’: 3306,
‘user’: ‘user’,
‘password’: ‘pass’
}
db = MySQLConnection(config)
db.connect()
user_service = UserService(db)
user = user_service.get_user(1)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
کاهش تکرار کد: پیکربندی پایگاه داده درون شیء اتصال کپسوله شده است. نیازی به رد کردنش نیست
از امکانات: به راحتی خاموش شوید MySQLConnection با کلاس اتصال پایگاه داده دیگر بدون تغییر UserService کد
کپسوله سازی و انتزاع: جزئیات پیاده سازی نحوه واکشی کاربران یا نحوه اتصال پایگاه داده پنهان می شود.
سهولت تمسخر و آزمایش: UserService را می توان به راحتی با تزریق یک نمونه یا خرد آزمایش کرد DatabaseConnection.
مدیریت طول عمر شی: چرخه حیات اتصالات پایگاه داده را می توان به صورت جزئی تری مدیریت کرد (مثلاً با استفاده از مدیران زمینه).
استفاده از اصول OOP: وراثت (کلاس پایه انتزاعی)، چندشکلی (پیاده سازی روش های انتزاعی) و پروتکل ها (واسط های تعریف شده توسط DatabaseConnection).
با ساختاردهی برنامه با استفاده از OOP و DI، کد ماژولارتر، آزمایش آسان تر و انعطاف پذیرتر در برابر تغییرات، مانند تعویض وابستگی ها یا تغییر تنظیمات می شود.
ساخت اپلیکیشن
می توانید تمام نمونه ها و جزئیات بیشتر را با نظرات در مخزن بیابید
شروع یک پروژه جدید
فقط یک چک لیست کوتاه:
1. مدیریت پروژه و وابستگی با شعر
شعر نه تنها به عنوان ابزاری برای ایجاد پروژه می درخشد، بلکه در مدیریت وابستگی ها و محیط های مجازی نیز سرآمد است. با تنظیم ساختار پروژه خود با استفاده از دستور زیر شروع کنید:
poetry new python-app-architecture-demo
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این دستور یک ساختار دایرکتوری به خوبی سازماندهی شده ایجاد می کند: پوشه های جداگانه برای کدهای پایتون و تست ها، با یک دایرکتوری ریشه برای متا اطلاعات مانند pyproject.toml، فایل ها را قفل کنید و تنظیمات git را قفل کنید.
2. کنترل نسخه با Git
Git را در فهرست پروژه خود راه اندازی کنید:
git init
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
a اضافه کنید .gitignore فایل های غیر ضروری را از مخزن خود حذف کنید. از پایتون استاندارد استفاده کنید .gitignore ارائه شده توسط GitHub، و هر گونه استثنای خاص مانند .DS_Store برای تنظیمات macOS یا IDE (.idea، .vscode، .zed، و غیره):
wget -O .gitignore https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
echo .DS_Store >> .gitignore
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
3. وابستگی ها را مدیریت کنید
وابستگی های پروژه خود را با استفاده از Poetry نصب کنید:
poetry add fastapi pytest aiogram
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بعداً میتوانید همه وابستگیها را با استفاده از:
poetry install
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
در صورت نیاز به دستورالعمل های خاص تری به اسناد رسمی هر کتابخانه مراجعه کنید.
4. فایل های پیکربندی
ایجاد یک config.py فایل برای متمرکز کردن تنظیمات برنامه شما – یک رویکرد رایج و موثر.
متغیرهای محیط را برای رازها و پارامترها تنظیم کنید:
touch .env example.env
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
.env حاوی داده های حساس است و باید از git نادیده گرفته شود example.env مقادیر متغیر یا پیش فرض را نگه می دارد و در مخزن بررسی می شود.
5. نقطه ورود برنامه
نقطه ورود درخواست خود را در آن مشخص کنید main.py:
python_app_architecture/main.py:
def run():
print(‘Hello, World!’)
if __name__ == ‘__main__’: # avoid run on import
run()
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پروژه خود را به عنوان یک کتابخانه قابل استفاده کنید و با وارد کردن آن امکان دسترسی برنامه ای را فراهم کنید run عملکرد در __init__.py:
python_app_architecture/init.py
from .main import run
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
با افزودن میانبر، اجرای مستقیم پروژه را با Poetry فعال کنید __main__.py. این به شما امکان می دهد از دستور استفاده کنید poetry run python python_app_architecture به جای طولانی تر poetry run python python_app_architecture/main.py.
python_app_architecture/اصلی.py:
from .main import run
run()
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تعریف دایرکتوری ها و لایه ها
سلب مسئولیت:البته هر اپلیکیشنی متفاوت است و معماری آنها بسته به اهداف و اهداف متفاوت خواهد بود. من نمی گویم که این تنها گزینه صحیح است، اما فکر می کنم که این گزینه نسبتاً متوسط و برای بخش بزرگی از پروژه ها مناسب است. سعی کنید به جای مثال های خاص، روی رویکردها و ایده های اصلی تمرکز کنید.
حال، بیایید دایرکتوری ها را برای لایه های مختلف برنامه تنظیم کنیم.
به طور معمول، عاقلانه است که API خود را نسخه کنید (مثلاً با ایجاد زیر شاخههایی مانند api/v1) اما فعلاً همه چیز را ساده نگه می داریم و این مرحله را حذف می کنیم.
.
├── python_app_architecture_demo
│ ├── coordinator.py
│ ├── entities
│ ├── general
│ ├── mappers
│ ├── providers
│ ├── repository
│ │ └── models
│ └── services
│ ├── api_service
│ │ └── api
│ │ ├── dependencies
│ │ ├── endpoints
│ │ └── schemas
│ └── telegram_service
└── tests
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
برنامه
موجودیت ها – ساختارهای داده در سطح برنامه. صرفاً حامل داده ها بدون منطق تعبیه شده است.
عمومی – کمربند برقی! یک مرکز ادغام شده برای خدمات مشترک، کمکی، و نمای کتابخانه.
نقشه برداران – متخصصان ترجمه داده ها، تبدیل بین فرم های داده مانند مدل های پایگاه داده به موجودیت ها یا بین فرمت های مختلف داده. این تمرین خوب است که به جای جهانی کردن نقشهبرها، آنها را تا مرز استفاده آن کپسوله کنید. به عنوان مثال، نقشهبردار مدلها – نهادها ممکن است بخشی از ماژول مخزن باشد. مثال دیگر: schemas-entities mapper باید در سرویس api باقی بماند و ابزار خصوصی آن باشد.
ارائه دهندگان – ستون فقرات منطق کسب و کار اصلی. ارائهدهندگان منطق اصلی برنامه را پیادهسازی میکنند، اما نسبت به جزئیات رابط بیاعتنا میمانند و از انتزاعی بودن و مجزا بودن عملیاتشان اطمینان میدهند.
مخزن – کتابداران متولیان دسترسی به دادهها، پیچیدگیهای تعاملات منبع داده را از طریق مدلها انتزاع میکنند، بنابراین عملیات پایگاه داده را از برنامه گستردهتر جدا میکنند.
مدل ها – تعاریف ساختارهای داده محلی در تعامل با پایگاه داده، متمایز و جدا از نهادها.
خدمات – هر سرویس به عنوان یک برنامه فرعی (تقریبا) مستقل عمل می کند و دامنه خاص منطق تجاری خود را هماهنگ می کند و در عین حال وظایف اساسی را به ارائه دهندگان محول می کند. این پیکربندی منطق متمرکز و یکنواخت در سراسر برنامه را تضمین می کند
سرویس API – ارتباطات خارجی را از طریق HTTP/S مدیریت می کند که بر اساس چارچوب FastAPI ساختار یافته است.
وابستگی ها – ابزارها و کمکهای ضروری مورد نیاز بخشهای مختلف API شما، که از طریق سیستم DI FastAPI یکپارچه شدهاند.
نقاط پایانی – مسیرهایی که منطق کسب و کار شما را از طریق HTTP به طور عمومی نشان می دهند.
طرحواره ها – تعاریف ساختار داده برای ورودی ها و خروجی های API، محدود به لایه سرویس آنها برای حفظ کپسولاسیون.
سرویس تلگرام – مشابه سرویس API عمل می کند و عملکردی را از طریق تلگرام بدون تکرار منطق اصلی کسب و کار ارائه می دهد. این امر با تماس با همان ارائه دهندگانی که API Service استفاده می کند، تضمین سازگاری و کاهش افزونگی کد به دست می آید.
تست ها – این دایرکتوری که صرفاً به آزمایش اختصاص دارد، حاوی تمام کدهای آزمایشی است که جدایی واضح از منطق برنامه را حفظ می کند.
ارتباط بین لایه ها چیزی شبیه به این خواهد بود:
توجه داشته باشید که موجودیت ها اجزای فعال نیستند، بلکه فقط ساختارهای داده ای هستند که بین لایه ها منتقل می شوند:
به خاطر داشته باشید که لایه ها مستقیماً به هم متصل نیستند، بلکه فقط به انتزاعات بستگی دارند. پیاده سازی ها با استفاده از تزریق وابستگی انجام می شوند:
این ساختار انعطاف پذیر به شما اجازه می دهد تا به راحتی قابلیت اضافه کنید، به عنوان مثال، پایگاه داده را تغییر دهید، یک سرویس ایجاد کنید، یا یک رابط جدید را بدون تغییرات اضافی یا تکرار کد متصل کنید، زیرا منطق هر ماژول در لایه خودش قرار دارد:
در همان زمان، تمام منطق یک سرویس جداگانه در آن محصور شده است:
کاوش در کد
نقطه پایانی
بیایید از نقطه پایانی شروع کنیم:
# api_service/api/endpoints/user.py
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from entities.user import UserCreate
from ..dependencies.providers import (
user_provider, # 1
UserProvider # 2
)
router = APIRouter()
@router.post(“/register”)
async def register(
user: UserCreate, # 3
provider: Annotated[UserProvider, Depends(user_provider)] # 4
):
provider.create_user(user) # 5
return {“message”: “User created!”}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
وارد کردن تابع کمکی تزریق وابستگی (ما در یک دقیقه به آن نگاه خواهیم کرد)
وارد کردن User Provider پروتکل برای حاشیه نویسی نوع
نقطه پایانی نیاز به داشتن بدنه درخواست دارد UserCreate طرح با فرمت json
را provider پارامتر در register تابع نمونه ای از پیاده سازی است UserProviderتزریق شده توسط FastAPI با استفاده از Depends سازوکار.
را create_user روش از UserProvider با داده های کاربر تجزیه شده فراخوانی می شود. این یک جدایی واضح از نگرانیها را نشان میدهد، جایی که لایه API منطق تجاری را به لایه ارائهدهنده تفویض میکند، و به این اصل پایبند است که لایههای رابط نباید دارای منطق تجاری باشند.
User Provider
حالا بیایید منطق کسب و کار را ببینیم:
# providers/user_provider.py
from typing import Protocol, runtime_checkable, Callable
from typing_extensions import runtime_checkable
from repository import UserRepository
from providers.mail_provider import MailProvider
from entities.user import UserCreate
@runtime_checkable
class UserProvider(Protocol): # 1
def create_user(self, user: UserCreate): …
@runtime_checkable
class UserProviderOutput(Protocol): # 2
def user_provider_created_user(self, provider: UserProvider, user: UserCreate): …
class UserProviderImpl: # 3
def __init__(self,
repository: UserRepository, # 4
mail_provider: MailProvider, # 4
output: UserProviderOutput | None, # 5
on_user_created: Callable[[UserCreate], None] | None # 6
):
self.repository = repository
self.mail_provider = mail_provider
self.output = output
self.on_user_created = on_user_created
# Implementation
def create_user(self, user: UserCreate): # 7
self.repository.add_user(user) # 8
self.mail_provider.send_mail(user.email, f”Welcome, {user.name}!”) # 9
if output := self.output: # unwraping the optional
output.user_provider_created_user(self, user) # 10
# 11
if on_user_created := self.on_user_created:
on_user_created(user)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تعریف رابط: UserProvider پروتکلی است که روش را مشخص می کند create_user، که هر کلاسی که به این پروتکل پایبند است باید آن را پیاده سازی کند. این به عنوان یک قرارداد رسمی برای عملکرد ایجاد کاربر عمل می کند.
پروتکل ناظر: UserProviderOutput به عنوان یک ناظر (یا نماینده) عمل می کند که هنگام ایجاد کاربر مطلع می شود. این پروتکل اتصال آزاد را فعال می کند و معماری رویداد محور برنامه را بهبود می بخشد.
اجرای پروتکل: UserProviderImpl منطق ایجاد کاربر را پیاده سازی می کند اما نیازی به اعلام صریح پایبندی خود به آن ندارد UserProvider به دلیل ماهیت پویای پایتون و استفاده از تایپ اردک.
وابستگی های اصلی: سازنده می پذیرد UserRepository و MailProviderهر دو به عنوان پروتکل تعریف می شوند – به عنوان پارامتر. تنها با تکیه بر این پروتکل ها، UserProviderImpl از پیادهسازیهای خاص جدا باقی میماند و اصول تزریق وابستگی را نشان میدهد که در آن ارائهدهنده جزئیات زیربنایی را نادیده میگیرد و فقط از طریق قراردادهای تعریفشده با هم ارتباط برقرار میکند.
نماینده خروجی اختیاری: سازنده یک گزینه اختیاری را می پذیرد UserProviderOutput به عنوان مثال، که در صورت ارائه، پس از تکمیل ایجاد کاربر اطلاع رسانی خواهد شد.
عملکرد برگشت به تماس: به عنوان جایگزینی برای نمایندگی خروجی، قابل فراخوانی است on_user_created را می توان برای رسیدگی به اقدامات اضافی پس از ایجاد کاربر، ارائه انعطاف پذیری در پاسخ به رویدادها ارسال کرد.
منطق تجاری مرکزی: create_user متد منطق اصلی کسب و کار برای افزودن کاربر را در بر می گیرد و جدایی از مدیریت API را نشان می دهد.
تعامل مخزن: از UserRepository برای انتزاع عملیات پایگاه داده (به عنوان مثال، افزودن یک کاربر)، اطمینان از اینکه ارائه دهنده به طور مستقیم پایگاه داده را دستکاری نمی کند.
منطق تجاری گسترده: شامل ارسال ایمیل از طریق MailProvider، نشان می دهد که مسئولیت های ارائه دهنده می تواند فراتر از عملیات ساده CRUD باشد.
اطلاع رسانی رویداد: اگر یک نماینده خروجی ارائه شود، این نماینده را در مورد رویداد ایجاد کاربر مطلع میکند و از الگوی مشاهدهگر برای افزایش تعامل و واکنشهای مدولار به رویدادها استفاده میکند.
اجرای برگشت به تماس: به صورت اختیاری یک تابع callback را اجرا می کند و روشی ساده برای گسترش عملکرد بدون سلسله مراتب کلاسی پیچیده یا وابستگی ارائه می دهد.
وابستگی های FastAPI
خوب، اما چگونه می توان ارائه دهنده را نمونه برداری کرد و آن را تزریق کرد؟ بیایید نگاهی به کد تزریق بیندازیم که توسط موتور DI FastAPI طراحی شده است:
# services/api_service/api/dependencies/providers.py
from typing import Annotated
from fastapi import Request, Depends
from repository import UserRepository
from providers.user_provider import UserProvider, UserProviderImpl
from providers.mail_provider import MailProvider
from coordinator import Coordinator
from .database import get_session, Session
import config
def _get_coordinator(request: Request) -> Coordinator:
# private helper function
# NOTE: You can pass the DIContainer in the same way
return request.app.state.coordinator
def user_provider(
session: Annotated[Session, Depends(get_session)], # 1
coordinator: Annotated[Coordinator, Depends(_get_coordinator)] # 2
) -> UserProvider: # 3
# UserProvider’s lifecycle is bound to short endpoint’s lifecycle, so it’s safe to use strong references here
return UserProviderImpl( # 4
repository=UserRepository(session), # 5
mail_provider=MailProvider(config.mail_token), # 6
output=coordinator, # 7
on_user_created=coordinator.on_user_created # 8
# on_user_created: lambda: coordinator.on_user_created() # add a lambda if the method’s signature is not compatible
)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دریافت یک جلسه پایگاه داده از طریق سیستم تزریق وابستگی FastAPI، با اطمینان از اینکه هر درخواست یک جلسه تمیز دارد.
به دست آوردن یک نمونه از Coordinator از حالت برنامه، که مسئول مدیریت وظایف گسترده تر در سطح برنامه و عمل به عنوان مدیر رویداد است.
توجه: تابع پروتکل را برمی گرداند اما پیاده سازی دقیق را ندارد
ساختن یک نمونه از UserProviderImpl با تزریق تمام وابستگی های لازم. این نشاندهنده کاربرد عملی تزریق وابستگی برای مونتاژ اشیاء پیچیده است.
مقدار دهی اولیه UserRepository با جلسه به دست آمده از سیستم DI FastAPI. این مخزن تمام عملیات پایداری داده را مدیریت می کند و تعاملات پایگاه داده را از ارائه دهنده انتزاع می کند.
راه اندازی MailProvider با استفاده از یک نشانه پیکربندی
تزریق کردن Coordinator به عنوان پروتکل خروجی این فرض می کند که Coordinator را اجرا می کند UserProviderOutput پروتکل، به آن اجازه می دهد تا هنگام ایجاد یک کاربر، اعلان ها را دریافت کند.
روشی را از Coordinator به عنوان یک تماس برگشتی که پس از ایجاد کاربر اجرا می شود. این اجازه می دهد تا عملیات یا اعلان های اضافی به عنوان یک اثر جانبی فرآیند ایجاد کاربر فعال شود.
این رویکرد ساختاریافته تضمین می کند که UserProvider مجهز به کلیه ابزارهای لازم برای انجام وظایف خود با رعایت اصول اتصال شل و انسجام بالا.
هماهنگ کننده
کلاس Coordinator به عنوان ارکستر اصلی در برنامه شما عمل می کند، خدمات مختلف، تعاملات، رویدادها، تنظیم حالت اولیه و تزریق وابستگی ها را مدیریت می کند. در اینجا به تفصیل نقش ها و عملکردهای آن بر اساس کد ارائه شده است:
# coordinator.py
from threading import Thread
import weakref
import uvicorn
import config
from services.api_service import get_app as get_fastapi_app
from entities.user import UserCreate
from repository.user_repository import UserRepository
from providers.mail_provider import MailProvider
from providers.user_provider import UserProvider, UserProviderImpl
from services.report_service import ReportService
from services.telegram_service import TelegramService
class Coordinator:
def __init__(self):
self.users_count = 0 # 1
self.telegram_service = TelegramService( # 2
token=config.telegram_token,
get_user_provider=lambda session: UserProviderImpl(
repository=UserRepository(session),
mail_provider=MailProvider(config.mail_token),
output=self,
on_user_created=self.on_user_created
)
)
self.report_service = ReportService(
get_users_count = lambda: self.users_count # 3
)
# Coordinator’s Interface
def setup_initial_state(self):
fastapi_app = get_fastapi_app()
fastapi_app.state.coordinator = self # 4
# 5
fastapi_thread = Thread(target=lambda: uvicorn.run(fastapi_app))
fastapi_thread.start()
# 6
self.report_service.start()
self.telegram_service.start()
# UserProviderOutput Protocol Implementation
def user_provider_created_user(self, provider: UserProvider, user: UserCreate):
self.on_user_created(user)
# Event handlers
def on_user_created(self, user):
print(“User created: “, user)
self.users_count += 1
# 7
if self.users_count >= 10_000:
self.report_service.interval_seconds *= 10
elif self.users_count >= 10_000_000:
self.report_service.stop() # 8
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
برخی از وضعیت ها را می توان بین ارائه دهندگان، خدمات، لایه ها و کل برنامه مختلف به اشتراک گذاشت
مونتاژ پیاده سازی ها و تزریق وابستگی ها
از مراجع دایره ای، بن بست ها و نشت حافظه در اینجا آگاه باشید، کد کامل جزئیات را ببینید
نمونه هماهنگ کننده را به وضعیت برنامه FastAPI منتقل کنید تا بتوانید از طریق سیستم DI FastAPI در نقاط پایانی به آن دسترسی داشته باشید.
همه خدمات را در موضوعات جداگانه شروع کنید
قبلاً در یک رشته جداگانه در داخل سرویس اجرا می شود
برای مثال، برخی از منطق خدمات متقابل اینجا
نمونه ای از خدمات کنترلی از هماهنگ کننده
این ارکستراتور کنترل و ارتباط بین اجزای مختلف را متمرکز می کند و مدیریت و مقیاس پذیری برنامه را افزایش می دهد. این به طور موثر اقدامات بین سرویس ها را هماهنگ می کند و اطمینان می دهد که برنامه به طور مناسب به تغییرات حالت و تعاملات کاربر پاسخ می دهد. این الگوی طراحی برای حفظ تفکیک تمیز نگرانیها و فعال کردن رفتار برنامههای کاربردی قویتر و انعطافپذیر بسیار مهم است.
کانتینر
با این حال، در برنامه های بزرگ مقیاس DI دستی می تواند به مقدار قابل توجهی از کد دیگ بخار منجر شود. این زمانی است که DI Container برای نجات می آید. DI Containers یا Dependency Injection Containers ابزارهای قدرتمندی هستند که در توسعه نرم افزار برای مدیریت وابستگی ها در یک برنامه استفاده می شوند. آنها به عنوان یک مکان مرکزی عمل می کنند که در آن اشیاء و وابستگی های آنها ثبت و مدیریت می شوند. هنگامی که یک شی به یک وابستگی نیاز دارد، DI Container به طور خودکار نمونه سازی و ارائه این وابستگی ها را کنترل می کند، و اطمینان حاصل می کند که اشیا تمام اجزای لازم را برای عملکرد مؤثر دریافت می کنند. این رویکرد با انتزاع کردن منطق پیچیده مدیریت وابستگی به دور از منطق تجاری برنامه، اتصال شل را ترویج میکند، آزمایشپذیری را افزایش میدهد و قابلیت نگهداری کلی پایگاه کد را بهبود میبخشد. کانتینرهای DI فرآیند توسعه را با خودکارسازی و متمرکز کردن پیکربندی وابستگی های مؤلفه ساده می کنند.
برای پایتون، کتابخانههای زیادی وجود دارد که پیادهسازیهای مختلف DI Container را ارائه میکنند، تقریباً همه آنها را نگاه کردم و بهترین IMO را نوشتم.
python-dependency-injector – خودکار، مبتنی بر کلاس، دارای گزینه های مختلف چرخه زندگی مانند Singleton یا Factory
lagom – یک رابط فرهنگ لغت با تفکیک خودکار
dishka – کنترل دامنه خوب از طریق مدیر زمینه
این بستگی دارد – پشتیبانی از مدیران زمینه (اشیاء مورد نیاز برای بسته شدن در پایان)، ادغام بومی fastapi
punq – رویکرد کلاسیک تر با register و resolve مواد و روش ها
rodi – کلاسیک، ساده، خودکار
main.py
برای پایان، اجازه دهید فایل main.py را به روز کنیم:
# main.py
from coordinator import Coordinator
def run(): # entry point, no logic here, only run the coordinator
coordinator = Coordinator()
coordinator.setup_initial_state()
if __name__ == ‘__main__’:
run()
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
نتیجه
برای به دست آوردن درک جامعی از استراتژی های معماری و پیاده سازی مورد بحث، بررسی همه فایل های موجود در مخزن مفید است. با وجود مقدار محدود کد، هر فایل با نظرات روشنگر و جزئیات اضافی غنی شده است که درک عمیق تری از ساختار و عملکرد برنامه ارائه می دهد. بررسی این جنبهها، آشنایی شما را با سیستم افزایش میدهد و اطمینان میدهد که برای تطبیق یا گسترش مؤثر برنامه به خوبی مجهز هستید.
این رویکرد به طور جهانی برای برنامه های مختلف پایتون مفید است. این برای سرورهای پشتیبان بدون حالت مانند سرورهای ساخته شده با FastAPI موثر است، اما مزایای آن به ویژه در برنامهها و برنامههای بدون چارچوب که حالت را مدیریت میکنند، آشکار است. این شامل برنامههای دسکتاپ (هم رابط کاربری گرافیکی و هم خط فرمان)، و همچنین سیستمهایی است که دستگاههای فیزیکی مانند دستگاههای اینترنت اشیا، روباتیک، هواپیماهای بدون سرنشین و سایر فناوریهای سختافزار محور را کنترل میکنند.
علاوه بر این، خواندن را به شدت توصیه می کنم کد پاک توسط رابرت مارتین برای غنی سازی بیشتر. در اینجا می توانید خلاصه و نکات کلیدی را بیابید. این منبع اصول و شیوه های اساسی را در اختیار شما قرار می دهد که برای حفظ استانداردهای بالا در توسعه نرم افزار بسیار مهم هستند.
ساختار یک پروژه با اصول کد SOLID، KISS، DRY و پاک، همراه با الگوهای طراحی کارآمد. به زبان ساده با مثال های واقعی.
با این راهنمای مبتدی، به اصول معماری برنامه های کاربردی مقیاس پذیر و تمیز در پایتون بپردازید. در اینجا، مفاهیم اساسی مانند برنامه نویسی شی گرا (OOP)، اصول جامد، تزریق وابستگی (DI) و تایپ را بررسی می کنیم. این مقاله طراحی شده است تا شما را از صفر به قهرمان برساند و استانداردهای پیچیده معماری را به بینشهای عملی تبدیل کند که ماژولار بودن، اتصال کم و انسجام بالا را افزایش میدهد. از طریق مثالهای عملی، یاد خواهید گرفت که چگونه معماریهای چندلایه را توسعه دهید که ماژولار بودن و استقلال را تضمین میکند و شما را برای پروژههای موفق و قابل نگهداری آماده میکند.
برخی از تئوری کد پاک
قبل از پرداختن به معماری، مایلم به چند سوال متداول پاسخ دهم:
- مزایای مشخص کردن انواع در پایتون چیست؟
- دلایل تقسیم یک اپلیکیشن به لایه ها چیست؟
- مزایای استفاده از OOP چیست؟
- اشکالات استفاده از متغیرهای سراسری یا تک تن چیست؟
اگر از قبل پاسخها را میدانید، میتوانید از بخشهای تئوری رد شوید و مستقیماً به بخش «ساخت برنامه» بروید.
همیشه انواع را حاشیه نویسی کنید
حاشیه نویسی همه انواع در پایتون با افزایش وضوح، استحکام و قابلیت نگهداری کد شما را به طور قابل توجهی افزایش می دهد:
-
نوع Safety: حاشیه نویسی تایپ کمک می کند تا ناهماهنگی های نوع را زودتر تشخیص دهید، اشکالات را کاهش می دهد و اطمینان می دهد که کد شما مطابق انتظار عمل می کند.
-
کد خود مستندسازی: نکات تایپ خوانایی کد شما را افزایش می دهد و به عنوان مستندات داخلی عمل می کند و انواع داده های ورودی و خروجی مورد انتظار توابع را روشن می کند.
-
بهبود کیفیت کد: استفاده از نکات نوع طراحی و معماری بهتر را تشویق می کند، برنامه ریزی و اجرای متفکرانه ساختارهای داده و رابط ها را ترویج می کند.
-
پشتیبانی ابزار پیشرفته: ابزارهایی مانند خواب آلود استفاده از حاشیه نویسی نوع برای بررسی نوع استاتیک، شناسایی خطاهای احتمالی قبل از زمان اجرا، در نتیجه روند توسعه را بهبود می بخشد.
-
پشتیبانی از کتابخانه های مدرن: کتابخانه هایی مانند FastAPI و پیدانتیک از حاشیه نویسی های نوع اهرمی برای ارائه عملکردهایی مانند اعتبار سنجی خودکار داده ها، تولید طرحواره و مستندات جامع API استفاده کنید.
-
مزایای کلاس های داده تایپ شده نسبت به ساختارهای داده ساده: کلاس های داده تایپ شده خوانایی، مدیریت ساختار داده ها و ایمنی نوع را در مقایسه با دیکت ها و تاپل ها بهبود می بخشد. آنها به جای کلیدهای رشته ای از ویژگی ها استفاده می کنند که خطاهای ناشی از اشتباهات تایپی را به حداقل می رساند و تکمیل خودکار کد را بهبود می بخشد. کلاس های داده همچنین تعاریف واضحی از ساختارهای داده ارائه می دهند، مقادیر پیش فرض را پشتیبانی می کنند و نگهداری کد و اشکال زدایی را ساده می کنند.
چرا باید برنامه را به لایه ها تقسیم کنیم؟
تقسیم یک برنامه به لایه ها، قابلیت نگهداری، مقیاس پذیری و انعطاف پذیری را افزایش می دهد. دلایل اصلی این استراتژی عبارتند از:
تفکیک نگرانی ها
- هر لایه روی یک جنبه خاص تمرکز دارد که توسعه، اشکال زدایی و نگهداری را ساده می کند. #### قابلیت استفاده مجدد
- لایه ها را می توان در بخش های مختلف برنامه یا در پروژه های دیگر مورد استفاده مجدد قرار داد. تکرار کد حذف شده است. #### مقیاس پذیری
- به لایه های مختلف اجازه می دهد تا بسته به نیاز، مستقل از یکدیگر مقیاس شوند. #### قابلیت سرویس دهی
- تعمیر و نگهداری را با بومی سازی توابع مشترک در لایه های جداگانه ساده می کند. #### بهبود همکاری
- تیم ها می توانند به طور مستقل روی لایه های مختلف کار کنند. #### انعطاف پذیری و سازگاری
- تغییرات در تکنولوژی یا طراحی را می توان در لایه های خاصی پیاده سازی کرد. فقط لایه های آسیب دیده باید تطبیق داده شوند، بقیه بدون تاثیر باقی می مانند. #### تست پذیری
- هر لایه را می توان به طور مستقل آزمایش کرد، آزمایش واحد و اشکال زدایی را ساده می کند.
اتخاذ یک معماری لایهای مزایای قابل توجهی در سرعت توسعه، مدیریت عملیاتی و نگهداری طولانیمدت دارد و سیستمها را قویتر، مدیریتپذیرتر و سازگارتر با تغییرات میکند.
ثابت های جهانی در مقابل پارامترهای تزریقی
هنگام توسعه نرم افزار، انتخاب بین استفاده از ثابت های جهانی و استفاده از تزریق وابستگی (DI) می تواند به طور قابل توجهی بر انعطاف پذیری، قابلیت نگهداری و مقیاس پذیری برنامه ها تأثیر بگذارد. این تجزیه و تحلیل به اشکالات ثابت های جهانی می پردازد و آنها را با مزایای ارائه شده توسط تزریق وابستگی مقایسه می کند.
ثابت های جهانی
-
پیکربندی ثابت: ثابت های جهانی ایستا هستند و نمی توانند به صورت پویا با محیط ها یا نیازمندی های مختلف بدون تغییر پایگاه کد سازگار شوند. این سختی، کاربرد آنها را در سناریوهای عملیاتی مختلف محدود می کند.
-
محدوده تست محدود: آزمایش با ثابت های جهانی چالش برانگیز می شود زیرا آنها به راحتی نادیده گرفته نمی شوند. توسعه دهندگان ممکن است نیاز به تغییر وضعیت جهانی یا به کارگیری راه حل های پیچیده برای تطبیق سناریوهای آزمایشی مختلف داشته باشند و در نتیجه خطر خطاها را افزایش دهند.
-
ماژولاریت کاهش یافته: تکیه بر ثابتهای جهانی، ماژولار بودن را کاهش میدهد زیرا مؤلفهها به مقادیر خاصی که در سطح جهانی تنظیم میشوند وابسته میشوند. این وابستگی قابلیت استفاده مجدد مولفه ها را در پروژه ها یا زمینه های مختلف کاهش می دهد.
-
اتصال کامل: ثابت های جهانی رفتارها و پیکربندی های خاص را مستقیماً در پایگاه کد ادغام می کنند و تطبیق یا تکامل برنامه بدون تغییرات گسترده را دشوار می کند.
-
وابستگی های پنهان: مانند متغیرهای سراسری، ثابتهای جهانی وابستگیهای درون یک برنامه کاربردی را پنهان میکنند. مشخص نیست که کدام بخش از سیستم به این ثابتها متکی است و درک و نگهداری کد را پیچیده میکند.
-
چالش های آلودگی فضای نام و مقیاس پذیری: ثابت های جهانی می توانند فضای نام را به هم ریخته و مقیاس بندی برنامه ها را پیچیده کنند. آنها طرحی را ترویج می کنند که مدولار نیست و مانع توزیع کارآمد در فرآیندهای مختلف می شود.
-
مشکلات تعمیر و نگهداری و بازسازی: با گذشت زمان، استفاده از ثابت های جهانی می تواند منجر به چالش های تعمیر و نگهداری شود. ایجاد مجدد چنین پایگاه کدی خطرناک است زیرا تغییرات در ثابت ها ممکن است به طور ناخواسته بر بخش های متفاوت برنامه تأثیر بگذارد.
-
تکثیر حالت در سطح ماژول: در پایتون، اگر وارد کردن در مسیرهای مختلف (مثلاً مطلق در مقابل نسبی) انجام شود، ممکن است کدهای سطح ماژول چندین بار اجرا شوند. این میتواند منجر به تکراری شدن نمونههای جهانی و ردیابی باگهای نگهداری سخت شود و پایداری و پیشبینیپذیری برنامه را پیچیدهتر کند.
پارامترهای تزریق شده
-
انعطاف پذیری و پیکربندی پویا: تزریق وابستگی امکان پیکربندی پویا اجزا را فراهم میکند و برنامهها را بدون نیاز به تغییر کد با شرایط مختلف سازگار میکند.
-
قابلیت تست پیشرفته: DI با فعال کردن تزریق اشیاء ساختگی یا پیکربندیهای جایگزین در طول آزمایش، قابلیت آزمایش را بهبود میبخشد، به طور موثر اجزا را از وابستگیهای خارجی جدا میکند و نتایج تست قابل اطمینانتری را تضمین میکند.
-
ماژولاریت و قابلیت استفاده مجدد افزایش یافته است: کامپوننتها مدولارتر و قابل استفادهتر میشوند زیرا برای عملکرد با هر پارامتر تزریقی که با رابطهای مورد انتظار مطابقت دارد طراحی شدهاند. این تفکیک نگرانی ها قابلیت حمل اجزا را در بخش های مختلف یک برنامه کاربردی یا حتی پروژه های مختلف افزایش می دهد.
-
اتصال سست: پارامترهای تزریق شده با جدا کردن منطق سیستم از پیکربندی آن، اتصال شل را تقویت می کنند. این رویکرد به روز رسانی و تغییرات آسان تر برنامه را تسهیل می کند.
-
اعلامیه وابستگی صریح: با DI، کامپوننت ها به وضوح وابستگی های خود را معمولاً از طریق پارامترهای سازنده یا تنظیم کننده ها اعلام می کنند. این وضوح درک، نگهداری و گسترش سیستم را آسانتر میکند.
-
مقیاس پذیری و مدیریت پیچیدگی: با رشد برنامهها، DI با بومیسازی نگرانیها و جدا کردن پیکربندی از کاربرد، به مدیریت پیچیدگی کمک میکند و به مقیاسبندی و نگهداری مؤثر سیستمهای بزرگ کمک میکند.
رویه ای در مقابل OOP
استفاده از برنامه نویسی شی گرا (OOP) و تزریق وابستگی (DI) می تواند به طور قابل توجهی کیفیت کد و قابلیت نگهداری را در مقایسه با رویکرد رویه ای با متغیرها و توابع جهانی افزایش دهد. در اینجا یک مقایسه ساده وجود دارد که این مزایا را نشان می دهد:
رویکرد رویه ای: متغیرها و توابع جهانی
# Global configuration
database_config = {
'host': 'localhost',
'port': 3306,
'user': 'user',
'password': 'pass'
}
def connect_to_database():
print(f"Connecting to database on {database_config['host']}...")
# Assume connection is made
return "database_connection"
def fetch_user(database_connection, user_id):
print(f"Fetching user {user_id} using {database_connection}")
# Fetch user logic
return {'id': user_id, 'name': 'John Doe'}
# Usage
db_connection = connect_to_database()
user = fetch_user(db_connection, 1)
-
تکرار کد:
database_config
باید به صورت سراسری در چندین توابع منتقل شود یا به آن دسترسی پیدا کرد. - سختی تست: تمسخر اتصال یا پیکربندی پایگاه داده شامل دستکاری وضعیت جهانی است که مستعد خطا است.
- اتصال کامل: توابع به طور مستقیم به وضعیت جهانی و پیاده سازی های خاص بستگی دارد.
OOP + رویکرد
from typing import Dict, Optional
from abc import ABC, abstractmethod
class DatabaseConnection(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def fetch_user(self, user_id: int) -> Dict:
pass
class MySQLConnection(DatabaseConnection):
def __init__(self, config: Dict[str, str]):
self.config = config
def connect(self):
print(f"Connecting to MySQL database on {self.config['host']}...")
# Assume connection is made
def fetch_user(self, user_id: int) -> Dict:
print(f"Fetching user {user_id} from MySQL")
return {'id': user_id, 'name': 'John Doe'}
class UserService:
def __init__(self, db_connection: DatabaseConnection):
self.db_connection = db_connection
def get_user(self, user_id: int) -> Dict:
return self.db_connection.fetch_user(user_id)
# Configuration and DI
config = {
'host': 'localhost',
'port': 3306,
'user': 'user',
'password': 'pass'
}
db = MySQLConnection(config)
db.connect()
user_service = UserService(db)
user = user_service.get_user(1)
- کاهش تکرار کد: پیکربندی پایگاه داده درون شیء اتصال کپسوله شده است. نیازی به رد کردنش نیست
-
از امکانات: به راحتی خاموش شوید
MySQLConnection
با کلاس اتصال پایگاه داده دیگر بدون تغییرUserService
کد - کپسوله سازی و انتزاع: جزئیات پیاده سازی نحوه واکشی کاربران یا نحوه اتصال پایگاه داده پنهان می شود.
-
سهولت تمسخر و آزمایش:
UserService
را می توان به راحتی با تزریق یک نمونه یا خرد آزمایش کردDatabaseConnection
. - مدیریت طول عمر شی: چرخه حیات اتصالات پایگاه داده را می توان به صورت جزئی تری مدیریت کرد (مثلاً با استفاده از مدیران زمینه).
-
استفاده از اصول OOP: وراثت (کلاس پایه انتزاعی)، چندشکلی (پیاده سازی روش های انتزاعی) و پروتکل ها (واسط های تعریف شده توسط
DatabaseConnection
).
با ساختاردهی برنامه با استفاده از OOP و DI، کد ماژولارتر، آزمایش آسان تر و انعطاف پذیرتر در برابر تغییرات، مانند تعویض وابستگی ها یا تغییر تنظیمات می شود.
ساخت اپلیکیشن
می توانید تمام نمونه ها و جزئیات بیشتر را با نظرات در مخزن بیابید
شروع یک پروژه جدید
فقط یک چک لیست کوتاه:
1. مدیریت پروژه و وابستگی با شعر
شعر نه تنها به عنوان ابزاری برای ایجاد پروژه می درخشد، بلکه در مدیریت وابستگی ها و محیط های مجازی نیز سرآمد است. با تنظیم ساختار پروژه خود با استفاده از دستور زیر شروع کنید:
poetry new python-app-architecture-demo
این دستور یک ساختار دایرکتوری به خوبی سازماندهی شده ایجاد می کند: پوشه های جداگانه برای کدهای پایتون و تست ها، با یک دایرکتوری ریشه برای متا اطلاعات مانند pyproject.toml
، فایل ها را قفل کنید و تنظیمات git را قفل کنید.
2. کنترل نسخه با Git
Git را در فهرست پروژه خود راه اندازی کنید:
git init
a اضافه کنید .gitignore
فایل های غیر ضروری را از مخزن خود حذف کنید. از پایتون استاندارد استفاده کنید .gitignore
ارائه شده توسط GitHub، و هر گونه استثنای خاص مانند .DS_Store
برای تنظیمات macOS یا IDE (.idea
، .vscode
، .zed
، و غیره):
wget -O .gitignore https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
echo .DS_Store >> .gitignore
3. وابستگی ها را مدیریت کنید
وابستگی های پروژه خود را با استفاده از Poetry نصب کنید:
poetry add fastapi pytest aiogram
بعداً میتوانید همه وابستگیها را با استفاده از:
poetry install
در صورت نیاز به دستورالعمل های خاص تری به اسناد رسمی هر کتابخانه مراجعه کنید.
4. فایل های پیکربندی
ایجاد یک config.py
فایل برای متمرکز کردن تنظیمات برنامه شما – یک رویکرد رایج و موثر.
متغیرهای محیط را برای رازها و پارامترها تنظیم کنید:
touch .env example.env
.env
حاوی داده های حساس است و باید از git نادیده گرفته شود example.env
مقادیر متغیر یا پیش فرض را نگه می دارد و در مخزن بررسی می شود.
5. نقطه ورود برنامه
نقطه ورود درخواست خود را در آن مشخص کنید main.py
:
python_app_architecture/main.py:
def run():
print('Hello, World!')
if __name__ == '__main__': # avoid run on import
run()
پروژه خود را به عنوان یک کتابخانه قابل استفاده کنید و با وارد کردن آن امکان دسترسی برنامه ای را فراهم کنید run
عملکرد در __init__.py
:
python_app_architecture/init.py
from .main import run
با افزودن میانبر، اجرای مستقیم پروژه را با Poetry فعال کنید __main__.py
. این به شما امکان می دهد از دستور استفاده کنید poetry run python python_app_architecture
به جای طولانی تر poetry run python python_app_architecture/main.py
.
python_app_architecture/اصلی.py:
from .main import run
run()
تعریف دایرکتوری ها و لایه ها
سلب مسئولیت:
البته هر اپلیکیشنی متفاوت است و معماری آنها بسته به اهداف و اهداف متفاوت خواهد بود. من نمی گویم که این تنها گزینه صحیح است، اما فکر می کنم که این گزینه نسبتاً متوسط و برای بخش بزرگی از پروژه ها مناسب است. سعی کنید به جای مثال های خاص، روی رویکردها و ایده های اصلی تمرکز کنید.
حال، بیایید دایرکتوری ها را برای لایه های مختلف برنامه تنظیم کنیم.
به طور معمول، عاقلانه است که API خود را نسخه کنید (مثلاً با ایجاد زیر شاخههایی مانند api/v1
) اما فعلاً همه چیز را ساده نگه می داریم و این مرحله را حذف می کنیم.
.
├── python_app_architecture_demo
│ ├── coordinator.py
│ ├── entities
│ ├── general
│ ├── mappers
│ ├── providers
│ ├── repository
│ │ └── models
│ └── services
│ ├── api_service
│ │ └── api
│ │ ├── dependencies
│ │ ├── endpoints
│ │ └── schemas
│ └── telegram_service
└── tests
-
برنامه
- موجودیت ها – ساختارهای داده در سطح برنامه. صرفاً حامل داده ها بدون منطق تعبیه شده است.
- عمومی – کمربند برقی! یک مرکز ادغام شده برای خدمات مشترک، کمکی، و نمای کتابخانه.
- نقشه برداران – متخصصان ترجمه داده ها، تبدیل بین فرم های داده مانند مدل های پایگاه داده به موجودیت ها یا بین فرمت های مختلف داده. این تمرین خوب است که به جای جهانی کردن نقشهبرها، آنها را تا مرز استفاده آن کپسوله کنید. به عنوان مثال، نقشهبردار مدلها – نهادها ممکن است بخشی از ماژول مخزن باشد. مثال دیگر: schemas-entities mapper باید در سرویس api باقی بماند و ابزار خصوصی آن باشد.
- ارائه دهندگان – ستون فقرات منطق کسب و کار اصلی. ارائهدهندگان منطق اصلی برنامه را پیادهسازی میکنند، اما نسبت به جزئیات رابط بیاعتنا میمانند و از انتزاعی بودن و مجزا بودن عملیاتشان اطمینان میدهند.
-
مخزن – کتابداران متولیان دسترسی به دادهها، پیچیدگیهای تعاملات منبع داده را از طریق مدلها انتزاع میکنند، بنابراین عملیات پایگاه داده را از برنامه گستردهتر جدا میکنند.
- مدل ها – تعاریف ساختارهای داده محلی در تعامل با پایگاه داده، متمایز و جدا از نهادها.
-
خدمات – هر سرویس به عنوان یک برنامه فرعی (تقریبا) مستقل عمل می کند و دامنه خاص منطق تجاری خود را هماهنگ می کند و در عین حال وظایف اساسی را به ارائه دهندگان محول می کند. این پیکربندی منطق متمرکز و یکنواخت در سراسر برنامه را تضمین می کند
-
سرویس API – ارتباطات خارجی را از طریق HTTP/S مدیریت می کند که بر اساس چارچوب FastAPI ساختار یافته است.
- وابستگی ها – ابزارها و کمکهای ضروری مورد نیاز بخشهای مختلف API شما، که از طریق سیستم DI FastAPI یکپارچه شدهاند.
- نقاط پایانی – مسیرهایی که منطق کسب و کار شما را از طریق HTTP به طور عمومی نشان می دهند.
- طرحواره ها – تعاریف ساختار داده برای ورودی ها و خروجی های API، محدود به لایه سرویس آنها برای حفظ کپسولاسیون.
- سرویس تلگرام – مشابه سرویس API عمل می کند و عملکردی را از طریق تلگرام بدون تکرار منطق اصلی کسب و کار ارائه می دهد. این امر با تماس با همان ارائه دهندگانی که API Service استفاده می کند، تضمین سازگاری و کاهش افزونگی کد به دست می آید.
-
سرویس API – ارتباطات خارجی را از طریق HTTP/S مدیریت می کند که بر اساس چارچوب FastAPI ساختار یافته است.
- تست ها – این دایرکتوری که صرفاً به آزمایش اختصاص دارد، حاوی تمام کدهای آزمایشی است که جدایی واضح از منطق برنامه را حفظ می کند.
ارتباط بین لایه ها چیزی شبیه به این خواهد بود:
توجه داشته باشید که موجودیت ها اجزای فعال نیستند، بلکه فقط ساختارهای داده ای هستند که بین لایه ها منتقل می شوند:
به خاطر داشته باشید که لایه ها مستقیماً به هم متصل نیستند، بلکه فقط به انتزاعات بستگی دارند. پیاده سازی ها با استفاده از تزریق وابستگی انجام می شوند:
این ساختار انعطاف پذیر به شما اجازه می دهد تا به راحتی قابلیت اضافه کنید، به عنوان مثال، پایگاه داده را تغییر دهید، یک سرویس ایجاد کنید، یا یک رابط جدید را بدون تغییرات اضافی یا تکرار کد متصل کنید، زیرا منطق هر ماژول در لایه خودش قرار دارد:
در همان زمان، تمام منطق یک سرویس جداگانه در آن محصور شده است:
کاوش در کد
نقطه پایانی
بیایید از نقطه پایانی شروع کنیم:
# api_service/api/endpoints/user.py
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from entities.user import UserCreate
from ..dependencies.providers import (
user_provider, # 1
UserProvider # 2
)
router = APIRouter()
@router.post("/register")
async def register(
user: UserCreate, # 3
provider: Annotated[UserProvider, Depends(user_provider)] # 4
):
provider.create_user(user) # 5
return {"message": "User created!"}
- وارد کردن تابع کمکی تزریق وابستگی (ما در یک دقیقه به آن نگاه خواهیم کرد)
- وارد کردن User Provider پروتکل برای حاشیه نویسی نوع
- نقطه پایانی نیاز به داشتن بدنه درخواست دارد
UserCreate
طرح با فرمت json - را
provider
پارامتر درregister
تابع نمونه ای از پیاده سازی استUserProvider
تزریق شده توسط FastAPI با استفاده ازDepends
سازوکار. - را
create_user
روش ازUserProvider
با داده های کاربر تجزیه شده فراخوانی می شود. این یک جدایی واضح از نگرانیها را نشان میدهد، جایی که لایه API منطق تجاری را به لایه ارائهدهنده تفویض میکند، و به این اصل پایبند است که لایههای رابط نباید دارای منطق تجاری باشند.
User Provider
حالا بیایید منطق کسب و کار را ببینیم:
# providers/user_provider.py
from typing import Protocol, runtime_checkable, Callable
from typing_extensions import runtime_checkable
from repository import UserRepository
from providers.mail_provider import MailProvider
from entities.user import UserCreate
@runtime_checkable
class UserProvider(Protocol): # 1
def create_user(self, user: UserCreate): ...
@runtime_checkable
class UserProviderOutput(Protocol): # 2
def user_provider_created_user(self, provider: UserProvider, user: UserCreate): ...
class UserProviderImpl: # 3
def __init__(self,
repository: UserRepository, # 4
mail_provider: MailProvider, # 4
output: UserProviderOutput | None, # 5
on_user_created: Callable[[UserCreate], None] | None # 6
):
self.repository = repository
self.mail_provider = mail_provider
self.output = output
self.on_user_created = on_user_created
# Implementation
def create_user(self, user: UserCreate): # 7
self.repository.add_user(user) # 8
self.mail_provider.send_mail(user.email, f"Welcome, {user.name}!") # 9
if output := self.output: # unwraping the optional
output.user_provider_created_user(self, user) # 10
# 11
if on_user_created := self.on_user_created:
on_user_created(user)
-
تعریف رابط:
UserProvider
پروتکلی است که روش را مشخص می کندcreate_user
، که هر کلاسی که به این پروتکل پایبند است باید آن را پیاده سازی کند. این به عنوان یک قرارداد رسمی برای عملکرد ایجاد کاربر عمل می کند. -
پروتکل ناظر:
UserProviderOutput
به عنوان یک ناظر (یا نماینده) عمل می کند که هنگام ایجاد کاربر مطلع می شود. این پروتکل اتصال آزاد را فعال می کند و معماری رویداد محور برنامه را بهبود می بخشد. -
اجرای پروتکل:
UserProviderImpl
منطق ایجاد کاربر را پیاده سازی می کند اما نیازی به اعلام صریح پایبندی خود به آن نداردUserProvider
به دلیل ماهیت پویای پایتون و استفاده از تایپ اردک. -
وابستگی های اصلی: سازنده می پذیرد
UserRepository
وMailProvider
هر دو به عنوان پروتکل تعریف می شوند – به عنوان پارامتر. تنها با تکیه بر این پروتکل ها،UserProviderImpl
از پیادهسازیهای خاص جدا باقی میماند و اصول تزریق وابستگی را نشان میدهد که در آن ارائهدهنده جزئیات زیربنایی را نادیده میگیرد و فقط از طریق قراردادهای تعریفشده با هم ارتباط برقرار میکند. -
نماینده خروجی اختیاری: سازنده یک گزینه اختیاری را می پذیرد
UserProviderOutput
به عنوان مثال، که در صورت ارائه، پس از تکمیل ایجاد کاربر اطلاع رسانی خواهد شد. -
عملکرد برگشت به تماس: به عنوان جایگزینی برای نمایندگی خروجی، قابل فراخوانی است
on_user_created
را می توان برای رسیدگی به اقدامات اضافی پس از ایجاد کاربر، ارائه انعطاف پذیری در پاسخ به رویدادها ارسال کرد. -
منطق تجاری مرکزی:
create_user
متد منطق اصلی کسب و کار برای افزودن کاربر را در بر می گیرد و جدایی از مدیریت API را نشان می دهد. -
تعامل مخزن: از
UserRepository
برای انتزاع عملیات پایگاه داده (به عنوان مثال، افزودن یک کاربر)، اطمینان از اینکه ارائه دهنده به طور مستقیم پایگاه داده را دستکاری نمی کند. -
منطق تجاری گسترده: شامل ارسال ایمیل از طریق
MailProvider
، نشان می دهد که مسئولیت های ارائه دهنده می تواند فراتر از عملیات ساده CRUD باشد. -
اطلاع رسانی رویداد: اگر یک نماینده خروجی ارائه شود، این نماینده را در مورد رویداد ایجاد کاربر مطلع میکند و از الگوی مشاهدهگر برای افزایش تعامل و واکنشهای مدولار به رویدادها استفاده میکند.
-
اجرای برگشت به تماس: به صورت اختیاری یک تابع callback را اجرا می کند و روشی ساده برای گسترش عملکرد بدون سلسله مراتب کلاسی پیچیده یا وابستگی ارائه می دهد.
وابستگی های FastAPI
خوب، اما چگونه می توان ارائه دهنده را نمونه برداری کرد و آن را تزریق کرد؟ بیایید نگاهی به کد تزریق بیندازیم که توسط موتور DI FastAPI طراحی شده است:
# services/api_service/api/dependencies/providers.py
from typing import Annotated
from fastapi import Request, Depends
from repository import UserRepository
from providers.user_provider import UserProvider, UserProviderImpl
from providers.mail_provider import MailProvider
from coordinator import Coordinator
from .database import get_session, Session
import config
def _get_coordinator(request: Request) -> Coordinator:
# private helper function
# NOTE: You can pass the DIContainer in the same way
return request.app.state.coordinator
def user_provider(
session: Annotated[Session, Depends(get_session)], # 1
coordinator: Annotated[Coordinator, Depends(_get_coordinator)] # 2
) -> UserProvider: # 3
# UserProvider's lifecycle is bound to short endpoint's lifecycle, so it's safe to use strong references here
return UserProviderImpl( # 4
repository=UserRepository(session), # 5
mail_provider=MailProvider(config.mail_token), # 6
output=coordinator, # 7
on_user_created=coordinator.on_user_created # 8
# on_user_created: lambda: coordinator.on_user_created() # add a lambda if the method's signature is not compatible
)
-
دریافت یک جلسه پایگاه داده از طریق سیستم تزریق وابستگی FastAPI، با اطمینان از اینکه هر درخواست یک جلسه تمیز دارد.
-
به دست آوردن یک نمونه از
Coordinator
از حالت برنامه، که مسئول مدیریت وظایف گسترده تر در سطح برنامه و عمل به عنوان مدیر رویداد است. -
توجه: تابع پروتکل را برمی گرداند اما پیاده سازی دقیق را ندارد
-
ساختن یک نمونه از
UserProviderImpl
با تزریق تمام وابستگی های لازم. این نشاندهنده کاربرد عملی تزریق وابستگی برای مونتاژ اشیاء پیچیده است. -
مقدار دهی اولیه
UserRepository
با جلسه به دست آمده از سیستم DI FastAPI. این مخزن تمام عملیات پایداری داده را مدیریت می کند و تعاملات پایگاه داده را از ارائه دهنده انتزاع می کند. -
راه اندازی
MailProvider
با استفاده از یک نشانه پیکربندی -
تزریق کردن
Coordinator
به عنوان پروتکل خروجی این فرض می کند کهCoordinator
را اجرا می کندUserProviderOutput
پروتکل، به آن اجازه می دهد تا هنگام ایجاد یک کاربر، اعلان ها را دریافت کند. -
روشی را از
Coordinator
به عنوان یک تماس برگشتی که پس از ایجاد کاربر اجرا می شود. این اجازه می دهد تا عملیات یا اعلان های اضافی به عنوان یک اثر جانبی فرآیند ایجاد کاربر فعال شود.
این رویکرد ساختاریافته تضمین می کند که UserProvider
مجهز به کلیه ابزارهای لازم برای انجام وظایف خود با رعایت اصول اتصال شل و انسجام بالا.
هماهنگ کننده
کلاس Coordinator به عنوان ارکستر اصلی در برنامه شما عمل می کند، خدمات مختلف، تعاملات، رویدادها، تنظیم حالت اولیه و تزریق وابستگی ها را مدیریت می کند. در اینجا به تفصیل نقش ها و عملکردهای آن بر اساس کد ارائه شده است:
# coordinator.py
from threading import Thread
import weakref
import uvicorn
import config
from services.api_service import get_app as get_fastapi_app
from entities.user import UserCreate
from repository.user_repository import UserRepository
from providers.mail_provider import MailProvider
from providers.user_provider import UserProvider, UserProviderImpl
from services.report_service import ReportService
from services.telegram_service import TelegramService
class Coordinator:
def __init__(self):
self.users_count = 0 # 1
self.telegram_service = TelegramService( # 2
token=config.telegram_token,
get_user_provider=lambda session: UserProviderImpl(
repository=UserRepository(session),
mail_provider=MailProvider(config.mail_token),
output=self,
on_user_created=self.on_user_created
)
)
self.report_service = ReportService(
get_users_count = lambda: self.users_count # 3
)
# Coordinator's Interface
def setup_initial_state(self):
fastapi_app = get_fastapi_app()
fastapi_app.state.coordinator = self # 4
# 5
fastapi_thread = Thread(target=lambda: uvicorn.run(fastapi_app))
fastapi_thread.start()
# 6
self.report_service.start()
self.telegram_service.start()
# UserProviderOutput Protocol Implementation
def user_provider_created_user(self, provider: UserProvider, user: UserCreate):
self.on_user_created(user)
# Event handlers
def on_user_created(self, user):
print("User created: ", user)
self.users_count += 1
# 7
if self.users_count >= 10_000:
self.report_service.interval_seconds *= 10
elif self.users_count >= 10_000_000:
self.report_service.stop() # 8
- برخی از وضعیت ها را می توان بین ارائه دهندگان، خدمات، لایه ها و کل برنامه مختلف به اشتراک گذاشت
- مونتاژ پیاده سازی ها و تزریق وابستگی ها
- از مراجع دایره ای، بن بست ها و نشت حافظه در اینجا آگاه باشید، کد کامل جزئیات را ببینید
- نمونه هماهنگ کننده را به وضعیت برنامه FastAPI منتقل کنید تا بتوانید از طریق سیستم DI FastAPI در نقاط پایانی به آن دسترسی داشته باشید.
- همه خدمات را در موضوعات جداگانه شروع کنید
- قبلاً در یک رشته جداگانه در داخل سرویس اجرا می شود
- برای مثال، برخی از منطق خدمات متقابل اینجا
- نمونه ای از خدمات کنترلی از هماهنگ کننده
این ارکستراتور کنترل و ارتباط بین اجزای مختلف را متمرکز می کند و مدیریت و مقیاس پذیری برنامه را افزایش می دهد. این به طور موثر اقدامات بین سرویس ها را هماهنگ می کند و اطمینان می دهد که برنامه به طور مناسب به تغییرات حالت و تعاملات کاربر پاسخ می دهد. این الگوی طراحی برای حفظ تفکیک تمیز نگرانیها و فعال کردن رفتار برنامههای کاربردی قویتر و انعطافپذیر بسیار مهم است.
کانتینر
با این حال، در برنامه های بزرگ مقیاس DI دستی می تواند به مقدار قابل توجهی از کد دیگ بخار منجر شود. این زمانی است که DI Container برای نجات می آید. DI Containers یا Dependency Injection Containers ابزارهای قدرتمندی هستند که در توسعه نرم افزار برای مدیریت وابستگی ها در یک برنامه استفاده می شوند. آنها به عنوان یک مکان مرکزی عمل می کنند که در آن اشیاء و وابستگی های آنها ثبت و مدیریت می شوند. هنگامی که یک شی به یک وابستگی نیاز دارد، DI Container به طور خودکار نمونه سازی و ارائه این وابستگی ها را کنترل می کند، و اطمینان حاصل می کند که اشیا تمام اجزای لازم را برای عملکرد مؤثر دریافت می کنند. این رویکرد با انتزاع کردن منطق پیچیده مدیریت وابستگی به دور از منطق تجاری برنامه، اتصال شل را ترویج میکند، آزمایشپذیری را افزایش میدهد و قابلیت نگهداری کلی پایگاه کد را بهبود میبخشد. کانتینرهای DI فرآیند توسعه را با خودکارسازی و متمرکز کردن پیکربندی وابستگی های مؤلفه ساده می کنند.
برای پایتون، کتابخانههای زیادی وجود دارد که پیادهسازیهای مختلف DI Container را ارائه میکنند، تقریباً همه آنها را نگاه کردم و بهترین IMO را نوشتم.
-
python-dependency-injector – خودکار، مبتنی بر کلاس، دارای گزینه های مختلف چرخه زندگی مانند Singleton یا Factory
-
lagom – یک رابط فرهنگ لغت با تفکیک خودکار
-
dishka – کنترل دامنه خوب از طریق مدیر زمینه
-
این بستگی دارد – پشتیبانی از مدیران زمینه (اشیاء مورد نیاز برای بسته شدن در پایان)، ادغام بومی fastapi
-
punq – رویکرد کلاسیک تر با
register
وresolve
مواد و روش ها -
rodi – کلاسیک، ساده، خودکار
main.py
برای پایان، اجازه دهید فایل main.py را به روز کنیم:
# main.py
from coordinator import Coordinator
def run(): # entry point, no logic here, only run the coordinator
coordinator = Coordinator()
coordinator.setup_initial_state()
if __name__ == '__main__':
run()
نتیجه
برای به دست آوردن درک جامعی از استراتژی های معماری و پیاده سازی مورد بحث، بررسی همه فایل های موجود در مخزن مفید است. با وجود مقدار محدود کد، هر فایل با نظرات روشنگر و جزئیات اضافی غنی شده است که درک عمیق تری از ساختار و عملکرد برنامه ارائه می دهد. بررسی این جنبهها، آشنایی شما را با سیستم افزایش میدهد و اطمینان میدهد که برای تطبیق یا گسترش مؤثر برنامه به خوبی مجهز هستید.
این رویکرد به طور جهانی برای برنامه های مختلف پایتون مفید است. این برای سرورهای پشتیبان بدون حالت مانند سرورهای ساخته شده با FastAPI موثر است، اما مزایای آن به ویژه در برنامهها و برنامههای بدون چارچوب که حالت را مدیریت میکنند، آشکار است. این شامل برنامههای دسکتاپ (هم رابط کاربری گرافیکی و هم خط فرمان)، و همچنین سیستمهایی است که دستگاههای فیزیکی مانند دستگاههای اینترنت اشیا، روباتیک، هواپیماهای بدون سرنشین و سایر فناوریهای سختافزار محور را کنترل میکنند.
علاوه بر این، خواندن را به شدت توصیه می کنم کد پاک توسط رابرت مارتین برای غنی سازی بیشتر. در اینجا می توانید خلاصه و نکات کلیدی را بیابید. این منبع اصول و شیوه های اساسی را در اختیار شما قرار می دهد که برای حفظ استانداردهای بالا در توسعه نرم افزار بسیار مهم هستند.