برنامه نویسی

تزریق معماری و وابستگی لایه ای: دستور العمل برای کد FastAPI تمیز و قابل آزمایش

مقدمه

ساختن یک برنامه FastAPI که مقیاس دارد؟ این فقط مربوط به نقاط پایانی Async و مدل های Pydantic نیست. در این پست ، من نحوه ساخت برنامه های FastAPI را برای حفظ طولانی مدت با استفاده از معماری لایه بندی شده و تزریق وابستگی به اشتراک می گذارم. شما یاد می گیرید که چگونه کد خود را سازمان یافته نگه دارید ، کد قابل آزمایش بنویسید و یک سیستم انعطاف پذیر بسازید که با نیازهای شما می تواند تکامل یابد.

درک معماری لایه ای

معماری لایه بندی شده (همچنین به عنوان معماری N-stier نیز شناخته می شود) الگویی است که کد را در لایه های مجزا سازماندهی می کند ، هر کدام مسئولیت خاصی دارند. این جدایی نگرانی ها باعث می شود که پایگاه کد قابل حفظ ، آزمایش و انعطاف پذیر باشد. مانند یک آشپزخانه خوب سازمان یافته در یک رستوران به آن فکر کنید: هر ایستگاه نقش خاص خود را دارد و جریان کار از یک جهت حرکت می کند ، از آماده سازی تا آبکاری.

شرح تصویر

لایه 1: لایه ارائه

لایه ارائه درب جلوی برنامه شما است. اینجاست که Fastapi به همراه سایر نقاط ورود مانند کارهای کرفس و دستورات Typer زندگی می کند. این لایه مسئول رسیدگی به کلیه ارتباطات خارجی ، خواه درخواست HTTP ، مشاغل پس زمینه یا دستورات CLI است.

در اینجا یک مثال معمولی از نحوه نگاه این لایه در عمل آورده شده است:

@router.post("/foos")
async def create_foo(
    foo: FooCreateDTO,
    foo_service: FooService = Depends(get_foo_service)
) -> FooResponseDTO:
    return await foo_service.create_foo(foo)
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

خصوصیات کلیدی:

  • حداقل کد با تمرکز بر روی نقشه برداری ورودی/خروجی: لایه به عنوان مترجم بین قالب های خارجی (HTTP ، کرفس ، CLI) و فرمت استاندارد لایه خدمات شما عمل می کند ، و هر دو تغییر درخواست و قالب بندی پاسخ را انجام می دهد.

  • تزریق خدمات از طریق سیستم تزریق وابستگی Fastapi: این لایه تزریق وابستگی داخلی FastAPI را برای ارائه خدمات به وابستگی های مورد نیاز خود اعمال می کند و باعث می شود کد قابل آزمایش تر و حفظ شود.

  • الگوی مداوم در تمام نقاط ورود: این که آیا شما در حال رسیدگی به درخواست HTTP ، کار پس زمینه یا یک دستور CLI هستید ، همان الگوی را دنبال می کنید ، و باعث می شود کد قابل پیش بینی و آسان تر باشد.

لایه 2: لایه خدمات (تجارت)

اینجاست که جادو اتفاق می افتد. لایه سرویس قلب برنامه شما است که شامل کلیه منطق تجارت و مدیریت معاملات است. این مانند آشپزخانه در قیاس رستوران ما است – جایی که آشپزی واقعی (منطق کسب و کار) اتفاق می افتد.

بیایید ببینیم که چگونه این لایه را ساختار می دهیم:

class FooServiceInterface(ABC):
    @abstractmethod
    async def create_foo(self, foo: FooCreateDTO) -> FooResponseDTO:
        ...

class StandardFooService(FooServiceInterface):
    def __init__(self, foo_dao: FooDAOInterface, uow: UnitOfWorkInterface):
        self.foo_dao = foo_dao
        self.uow = uow

    async def create_foo(self, foo: FooCreateDTO) -> FooResponseDTO:
        async with self.uow:
            return await self.foo_dao.create(foo)
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ویژگی های کلیدی:

  • طراحی مبتنی بر رابط که امکان پیاده سازی های متعدد را فراهم می کند: با تعریف رابط های روشن ، ما به راحتی می توانیم بدون تأثیر بر بقیه سیستم ، پیاده سازی ها را عوض کنیم.

  • مدیریت معامله از طریق واحد الگوی کار: واحد الگوی کار تضمین می کند که تمام عملیات پایگاه داده در یک معامله یا با هم موفق شوند یا با هم شکست بخورند.

  • منطق تجاری متمرکز که پیدا کردن و اصلاح آن آسان است: کلیه قوانین و عملیات تجاری در لایه سرویس موجود است و باعث می شود آنها مکان یابی را آسان کنند.

  • کد خود مستند از طریق نام و مسئولیت های روش روشن: روش ها و کلاس های لایه سرویس نامگذاری شده است تا به وضوح هدف و مسئولیت های آنها را نشان دهد.

لایه سرویس جایی است که بیشتر وقت خود را صرف اجرای قوانین تجاری می کنید. همچنین در جایی است که شما عملیات پیچیده ای را انجام خواهید داد که ممکن است شامل چندین عملیات دسترسی به داده ها باشد ، و اطمینان حاصل کنید که همه آنها موفق هستند یا با هم شکست می خورند.

لایه 3: لایه پایداری (DAOS)

لایه پایداری حافظه برنامه شما است. این همه تعامل پایگاه داده را از طریق اشیاء دسترسی به داده ها (DAOS) انجام می دهد ، که مانند پیشخدمت های تخصصی در قیاس رستوران ما هستند – آنها دقیقاً می دانند که چگونه با سیستم ذخیره سازی ارتباط برقرار کنند.

در اینجا نحوه اجرای این لایه آورده شده است:

class FooDAOInterface(ABC):
    @abstractmethod
    async def create(self, foo: FooDTO) -> FooDTO:
        ...

class SQLAlchemyFooDAO(FooDAOInterface):
    def __init__(self, session: AsyncSession):
        self.session = session

    async def create(self, foo: FooDTO) -> FooDTO:
        db_foo = Foo(**foo.model_dump())
        self.session.add(db_foo)
        await self.session.flush()
        return FooDTO.model_validate(db_foo)
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

جنبه های کلیدی:

  • طراحی DAO مبتنی بر رابط: هر DAO رابط واضحی را اجرا می کند که قرارداد عملیات دسترسی به داده ها را مشخص می کند.

  • مدیریت معامله در سطح خدمات: لایه DAO هرگز تعهدات را انجام نمی دهد ، و مدیریت معاملات را برای کنترل بهتر به لایه خدمات واگذار می کند.

  • مبادله اجرای انعطاف پذیر: طراحی مبتنی بر رابط امکان جابجایی آسان بین ORM های مختلف یا راه حل های ذخیره سازی داده را بدون تأثیرگذاری بر لایه سرویس فراهم می کند.

DTOS: چسب بین لایه ها

DTO (اشیاء انتقال داده) پیام رسان بین لایه های شما هستند. آنها مانند فرم های سفارش استاندارد در رستوران ما هستند – آنها اطمینان می دهند که همه به همان زبان صحبت می کنند.

در اینجا چگونه DTO های خود را تعریف می کنیم:

from pydantic import BaseModel
from decimal import Decimal
from typing import List

class FooItemDTO(BaseModel):
    name: str
    quantity: int

class FooDTO(BaseModel):
    id: str
    name: str
    items: List[FooItemDTO]
    total: Decimal
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

خصوصیات:

  • انتقال داده های تغییر ناپذیر: DTO ها به عنوان مدلهای Pydantic منجمد اجرا می شوند و از هماهنگی داده ها در هنگام جریان بین لایه ها اطمینان می دهند.

  • بدون اعتبار سنجی با طراحی: DTOS به طور معمول منطق اعتبارسنجی را شامل نمی شود ، مگر اینکه به عنوان مدلهای ورودی FastAPI استفاده شود که در آن می توانیم روش های اعتبار سنجی خاص API را اضافه کنیم.

  • ارتباط لایه ای از نوع ایمن: DTO اطمینان از ایمنی نوع را با حرکت داده ها بین لایه ها ، باعث می شود که کد قابل حفظ تر و خطاهای زودرس باشد.

  • سریال سازی JSON آماده است: DTO ها به گونه ای طراحی شده اند که به راحتی از JSON قابل سریال باشند و آنها را برای پاسخ های API و ارتباطات خارجی مناسب می کند.

تزریق وابستگی با سرویس وابستگی

تزریق وابستگی مانند داشتن یک آشپزخانه خوب سازمان یافته است که در آن هر سرآشپز دقیقاً می داند چه ابزارهایی را نیاز دارند. سرویس وابستگی ما مدیر آشپزخانه است که تضمین می کند همه آنچه را که لازم دارند داشته باشند.

در اینجا نحوه اجرای آن آورده شده است:

class DependencyService:
    @staticmethod
    def get_foo_service(
        db: AsyncSession = Depends(get_db),
    ) -> FooServiceInterface:
        uow = SQLAUnitOfWork(db)
        return FooService(
            foo_dao=FooDAO(uow.db),
            foo_client=FooClient(),
            uow=uow,
        )
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

مزایا:

  • مونتاژ خدمات شفاف: سرویس وابستگی به وضوح نشان می دهد که هر سرویس به آن نیاز دارد و درک و اصلاح تنظیمات خدمات را آسان می کند.

  • مدیریت صریح وابستگی: همه وابستگی ها به وضوح اعلام شده و تزریق می شوند ، الزامات پنهان را از بین می برد و کد را قابل پیش بینی تر می کند.

  • معماری گسترده: سرویس وابستگی متمرکز باعث می شود نگرانی های متقاطع مانند حافظه پنهان ، پرچم های ویژگی یا ورود به سیستم بدون تغییر منطق کسب و کار را آسان کنید.

  • پیکربندی متمرکز: تمام مدیریت وابستگی در یک مکان اتفاق می افتد و حفظ و اصلاح ساختار برنامه را آسان تر می کند.

استراتژی تست

آزمایش در هر معماری بسیار مهم است و رویکرد لایه بندی شده ما آزمایش هر یک از مؤلفه ها را در انزوا آسان تر می کند. ما هرم تست کلاسیک را دنبال می کنیم:

شرح تصویر

تست های سیستم

تست های سیستم تأیید می کنند که کل برنامه شما با هم کار می کند. آنها مانند داشتن یک انتقاد غذایی از رستوران شما هستند:

class TestFooAPI:
    @pytest.mark.anyio
    async def test_get_foo(
        self,
        async_client: AsyncClient,
    ) -> None:
        # given
        payload = { bar: "bar" }
        response = await async_client.post("v1/foo/", json=payload)
        foo_id = response.json()["id"]

        # when
        response = await async_client.get(f"v1/foo/{foo_id}")

        # then
        assert response.status_code == 200
        assert response.json() == {
            "id": str(job.id),
            "bar": "bar",
        }
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

تست های ادغام

تست های ادغام تأیید می کنند که لایه های شما به درستی با هم کار می کنند. آنها مانند آزمایش هر ایستگاه در آشپزخانه هستند:

class TestSQLAFooDAO:
    @pytest.mark.anyio
    async def test_create(
        self,
        test_session: AsyncSession,
    ) -> None:
        # Given
        dao = SQLAlchemyFooDAO(test_session)
        foo_dto = FooDTO(bar="bar")

        # When
        result = await dao.create(foo_dto)

        # Then
        assert result.bar == "bar"
        assert result.created_at is not null
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

تست های واحد

تست های واحد اجزای جداگانه را در انزوا تأیید می کنند. آنها مانند آزمایش مهارت های هر سرآشپز هستند:

@pytest.mark.anyio
class TestFooService:
    async def test_foo_happy(self) -> None:
        # given
        foo_dao = AsyncMock(spec=FooDAOInterface)
        foo_client = AsyncMock(spec=FooClientInterface)
        foo_service = FooService(
            foo_dao=foo_dao,
            foo_client=foo_client
        )
        foo_id = "foo_id"
        foo_dao.get_one.return_value = FooDTO(bar="bar", id="foo_id")
        foo_client.get.return_value = FooDTO(bar="bar", id="foo_id")

        # when
        response = await foo_service.get_one(foo_id=foo_id)

        # then
        assert response == FooDTO(bar="bar", id="foo_id")
        foo_dao.get_one.assert_called_once_with(foo_id)
        foo_client.get.assert_called_once_with(foo_id)
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

معاملات و چالش ها

در حالی که معماری لایه ای فواید بسیاری را ارائه می دهد ، درک معاملات و چالش هایی که ممکن است هنگام اجرای این الگوی با آن روبرو باشید ، مهم است. این چالش ها نباید شما را از بین ببرد ، بلکه به شما کمک می کند تا در مورد اینکه آیا این معماری برای پروژه شما مناسب است ، تصمیم آگاهانه بگیرید.

چالش

  • توسعه اولیه بالای سر: معماری لایه بندی شده به کد دیگ بخار بیشتری نیاز دارد ، اما این سرمایه گذاری در قابلیت حفظ آن را پرداخت می کند. با این حال ، با قوانین مناسب مکان نما یا GitHub Copilot ، می توانید این سربار را به میزان قابل توجهی کاهش دهید زیرا این ابزارهای هوش مصنوعی می توانند کدی را تولید کنند که از الگوهای تعیین شده شما پیروی کند.

  • منحنی یادگیری برای توسعه دهندگان جدید: اعضای تیم برای درک الگوهای معماری و مفاهیم تزریق وابستگی به زمان نیاز دارند. با این حال ، اگر شرکت شما از این الگوی به طور مداوم در پروژه ها استفاده می کند ، توسعه دهندگان می توانند به سرعت در حال حرکت بین پروژه ها باشند ، زیرا الگوهای آشنا هستند.

  • مهندسی بیش از حد بالقوه: برای کاربردهای بسیار کوچک ، این معماری ممکن است پیچیدگی و سربار غیر ضروری را معرفی کند. به عنوان مثال ، اگر فقط یک مدل ML را از طریق یک نقطه انتهایی API در معرض دید قرار می دهید ، معماری کامل لایه بندی شده بیش از حد خواهد بود زیرا منطق کسب و کار حداقل برای مدیریت وجود دارد.

فواید

با وجود این چالش ها ، مزایای معماری لایه ای اغلب از سرمایه گذاری اولیه فراتر می رود. بیایید مزایای کلیدی را که این الگوی معماری را برای بسیاری از پروژه ها ارزشمند می کند ، بررسی کنیم.

  • قابلیت حفظ افزایش یافته: جدایی واضح نگرانی ها و الگوهای سازگار ، پایگاه کد را آسان تر و اصلاح می کند.

  • قابلیت آزمایش جامع: هر لایه را می توان در انزوا آزمایش کرد و نوشتن و نگهداری تست ها را آسان تر می کند.

  • انعطاف پذیری اجرای: طراحی مبتنی بر رابط امکان تعویض آسان پیاده سازی ها را بدون تأثیر سایر قسمت های سیستم فراهم می کند. این امر به ویژه برای اعتبار سنجی زودهنگام مفید است ، زیرا می توانید DAO های NULL را برای داشتن یک برنامه پایان کار کاملاً کاربردی بدون پایداری اجرا کنید ، به شما امکان می دهد قبل از تعهد به یک راه حل خاص پایگاه داده ، منطق کسب و کار را تأیید کنید.

  • الگوهای توسعه مداوم: معماری الگوهای مداوم را در قسمت پایگاه کد اعمال می کند و همکاری تیم ها را آسان تر می کند.

چه موقع از این معماری استفاده کنید

انتخاب معماری مناسب برای پروژه شما بسیار مهم است. در حالی که معماری لایه ای قدرتمند است ، اما یک راه حل یک اندازه مناسب نیست. در اینجا سناریوهایی وجود دارد که این الگوی معماری واقعاً می درخشد و می تواند بیشترین ارزش را برای پروژه شما فراهم کند.

  • برنامه های متمرکز بر دامنه: هنگامی که برنامه شما دارای یک دامنه واضح و کاملاً تعریف شده با قوانین پیچیده تجاری است که باید به طور مؤثر مدیریت شوند. برای کاربردهای چند دامنه یا یکپارچه های بزرگ ، معماری های دیگری که رابط های بین دامنه بهتری دارند (مانند معماری شش ضلعی) باید ترجیح داده شوند.

  • پروژه های در حال رشد: وقتی پیش بینی می کنید برنامه با گذشت زمان رشد می کند و به ساختاری نیاز دارد که بتواند با آن مقیاس کند.

  • منطق تجارت پیچیده: هنگامی که درخواست شما برای مدیریت قوانین و فرآیندهای پیچیده تجارت نیاز به تفکیک واضح نگرانی ها دارد.

  • چندین نقطه ورود: هنگامی که درخواست شما نیاز به رسیدگی به انواع مختلف درخواست ها (API ، CLI ، مشاغل پس زمینه) ضمن حفظ منطق تجاری مداوم دارد.

  • نگهداری طولانی مدت: هنگامی که به یک پایگاه کد نیاز دارید که در طی یک دوره طولانی حفظ و تمدید شود.

  • توسعه با کیفیت: هنگامی که به ساختاری نیاز دارید که از آزمایش جامع و تضمین کیفیت پشتیبانی کند.

پایان

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

می توانید تمام نمونه های کد را در مخزن GitHub ما پیدا کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا