کوئرینویسی در Django ORM؛ filter، annotate و روابط مدلها
Django ORM فقط راهی برای حذف نوشتن SQL نیست؛ باید زمان اجرای QuerySet، معنای اتصالها و هزینه خواندن روابط را …
معماری تمیز یا Clean Architecture یعنی مرزبندی قواعد مهم برنامه از جزئیات قابل تغییر و کنترل جهت وابستگی. در این راهنما لایهها و درگاهها را با مثال کوچک پایتون و اتصال به جنگو میبینید؛ بدون آنکه آموزش به معرفی کتاب یا ساختن پوشههای اضافی محدود شود.
آخرین بهروزرسانی: ۱۷ آبان ۱۴۰۵ — منابع بررسیشده در این روز: نوشته معماری تمیز رابرت مارتین، ششضلعی کاکبرن، پیازی پالرمو، مارتین فاولر، مایکروسافت و دوازدهفاکتور.
⚡ پاسخ کوتاه
وابستگی کد باید از جزئیات بیرونی به قواعد درونی باشد. درگاه موردنیاز هسته را داخل تعریف کنید و مبدل بیرونی آن را اجرا کند. این کار آزمون مستقل را آسانتر میکند، اما هزینه قرارداد و نگاشت دارد؛ برای هر پروژه لازم نیست همه لایهها و الگوها را یکجا بسازید.
📑 فهرست مطالب
معماری تمیز تلاشی برای نگه داشتن قواعد مهم کسبوکار دور از جزئیات متغیر است. اگر برای آزمایش محاسبه قیمت، مجبورید سرور وب، پایگاه داده و چند خدمت بیرونی را راه بیندازید، مرزهای برنامه احتمالاً با هم مخلوط شدهاند. هدف این معماری آن است که تغییر ابزار بیرونی، بیدلیل به هسته قواعد نشت نکند و بتوانید بخشهای مهم را مستقلتر بفهمید و آزمایش کنید.
این هدف با «هیچ وابستگی نداشتن» فرق دارد. هر برنامه واقعی ابزار و کتابخانه دارد؛ بحث بر سر جهت و محل وابستگی است. هسته نباید برای بیان یک قاعده عمومی، نام درخواست وب یا مدل اختصاصی پایگاه داده را بشناسد. در عوض، لایه بیرونی باید داده را به شکل موردنیاز هسته تبدیل کند. این مرزبندی هزینه دارد، پس آن را برای مسئله واقعی انتخاب کنید، نه برای زیاد شدن پوشهها.
رابرت سی. مارتین در نوشته معماری تمیز خود، ایدههایی با هدف جداسازی مسئولیتها را جمعبندی میکند و کتابی نیز با همین موضوع دارد. برای معرفی کتابها و نویسنده، معرفی آثار رابرت مارتین و معرفی عمو باب را ببینید. این مقاله نقد کتاب نیست؛ تمرکزش قانون وابستگی، مرزهای اجرایی و یک نمونه کوچک پایتونی است تا بتوانید اصل را در پروژه تشخیص دهید.
ممکن است نام متغیرها عالی باشد اما قاعده تخفیف در پنج ویو تکرار شده باشد. ممکن است چهار پوشه زیبا داشته باشید ولی همه لایهها مدل پایگاه داده را وارد کنند. اصول کدنویسی حرفهای و کد تمیز به کیفیت کد کمک میکند، اما مرز معماری را باید با مسیر تغییر و وابستگی بسنجید. پرسش مهم این است که برای تغییر یک سیاست، کدام بخشها ناچار به تغییر میشوند.
در تصویر شناختهشده این معماری، قواعد عمومیتر در مرکز و جزئیات ابزارها در بیرون قرار میگیرند. قانون وابستگی درباره وابستگی کد منبع است: بخش داخلی نباید نام تعریفشده در بیرون را لازم داشته باشد. جریان اجرای برنامه ممکن است از وب به هسته و دوباره به ذخیرهسازی برود، اما جهت وارد کردن کدها باید با مرزها سازگار بماند؛ جریان کنترل و وابستگی را یکی نگیرید.
موجودیت لزوماً یک مدل جدول یا کلاس بزرگ نیست. میتواند یک ساختار داده و چند تابع باشد که قاعده عمومی را بیان میکند؛ مثلاً تعداد سفارش باید مثبت باشد یا مبلغ یک خط از قیمت واحد و تعداد محاسبه شود. اگر این قاعده در وب، خط فرمان و پردازش پسزمینه یکسان است، جای مناسبی برای قرار گرفتن در هسته دارد. برای هر جدول هم مجبور نیستید موجودیت تازهای بسازید.
مورد کاربرد یک هدف مشخص را هماهنگ میکند: گرفتن پیشنهاد قیمت، ثبت درخواست یا تأیید یک عملیات مجاز. این لایه تعیین میکند چه دادهای لازم است و کدام درگاه باید فراخوانی شود، اما لازم نیست جزئیات درخواست شبکه یا کوئری را بداند. اگر رفتار برنامه تغییر کند، تغییر این لایه طبیعی است؛ استقلال از ابزار به معنی ثابت ماندن همه منطق برای همیشه نیست.
کنترلکننده وب، نگاشت داده و پیادهسازی درگاه ذخیرهسازی در این ناحیه قرار میگیرند. مسئولیتشان ترجمه است: ورودی وب را به داده معتبر مورد کاربرد و نتیجه را به پاسخ مناسب تبدیل کنند. اگر شیء درخواست یا ردیف اختصاصی پایگاه داده را مستقیم به قلب منطق بفرستید، مرز را ظاهراً حفظ کردهاید ولی وابستگی را از مسیر داده وارد کردهاید. شکل داده عبوری هم بخشی از طراحی است.
فریمورک وب، پایگاه داده و سرویس بیرونی جزئیات اجراییاند. این حرف به معنی کماهمیت بودنشان نیست؛ امنیت، پایداری و کارایی زیرساخت حیاتی است. منظور این است که قاعده دامنه نباید برای توصیف خودش به آنها وابسته باشد. در نوشته اصلی نیز چهار دایره الزام عددی نیستند؛ میتوانید مرز کمتر یا بیشتری داشته باشید، به شرط آنکه اصل وابستگی را حفظ کنید.

فرض کنید مورد کاربرد برای محاسبه پیشنهاد قیمت، به قیمت محصول نیاز دارد. اگر مستقیم مدل جنگو را صدا بزند، هسته برنامه به جزئیات ذخیرهسازی گره میخورد. میتوانید قرارداد کوچکی در سمت داخلی تعریف کنید: «برای این شناسه کالا، قیمت را بده». مبدل بیرونی این قرارداد را با جنگو یا منبع دیگری اجرا میکند. در زمان اجرا، هسته از درگاه استفاده میکند؛ در کد، به پیادهسازی بیرونی وابسته نیست.
این کار در پایتون الزاماً به سلسلهمراتب کلاسهای طولانی نیاز ندارد. یک پروتکل، تابع تزریقشده یا شیء با قرارداد روشن کافی است. مهم آن است که قرارداد را نیاز مصرفکننده تعیین کند، نه اینکه همه متدهای ابزار بیرونی را کپی کنید. درگاهی با دهها متد نامرتبط، معمولاً وابستگی بزرگ را فقط پشت یک نام تازه پنهان میکند و آزمون را همچنان دشوار نگه میدارد.
برای داده عبوری، ساختارهای ساده و مشخص انتخاب کنید؛ شناسه، عدد، رشته یا داده انتقالی که معنایش در هسته روشن است. اگر QuerySet، درخواست وب یا مدل اعتبارسنجی وابسته به فریمورک را به درون میدهید، بررسی کنید آیا هسته مجبور به دانستن قرارداد آن ابزار شده است. گاهی یک انتخاب عملگرایانه قابل قبول است، اما باید آن را بهعنوان بدهی یا تصمیم آگاهانه ثبت کنید، نه استقلال کامل فرض کنید.
مثال ما فقط پیشنهاد قیمت یک کالا را محاسبه میکند؛ نه سفارش نهایی، نه کنترل موجودی و نه پرداخت. دادهها آموزشیاند و مبلغها واحد پول واقعی ندارند. برای کوتاه ماندن مثال، موجودیت، درگاه و مورد کاربرد در یک فایل هسته آمدهاند؛ در پروژه بزرگ میتوانید آنها را جدا کنید. مرز منطقی مهمتر از تعداد فایل است و هسته زیر هیچ وابستگی به جنگو ندارد.
# quote_core.py
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol
@dataclass(frozen=True)
class QuoteLine:
unit_price: Decimal
quantity: int
def __post_init__(self):
if self.quantity <= 0:
raise ValueError("quantity must be positive")
if not self.unit_price.is_finite() or self.unit_price < 0:
raise ValueError("invalid price")
@property
def total(self) -> Decimal:
return self.unit_price * self.quantity
class PriceCatalog(Protocol):
def price_for(self, sku: str) -> Decimal: ...
def make_quote(sku: str, quantity: int, catalog: PriceCatalog) -> QuoteLine:
price = catalog.price_for(sku)
return QuoteLine(unit_price=price, quantity=quantity)
در این کد، محاسبه مبلغ در موجودیت است و مورد کاربرد دریافت قیمت را هماهنگ میکند. نوعدهی پایتون بهتنهایی اعتبارسنجی زمان اجرا نیست؛ در مرز ورودی باید نوع و دامنه تعداد را هم کنترل کنید. قیمت از داده ارسالی کاربر دریافت نمیشود، بلکه از درگاه فهرست کالا میآید. این جداسازی جلوی یک خطای متداول را میگیرد: اعتماد به مبلغی که مرورگر برای شما فرستاده است.
برای آزمون، به پایگاه داده نیاز نداریم. یک مبدل کوچک قیمت ساختگی مشخصی میدهد و میتوانیم محاسبه و رد ورودی نامعتبر را کنترل کنیم. این بخش در آمادهسازی مطلب با پایتون محلی اجرا و بررسی شد؛ نتیجه آن را به آزمون یک فروشگاه واقعی یا یکپارچگی جنگو تعمیم نمیدهیم. تست هسته یک لایه از اطمینان است، نه جایگزین آزمون اتصالها و مجوزها.
from decimal import Decimal
from quote_core import make_quote
class MemoryCatalog:
def price_for(self, sku: str) -> Decimal:
if sku != "demo":
raise LookupError("unknown sku")
return Decimal("12.50")
line = make_quote("demo", 2, MemoryCatalog())
assert line.total == Decimal("25.00")
try:
make_quote("demo", 0, MemoryCatalog())
except ValueError:
pass
else:
raise AssertionError("invalid quantity accepted")

فرض میکنیم مدل Product با فیلدهای sku یکتا، price از نوع اعشاری و active از نوع بولی دارید. مبدل زیر قیمت را از مدل میخواند و خطای نبود کالا را به خطای قراردادی تبدیل میکند. نامها نمونهاند و باید با پروژه شما تطبیق یابند. این قطعه برای نمایش اتصال است و اجرای یکپارچه آن در محیط جنگو برای این مقاله انجام نشده است.
# adapters.py
from .models import Product
class DjangoCatalog:
def price_for(self, sku):
try:
product = Product.objects.only("price").get(sku=sku, active=True)
except Product.DoesNotExist as exc:
raise LookupError("unknown sku") from exc
return product.price
حالا ویو باید ورودی را اعتبارسنجی کند، مورد کاربرد را فراخوانی کند و نتیجه را به پاسخ تبدیل کند. برای مرور نقش ویو، ساختار پروژه جنگو را ببینید. در مثال زیر فرم، تعداد را محدود میکند؛ این سقف صرفاً قرارداد آموزشی دلخواه است. محصول عمومی به قواعد دسترسی، نرخ درخواست، ثبت رویداد و سیاست خطای متناسب نیاز دارد و نباید این نمونه را مسیر آماده تولید دانست.
# views.py
from django import forms
from django.http import JsonResponse
from django.views.decorators.http import require_GET
from .adapters import DjangoCatalog
from .quote_core import make_quote
class QuoteForm(forms.Form):
sku = forms.CharField(max_length=80)
quantity = forms.IntegerField(min_value=1, max_value=100)
@require_GET
def quote_view(request):
form = QuoteForm(request.GET)
if not form.is_valid():
return JsonResponse({"error": "invalid input"}, status=400)
try:
line = make_quote(catalog=DjangoCatalog(), **form.cleaned_data)
except LookupError:
return JsonResponse({"error": "not found"}, status=404)
except ValueError:
return JsonResponse({"error": "invalid quote"}, status=400)
return JsonResponse({"total": str(line.total)})
اگر بعداً عملیات نوشتن اضافه شود، مرز تراکنش را هم طراحی کنید. ذخیره سفارش، کاهش موجودی و ارسال پیام بیرونی نباید با چند فراخوانی پراکنده و بدون قرارداد شکست هماهنگ شوند. ممکن است واحد کار، ثبت رویداد یا الگوی دیگری لازم شود؛ اینها را فقط وقتی نیازشان روشن است اضافه کنید. نمونه خواندنی پیشنهاد قیمت عمداً از ادعای حل همه مشکلات تراکنشی دور مانده است.
مدل، ویو و کنترلکننده یا شکل مدل، ویو و قالب، بیشتر به سازماندهی تعامل و ارائه مربوطاند. معماری تمیز درباره مرز قواعد و جهت وابستگی در مقیاس گستردهتر صحبت میکند. میتوانید در لایه مبدل، ساختار متناسب با فریمورک داشته باشید و همچنان قواعد مرکزی را جدا نگه دارید. انتخاب یکی، الزاماً رد دیگری نیست؛ این مفاهیم در یک سطح واحد رقابت نمیکنند.
معماری ششضلعی بر درگاهها و مبدلها و استقلال منطق از فناوری بیرونی تأکید میکند؛ معماری پیازی هم جهت وابستگی به مرکز را برجسته میسازد. اشتراکهای مهمی با معماری تمیز دارند، اما نامگذاری و تمرکز توضیحشان یکسان نیست. برای شناخت دقیق، نوشتههای اصلی کاکبرن و پالرمو را بخوانید. صرف کشیدن دایره یا ششضلعی دور چند پوشه، هیچکدام از این اصول را اجرا نمیکند.
میتوانید یک مونولیت با مرزهای تمیز داشته باشید یا چند میکروسرویس با وابستگیهای آشفته بسازید. مقایسه مونولیتیک و میکروسرویس درباره مرز استقرار و ارتباط سرویسهاست؛ قانون وابستگی میتواند داخل هر کدام اجرا شود. برای جداسازی قواعد، لازم نیست ابتدا شبکه و سرویس مستقل اضافه کنید. گاهی همان ماژولبندی روشن در یک برنامه، مسئله را با هزینه کمتر حل میکند.
راهنمای دوازدهفاکتور اپلیکیشن نیز بیشتر به ساخت و اجرای برنامه خدمتمحور، پیکربندی و محیط مربوط است. این اصول مکمل مرزبندی کد هستند، نه جایگزین آن. فرایند یکپارچه منطقی هم به شیوه پیشبرد توسعه مربوط میشود؛ نام یک فرایند مدیریت پروژه، بهخودیخود جهت وابستگی وارد کردن ماژولها را تعیین نمیکند. هر ابزار را برای همان مسئلهای که حل میکند بشناسید.

مزیت مهم، آزمون قواعد بدون آماده کردن تمام زیرساخت است. مزیت دیگر، روشن شدن مسئولیت تغییر است: وقتی فرمت پاسخ عوض میشود، قرار نیست محاسبه دامنه تغییر کند. اما استقلال رایگان نیست؛ مبدل، نگاشت داده، قرارداد و آزمون اتصال میخواهید. اگر محصول فقط چند عملیات ساده داده دارد و قواعد پیچیدهای ندارد، پیادهسازی کامل همه الگوها ممکن است هزینه اضافی بسازد.
نشانه نیاز را از درد واقعی بگیرید: منطق تکرارشونده در ویوها، آزمونهای شکننده وابسته به شبکه، تغییر یک خدمت بیرونی که چند بخش نامرتبط را میشکند یا قواعدی که در چند ورودی باید یکسان باشند. ابتدا یک مورد کاربرد مهم را جدا کنید و اثرش را بسنجید. انتقال یکباره کل برنامه به ساختار تازه، بدون آزمون رفتاری، ریسک زیادی دارد و لزوماً سود بیشتری ایجاد نمیکند.
اول، مخزن عمومیای که فقط همه متدهای مدل را با نام تازه تکرار میکند؛ این اغلب انتزاع مفیدی به مصرفکننده نمیدهد. دوم، دامنهای که در نام مستقل است اما برای اعتبارسنجی، مدل وب یا پایگاه داده را وارد میکند. سوم، تبدیلهای بیشمار داده برای عملیاتی که هیچ قاعده مهمی ندارد. در هر سه، ظاهر معماری جای دلیل مهندسی را گرفته است؛ مرز باید از تغییر مورد انتظار دفاع کند.
یک آزمون کاربردی این است که هسته را بدون نصب فریمورک بارگذاری و قواعدش را آزمایش کنید. سپس برای مبدل واقعی، آزمون اتصال با پایگاه داده بنویسید. هیچیک جای دیگری را نمیگیرد: تست حافظهای نمیفهمد نام فیلد واقعی اشتباه است و تست اتصال هم نباید تنها راه بررسی یک ضرب ساده باشد. برای ارزیابی پروژه خودتان، خدمات توسعه وبسایت رادینو را بر مبنای نیاز و محدودیت واقعی بررسی کنید.
کد لایه داخلی نباید نامها و ساختارهای لایه بیرونی را لازم داشته باشد. برای ارتباط با بیرون، قراردادی متناسب با نیاز هسته تعریف میشود و مبدل بیرونی آن را اجرا میکند. جهت اجرای فراخوانی با جهت وابستگی کد یکی نیست.
اولی مرز قواعد و جزئیات را توضیح میدهد؛ دومی الگویی برای سازماندهی تعامل و ارائه است. میتوانید بخش ارائه را با الگوی رایج فریمورک بسازید و منطق اصلی را پشت مرز روشن نگه دارید؛ الزاماً دو انتخاب متضاد نیستند.
اگر فقط پوشه و کلاس اضافه کنید، بله میتواند افراطی باشد. جداسازی یک قاعده مهم در تابع خالص هم قدم مفیدی است. از حداقل مرزی شروع کنید که آزمون و تغییر را بهتر میکند، نه از بازسازی همه نمودارهای کتاب.
هدفهای مشترکی مثل جداسازی مسئولیت و استقلال از ابزار دارند، اما زاویه بیان، نقش درگاهها و نامگذاری لایهها فرق دارد. نوشتههای اصلی را مبنا بگیرید و از تبدیل تفاوت نامها به جنگ سلیقهای پرهیز کنید.
یک مورد کاربرد با قاعده روشن انتخاب کنید، ورودی و خروجی ساده تعریف کنید و وابستگی مدل را در مبدل بیرونی نگه دارید. سپس آزمون هسته و آزمون اتصال اضافه کنید. کل پروژه را بدون نیاز و آزمون به چندین لایه تازه تقسیم نکنید.
از بیرون کشیدن یک قاعده محدود شروع کنید و پیش از تغییر ساختار، رفتار فعلی را با آزمون ثبت کنید. ورودی و خروجی همان قاعده را مشخص کنید، وابستگی بیرونی را به یک درگاه کوچک منتقل کنید و مسیر قدیمی را از مبدل تازه عبور دهید. در هر قدم باید بتوانید همان قابلیت را تحویل دهید. تغییر همزمان نامها، ساختار، قرارداد داده و منطق کسبوکار، پیدا کردن علت خطا را سخت میکند و بازگشت را پرهزینهتر میسازد.
هسته میتواند خطای معنادار دامنه یا نتیجه شکست مشخص داشته باشد، اما نباید برای اعلام آن به شماره وضعیت وب وابسته شود. مبدل وب آن شکست را به پاسخ متناسب تبدیل میکند؛ مبدل خط فرمان ممکن است پیام و کد خروج دیگری بدهد. جزئیات داخلی اتصال و اطلاعات حساس را در پاسخ عمومی نریزید. از سوی دیگر، گرفتن همه استثناها و تبدیلشان به «درخواست نامعتبر» هم خطای برنامهنویسی را پنهان میکند؛ خطاهای قابل انتظار را از خرابی غیرمنتظره جدا نگه دارید.
برای ارزیابی موفقیت، فقط تعداد تستها را نشمارید. ببینید آیا تغییر یک منبع بیرونی هنوز هسته را ویرایش میکند، آیا قرارداد ورودی خواناست و آیا اعضای تیم محل درست هر قاعده را میفهمند. اگر طراحی تازه صرفاً چند نام تخصصی اضافه کرده و توضیح تغییر را دشوارتر ساخته است، مرزها را سادهتر کنید. هدف، قابل فهم شدن رفتار و کاهش هزینه تغییر است، نه وفاداری ظاهری به یک نمودار.
نظرات 0
برای ارسال نظر وارد حساب کاربری شوید.
هنوز نظری ثبت نشده؛ اولین نفر باشید.