قسمت اول : مقدمات
قسمت دوم : مبانی معماری
قسمت سوم : نگاهی به معماری سنتی سه لایه
قسمت چهارم : اجزای Clean Architecture
قسمت پنجم : پیاده سازی بر اساس سرویس ها
قسمت ششم : پیاده سازی بر اساس UseCase ها (شما در حال خواندن این مقاله هستید)
قسمت هفتم : آشنایی با CQRS
پیاده سازی معماری Cleanرا به کمک سرویس ها دیدیم. تصویر معروف این معماری را به خاطر بیاورید.
پیام اصلی این معماری این است :
لایه های درونی هیچ اطلاعی از لایه های بیرونی ندارند و این ویژگی را به کمک Dependency Injection به راحتی میتوان پیاده کرد.
در بخش قبل دیدم که لایه مرکزی یعنی Core که حاوی Doman (یا همان Entity ها) بود هیچ اطلاعی از اینکه چطور دارد استفاده میشود نداشت. سرویس های موجود در این لایه فقط ارائه دهنده خدماتی بودند که ورود و خروجشان را نمیدانستند.
آنچه Uncle Bob's به عنوان Clean Architecture در لایه Core معرفی کرده شامل UseCase ها و Entityهاست(که ما به Domain میشناسیم؛ به شکل قراردادی مدل های دیتابیسی را Entity میگوییم).
اول ببینیم سرویسها معمولا در ساختاری که در مقاله قبل تعریف شد به مرور بزرگ و بزرگ تر میشوند. اینجکشن های درون یک سرویس زیاد شده و گاهی مدیریت آنها سخت میشود.
یوزکیس(UseCase) یا Interactor یک جایگزین برای سرویس نیست بلکه مرکز اصلی پیاده سازی لاجیک محسوب میشود. ما یک پروژه را برای یک هدف دنبال میکنیم و قرار است نرم افزار ما مسائل دنیای واقعی را در راستای این هدف حل کند. هر مسئله را میتوان یک UseCase دانست با ورودی مشخص و خروجی قابل محاسبه. با این دیدگاه یک سرویس که حاوی متدهای مختلف برای برآورده کردن نیازهاست به چندین کلاس UseCase شکسته میشود.
مثلا یک UseCase وظیفه دارد آبجکت Post را به جدول Post اضافه کند. در این مسیر باید ابتدا بسنجد که آیا این آبجکت از قبل وجود داشته یا نه اگر وجود داشت اکسپشن مناسب تولید کند و اگر نداشت ایجادش کند.
ما قبلا همین منطق را مثلا در متدی به نام Add در سرویس PostService ایجاد میکردیم ولی در روش فعلی باید یک UseCase برای اینکار ایجاد کنیم که کاملا ایزوله باشد. حتا ترجیح این است که هندل کننده ی این روال هیچ نوع ریترنی نداشته باشد و از ورودی، ظرفی را دریافت کند و آن ظرف را پُر کند.
میخواهیم api برای add کردن یک Post ایجاد کنیم. اینبار از روش UseCaseی جلو میرویم.
اما چطور UseCase بنویسیم؟
باید یک سری محدودیت ها و قواعدی برای نوشتن UseCase طراحی کنیم که برنامه نویس ملزم به رعایت آن باشد. مثلا اینکه ورودی چگونه باشد و خروجی به چه شکل تولید شود.
اینترفیس زیر را ایجاد میکنیم.
این اینترفیس جنریک است و دو تایپ به عنوان TUseCaseRequest و TUseCaseResponse در آن استفاده میشود که ارتباط بین این دو تایپ نیز توسط IUseCaseRequest تعریف شده است.
این اینترفیس هم جنریک است و تایپ جنریکی که با آن کار میکند به شکل out معرفی شده است. Out حالتی است که پارامتر حتما باید توسط متد تغییر کند. این یعنی چه؟
همانطور که متدهای موجود در سرویسها ورودی و خروجی دارند، این موضوع برای UseCase هم صادق است. هر UseCase را میتوان معادل یک متد در سرویس دانست. اگر ورودی UseCase را به عنوان Request و خروجی آن را Response در نظر بگیریم این جریان دیتایی را میتوان یک نوع ارتباط مسیجی تعریف کرد.
میخواهیم یک api برای Add کردن یک Post بنویسیم. این Api یک UseCase به نام AddPostUseCase را صدا میزند. ورودی این UseCase مدلی به نام AddPostRequest است که مشخصات زیر را دارد:
همانطور که میبینیم از IUseCaseRequest ارث بری کرده و به دلیل خاصیت این اینترفیس مجبور است یک مدل به عنوان Response نیز تعریف کنیم. فرض کنید AddPostResponse مدل زیر است.
تا اینجای کار روند ورودی و خروجی دیتا را تعریف کردیم.
برای نوشتن متد UseCas کلاسی به نام AddPostUseCase تعریف میکنیم که باید IAddPostUseCase را پیاده سازی کند. این اینترفیس را به شکل زیر تعریف میکنیم
این اینترفیس از IUseCaseRequestHandler ارث بری میکند و مشخص است که به جای in و out چه مدل هایی باید قرار گیرد. کلاس پیاده سازی این اینترفیس با مدل ورودی و خروجی که پیش از این تعریف کردیم کار میکند پس به شکل فوق تعریف شده است.
بر اساس آنچه در IUseCaseRequestHandler تعریف شده کلاس هایی که هر اینترفیس ارث بری شده از این اینترفیس را پیاده کنند مجبورند متد HandleAsync را داشته باشند که مدلی به نام message را از نوع Request تحویل گرفته و چیزی از تایپ IOutputPort را که جنریکی از تایپ Response است میسازند!
از عبارت میسازند به جای تحویل میدهد استفاده کردم چون UseCase ها مثل Service ها ریترن به درد بخور ندارند؛ یعنی نتیجه مجموعه لاجیکی که روی مقادیر ورودی اعمال میشود در ظرفی به نام outputPort ریخته میشود.
این ظرف از کجا می آید؟ کدام متد از کدام کلاس قرار است IOutputPort را پیاده کند که ما Handle آن را صدا بزنیم؟
اینجاست که Presenter وارد عمل میشود. برخلاف حالت سرویسی که در مقاله قبل توضیح دادیم، ظهور Presenter در روش فعلی بسیار واضح و موثر است. ما میتوانیم یک کلاس پرزنتر به نام PostApiPresenter تعریف کنیم که جنریک باشد و آبجکت خروجیِ یوزکیس را بگیرد و هر بلایی خواست سرآن بیاورد تا برای نمایش آماده شود. این کلاس وظیفه ساختار دهی به خروجی را نیز دارد و میتوانیم همانکاری را که قبلا در BaseController انجام دادیم در اینجا انجام دهیم.
این کلاس به شکل زیر است:
پرزنتر ما در اینجا وظیفه جیسون کردن خروجی را بر عهده دارد و از IOutputPort باید ارث بری کند، چون آبجکتی از پرزنتر به عنوان پارامتر دوم هر UseCase به آن ارسال میشود تا response را بگیرد. به پارامتر ورودی دوم در UseCase دقت کنید.
کد زیر یک UseCase برای add کردن یک post جدید را نشان میدهد. آرگومان دوم چیزی از جنس IOutputPort است. ظرفی که قرار است توسط این UseCase با خروجی مناسب پر شود. پرزنتر همین ظرف است.
میتوان پرزنتر را به شکل عمومی تر نیز تعریف کرد. پرزنتری که ما ساختیم با توجه به نام آن، در سطح یک کنترلر قابل استفاده است. مثلا برای کنترلر Post بر اساس خروجیهای مختلفی که در اکشن هایش داریم، این پرزنتر میتواند استفاده شود، چون جنریک است.
مثلا اگر متد HTTPPOST مینویسیم و خروجی ما قرار است از جنس AddPostResponse باشد کافیست که PostApiPresenter<AddPostResponse> را به کنترلر اینجکت و از آن به شکل زیر استفاده کنیم.
دو پراپرتی addPostUseCase_ و addApiPresenter_ مقادیر اینجکت شده در کنترلر هستند که در این اکشن استفاده شده اند.
چند نکته :
کمی پرزنتر را مجهز می کنیم. ابتدا مدل یکسان خروجی ها را میسازیم. این کلاس را در لایه Coreایجاد میکنیم.
این مدل حاوی data و خطا نیست. یعنی خروجی یک api اگر حاوی خطا یا دیتا باشد این مدل به تنهایی کافی نیست. باید یک کلاس جدید از این مدل ارث بری کنیم که جنریک باشد و دیتای خروجی یا خطا را نیز شامل شود. این کلاس و کلاس Error ی که در آن استفاده شده را نیز در لایه Core ایجاد میکنیم.
سه نوع پیاده سازی کانستراکتور تعریف کرده ایم:
حالا این کلاس را در پرزنتر استفاده میکنیم.
با این تغییر اکشن ما به شکل زیر دچار خطا میشود
علت خطا تفاوت جنس پرزنتر و آرگومان دوم UseCase است.
پرزنتر از جنس IOutputPort<GenericResponse<T>> است. پس مجبوریم اینترفیس یوزکیس را اصلاح کنیم و تایپ خروجی GenericResponse را اضافه کنیم.
خطای دیگری در اولین تایپ میبینیم. AddPostRequest هم به شکل زیر باید اصلاح شود.
مشکل HandleAsync در کنترلر برطرف میشود.
حالا به پیاده سازی بیزنس در UseCase میپردازیم.
کافیست متد Add را در ریپازیتوری درست کنیم. این متد ورودی از نوع دامین میگیرد و خروجی آن آبجکت کاملی از نوع دامین است. (که id آن پس از اضافه شدن به دیتابیس مشخص شده)
حالا addedItem برای یوزکیس یک آبجکت دامینی حساب میشود و باید آن را به خروجی مورد نظرش map کند. اینکار را به شکل دستی در مثال فوق انجام دادیم تا outputObject را بسازیم.
در مرحله آخر ouputObject را به handle میدهیم که به اصطلاح "ظرف پرزنتر را پر کند" و در سمت اکشن ContentResult داشته باشیم.
چند نکته :
https://gist.github.com/e19efc4a8b49091951d087f630254909
خب ؛ حالا که ساختار مشخص شد برای نوشتن یوزکیس جدید Delete به شکل زیر عمل میکنیم.
تفاوت این یوزکیس با یوزکیس قبل فقط در هندل کردن خطاهاست.
چند نکته در مورد روند کار DeleteUseCase
در این مقاله ساختار یک پروژه بر اساس Clean Architecture معرفی شد. این پروژه را میتوانید در اینجا ببینید.
الان نوبت شماست که توانایی های خودتون رو نشون بدید. اکستنشن متد بنویسید، میدل ویرهای مختلف تعریف کنید. کاری کنید که با کمترین زحمت بیشترین نتیجه را بگیرید. چه کارهایی بلدید؟
فرصت بررسی همه موارد فوق نیست و امیدوارم در آینده ای نزدیک به شکل ویدیویی موارد فوق را با هم پیاده سازی کنیم. اما فعلا منتظر مقاله بعدی باشید که با کتابخانه ای آشنا خواهیم شد که استفاده از UseCase را ساده تر میکند.