ویرگول
ورودثبت نام
زهرا بهرامیان
زهرا بهرامیان
زهرا بهرامیان
زهرا بهرامیان
خواندن ۹ دقیقه·۱ سال پیش

DDD در Django

راهنمای سبک اعمال 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 مانند نمودارهای کامپوننت(به درک بهتر روابط بین اجزا و طراحی دقیق تر معماری کمک میکند.

کسب وکار
۳
۱
زهرا بهرامیان
زهرا بهرامیان
شاید از این پست‌ها خوشتان بیاید