راهنمای سبک اعمال Domain-Driven Design در Django
مقدمه: معماری فعلی Django
Django به طور پیشفرض از معماری Model-View-Template (MVT) پیروی میکند که شباهت زیادی به الگویModel-View-Controller (MVC) دارد.
در این معماری:
- Model : لایه دسترسی به داده و منطق کسب وکار
- View : لایه منطق برنامه و واسط بین Model و Template
- : Template لایه نمایشی و رابط کاربری
در پروژه های کوچک، این معماری ساده و کارآمد است. اما با بزرگ شدن پروژه، مشکلاتی ظاهر میشوند:
1. مدل ها و ویو ها به سرعت پیچیده و حجیم میشوند.
2. منطق کسب وکار در سراسر پروژه پراکنده میشود.
3. تست پذیری کاهش می یابد.
4. وابستگی بین کامپوننت ها افزایش می یابد.
5. تغییر در یک بخش ممکن است اثرات ناخواسته در بخشهای دیگر ایجاد کند.
مشکلات معماری پیشفرض Django
معماری پیشفرض Django برای پروژه های بزرگ چند مشکل اساسی دارد:
1. عدم جداسازی مناسب لایه ها: منطق کسب وکار (Business Logic ) اغلب در مدل ها یا ویو ها قرار میگیرد که باعث میشود تغییرات در یک حوزه (Domain) بر حوزه های دیگر تأثیر بگذارد.
2. وابستگی متقابل: در پروژه های بزرگ، مدل ها و ویو ها به شدت به یکدیگر وابسته میشوند که نگهداری را سخت میکند.
3. عدم تمرکز بر Domain : معماری پیش فرض ، توسعه دهندگان را تشویق میکند بیشتر به جنبه های فنی (مانند مدل های دیتابیس) توجه کنند تا منطق کسب وکار.
4. مشکل در تست پذیری: به دلیل ترکیب منطق کسب وکار با لایه های دیگر، نوشتن تست های واحد مستقل دشوار میشود.
پس این راهنمای سبک با معرفی اصول Domain-Driven Design (DDD) سعی دارد این مشکلات را حل کند.
پیشینه Domain-Driven Design
DDD یک رویکرد توسعه نرم افزار است که تمرکز اصلی آن بر روی مدل سازی دقیق دامنه (Domain) کسب وکار است. اریک اوانز در کتاب خود به نام
Domain-Driven Design: Tackling Complexity in the Heart of Software" "
این مفاهیم را معرفی کرد.
مفاهیم کلیدی DDD از دیدگاه فاولر:
الف) Domain Model
مدل دامنه نمایش انتزاعی از دانش و فعالیت های مربوط به یک حوزه خاص کسب وکار است. این مدل باید:
- مفاهیم و روابط کلیدی حوزه را منعکس کند.
- به زبان کسب وکار (نه زبان فنی) بیان شود.
- به صورت اشیاء و عملیات پیاده سازی شود.
در این راهنمای سبک، مدل دامنه به جای اینکه فقط در مدلهای Django قرار گیرد، در لایه های خاص خود سازماندهی میشود.
مثال از منبع:
class Order:
def init (self, items, user):
self.items = items # Domain logic here
self.user = user
ب) ) Ubiquitous Language زبان همه جا حاضر(
یک زبان مشترک بین توسعه دهندگان و متخصصان حوزه که در تمام مراحل توسعه استفاده میشود. این زبان باید:
- در کد، اسناد و گفتگوها یکسان باشد.
- از اصطلاحات فنی غیرضروری پرهیز کند.
- مفاهیم حوزه را دقیقاً همانطور که متخصصان حوزه می فهمند بیان کند.
در این راهنمای سبک، نام کلاسها، متدها و ماژول ها باید از این زبان پیروی کنند.
استفاده از اصطلاحات کسب وکار در نام گذاری:
مثال: Order.is_eligible_for_discount() به جای Order.check_discount().
ج) Multiple Canonical models
در سیستم های بزرگ، یک مدل واحد برای کل سیستم ممکن است بسیار پیچیده شود. DDD پیشنهاد میکند از چندین مدل متعارف استفاده شود که هر کدام بخش خاصی از سیستم را با دیدگاه خاصی مدل سازی میکنند.
در این راهنمای سبک، این مفهوم با تقسیم سیستم به حوزه های مستقل (Bounded Contexts) پیاده سازی میشود.
مثال: مدل User در حوزه orders ممکن است با مدل User در حوزه payments متفاوت باشد.
د) Bounded Context (حوزه محدود)
مرز مشخصی که در آن یک مدل دامنه خاص معنا پیدا میکند و تعاریف دقیق دارد. ویژگی های کلیدی:
- هر حوزه محدود مسئولیت های مشخصی دارد.
- مدل های مختلف می توانند در حوزه های محدود مختلف تعاریف متفاوتی داشته باشند.
- ارتباط بین حوزه ها باید به دقت مدیریت شود.
در این راهنمای سبک، هر حوزه محدود به عنوان یک اپلیکیشن Django جداگانه پیاده سازی میشود.
درک و اعمال راهنمای سبک
این راهنمای سبک پیشنهاد میکند پروژه های Django را بر اساس اصول DDD سازماندهی کنیم.
اصول کلیدی در پیاده سازی:
1. جداسازی حوزه ها: هر حوزه کسب وکار (مانند سفارشات، محصولات) باید در ماژول خود قرار گیرد و وابستگی حداقلی به دیگر حوزه ها داشته باشد.
2. تمرکز بر : Domain Logic منطق کسب وکار باید در سرویس ها و مدل های دامنه قرار گیرد، نه در ویوها یا کنترلرها.
3. استفاده از الگویRepository : برای جداسازی لایه دسترسی به داده از لایه منطق کسب وکار.
4. تعریف واضح مرزها: ارتباط بین حوزه ها باید از طریق واسط های تعریف شده و واضح انجام شود.
5. تست پذیری: با جداسازی مناسب لایه ها، نوشتن تست های واحد آسان تر میشود.
مدلسازی معماری با نمودار های UML
نمودار کامپوننت برای معماری پیشنهادی
@startuml
package "Core" {
[Common Utilities]
[Shared Kernel]
}
package "Order Domain" as orders {
[Order Model]
[Order Service]
[Order Repository]
}
package "Product Domain" as products {
[Product Model]
[Product Service]
[Product Repository]
}
package "Interfaces" {
[API Controllers]
[Web Views]
}
[API Controllers] --> [Order Service] : uses
[API Controllers] --> [Product Service] : uses
[Web Views] --> [Order Service] : uses
[Web Views] --> [Product Service] : uses
[Order Service] --> [Order Repository] : depends on
[Order Repository] --> [Order Model] : manages
[Order Service] --> [Order Model] : operates on
[Product Service] --> [Product Repository] : depends on
[Product Repository] --> [Product Model] : manages
[Product Service] --> [Product Model] : operates on
[Order Service] --> [Shared Kernel] : uses
[Product Service] --> [Shared Kernel] : uses
@enduml
توضیح نمودار:
1. اینترفیس ها: شامل کنترلرهای API و ویوهای وب که به عنوان نقطه ورود سیستم عمل میکنند.
2. سرویس های حوزه: حاوی منطق کسب وکار و از طریق واسط های تعریف شده با لایه های دیگر ارتباط برقرار میکنند.
3. Repositoryها : مسئول دسترسی به داده ها و جداسازی لایه ذخیره سازی از منطق کسب وکار.
4. مدل ها: نمایش مفاهیم حوزه با استفاده از Ubiquitous Language.
5. هسته مشترک ( Shared Kernel ) : شامل قابلیت های مشترک بین چندین حوزه.
مدل Order:
from dataclasses import dataclass
from datetime import datetime
from typing import List
@dataclass
class OrderItem:
product_id: str
quantity: int
price: float
@dataclass
class Order:
id: str
user_id: str
items: List[OrderItem]
status: str = "pending"
created_at: datetime = datetime.now()
def total_price(self):
return sum(item.price * item.quantity for item in self.items)
def mark_as_paid(self):
if self.status != "pending":
raise ValueError("Order must be pending")
self.status = "paid"
سرویس Order با تزریق وابستگی:
class OrderService:
def init(self, order_repo, payment_gateway):
self.order_repo = order_repo
self.payment_gateway = payment_gateway
def place_order(self, user_id, items):
order = Order(str(uuid4()), user_id, items)
self.order_repo.save(order)
return order
def process_payment(self, order_id, payment_details):
order = self.order_repo.get(order_id)
self.payment_gateway.charge(order.total_price(), payment_details)
order.mark_as_paid()
self.order_repo.save(order)
تست پذیری سرویس Order :
from unittest.mock import Mock
def test_order_payment():
mock_repo = Mock()
mock_gateway = Mock()
service = OrderService(mock_repo, mock_gateway)
order = service.place_order("user1", [{"product_id": "1", "quantity": 2, "price": 10}])
service.process_payment(order.id, {"card": "1234"})
assert mock_gateway.charge.called
assert order.status == "paid"
نمودار توالی پرداخت:
@startuml
actor Customer
participant "Order API" as api
participant "Order Service" as service
participant "Payment Gateway" as gateway
participant "Order Repository" as repo
Customer -> api: POST /orders
api -> service: place_order()
service -> repo: save()
api -> Customer: 201 Created
Customer -> api: POST /payments
api -> service: process_payment()
service -> gateway: charge()
service -> repo: save()
api -> Customer: 200 OK
@enduml
نمودار UML بهینه شده (مطابق اصول طراحی نرم افزار)
@startuml
component "Order API" as api {
[OrderViewSet]
}
component "Order Domain" as domain {
[OrderService]
[OrderRepository]
[OrderModel]
}
component "Product Domain" as product {
[ProductService]
}
api --> domain : uses
domain --> product : depends (via interface)
note right of domain
Bounded Context:
- OrderModel encapsulates
domain logic
- Repository handles DB
end note
@enduml
مزایای این معماری:
1. قابلیت نگهداری: تغییر در یک حوزه تأثیر کمی بر حوزه های دیگر دارد.
2. قابلیت تست: هر جزء را میتوان به صورت مستقل تست کرد.
3. انعطاف پذیری: میتوان تکنولوژی های مختلف را برای حوزه های مختلف انتخاب کرد.
4. مقیاس پذیری: هر حوزه را می توان به صورت مستقل مقیاس داد.
5. شفافیت: ساختار کد منعکس کننده ساختار کسب وکار است.
مثال واقعی: سیستم فروشگاه آنلاین با معماری DDD در Django
در این مثال، یک سیستم فروشگاه آنلاین را با معماری DDD پیادهسازی میکنیم. حوزههای اصلی عبارتند از:
- سفارشات (Order)
- محصولات (Product)
- کاربران (User)
گام ۱: ساختار پروژه (Project Structure)
ساختار پیشنهادی مبتنی بر Bounded Contexts:
my_shop/
├── core/ # Shared Kernel
│ ├── exceptions.py
│ └── utils.py
├── domains/
│ ├── orders/ # Bounded Context: Order
│ │ ├── models.py # Domain Models
│ │ ├── services.py # Domain Services
│ │ ├── repositories.py # Repositories
│ │ └── events.py # Domain Events
│ ├── products/ # Bounded Context: Product
│ │ ├── models.py
│ │ └── services.py
│ └── users/ # Bounded Context: User
│ └── models.py
├── interfaces/
│ ├── api/ # API Layer
│ │ └── orders/
│ │ └── views.py
│ └── web/ # Web Layer (Optional)
└── infrastructure/
├── db/ # Database Adaptors
│ └── django_orm.py
└── event_handlers/ # Event Handlers
گام ۲: پیادهسازی مدل دامنه (Domain Models)
۱. مدل Order (در domains/orders/models.py)
from dataclasses import dataclass
from datetime import datetime
from typing import List
from domains.users.models import User
from domains.products.models import Product
@dataclass
class OrderItem:
product: Product
quantity: int
price: float
@dataclass
class Order:
id: str
user: User
items: List[OrderItem]
created_at: datetime = datetime.now()
def total_price(self) -> float:
return sum(item.price * item.quantity for item in self.items)
def add_item(self, product: Product, quantity: int, price: float):
self.items.append(OrderItem(product, quantity, price))
۲. مدل Product (در domains/products/models.py)
@dataclass
class Product:
id: str
name: str
price: float
stock: int
def reduce_stock(self, quantity: int):
if self.stock < quantity:
raise ValueError("Insufficient stock")
self.stock -= quantity
گام ۳: پیادهسازی سرویس دامنه (Domain Services) vices
۱. OrderService (درdomains/orders/services.py )
from domains.orders.models import Order
from domains.orders.repositories import OrderRepository
from domains.products.services import ProductService
class OrderService:
def init(self, order_repository: OrderRepository, product_service: ProductService):
self.order_repository = order_repository
self.product_service = product_service
def place_order(self, user, items: List[dict]) -> Order:
for item in items:
self.product_service.reduce_stock(item["product_id"], item["quantity"])
order = Order.create(user, items)
self.order_repository.save(order)
from domains.orders.events import OrderPlacedEven
OrderPlacedEvent(order.id).publish
return order
2. ProductService (در domains/products/services.py)
from domains.products.repositories import ProductRepository
class ProductService:
def init(self, product_repository: ProductRepository):
self.repository = product_repository
def reduce_stock(self, product_id: str, quantity: int):
product = self.repository.get_by_id(product_id)
product.reduce_stock(quantity)
self.repository.save(product)
گام ۴: پیادهسازی Repository (دسترسی به داده)
۱. OrderRepository (در domains/orders/repositories.py)
from domains.orders.models import Order
from infrastructure.db.django_orm import DjangoOrderModel
class OrderRepository:
def save(self, order: Order):
django_order = DjangoOrderModel.from_domain(order)
django_order.save()
def get_by_id(self, order_id: str) -> Order:
django_order = DjangoOrderModel.objects.get(id=order_id)
return django_order.to_domain()
۲. DjangoOrderModel (در infrastructure/db/django_orm.py )
from django.db import models
class DjangoOrderModel(models.Model):
id = models.UUIDField(primary_key=True)
user_id = models.UUIDField()
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField()
@classmethod
def from_domain(cls, order):
return cls)
id=order.id,
user_id=order.user.id,
total=order.total_price(),
created_at=order.created_at
(
def to_domain(self):
from domains.orders.models import Order
return Order)
id=self.id,
user=User(id=self.user_id),
items=[], # Load items separately
created_at=self.created_at
گام ۵: پیادهسازی API (Interface Layer)
۱. OrderViewSet (در interfaces/api/orders/views.py )
from rest_framework import viewsets
from domains.orders.services import OrderService
from domains.orders.repositories import OrderRepository
from domains.products.services import ProductService
class OrderViewSet(viewsets.ViewSet):
def create(self, request):
user = request.user
items = request.data.get("items", [])
order_service = OrderServic)
OrderRepository(),
ProductService(ProductRepository())
(
order = order_service.place_order(user, items)
return Response({"order_id": order.id}, status=201)
گام ۶: مدیریت رویدادهای دامنه (Domain Events)
۱. تعریف رویداد (در domains/orders/events.py )
from core.events import DomainEvent
class OrderPlacedEvent(DomainEvent):
def init(self, order_id: str):
self.order_id = order_id
def handle(self):
print(f"Order {self.order_id} placed!")
گام ۷: تستها (Testability)
۱. تست OrderService (در tests/orders/test_services.py )
from unittest.mock import Mock
from domains.orders.services import OrderService
def test_place_order():
repo = Mock()
product_service = Mock()
service = OrderService(repo, product_service)
service.place_order(user=None, items=[{"product_id": "1", "quantity": 2}])
assert repo.save.called
assert product_service.red
نتیجه گیری
اعمال اصول DDDدر Djangoنیاز به تغییر در شیوه سنتی توسعه دارد، اما مزایای آن در پروژه های متوسط و بزرگ کاملاً محسوس است. این راهنمای سبک با معرفی ساختار مبتنی بر حوزه های محدود، جداسازی مسئولیت ها و تعریف واسط های واضح ، به ایجاد سیستم های قابل نگهداری، انعطاف پذیر و متناسب با نیازهای کسب وکار کمک میکند. مدل سازی مناسب با ) UML مانند نمودارهای کامپوننت(به درک بهتر روابط بین اجزا و طراحی دقیق تر معماری کمک میکند.