در مقاله قبل با بخش های مختلف Clean Architecture بصورت تفکیک شده آشنا شدیم. در این مقاله سعی میکنم یک پروژه بر این اساس ایجاد کنم. دوباره این تصویر را ببینیم و در طول مقاله به آن مراجعه کنید. فرض کنید میخواهیم یک برنامه مدیریت محتوا تولید کنیم. این برنامه تحت وب است. پروژه در قابل دسترسی است. از دات نت کور برای تولید این برنامه استفاده میکنیم ولی مفاهیم تقریبا یکسانی برای ایجاد یک معماری Clean در ابزارهای مختلف توسعه نرم افزار وجود دارد پس اگر با جاوا یا پایتون یا نود جی اس یا زبان های دیگر کار می‌کنید پیشنهاد نمی‌کنم که بقیه مطلب را رها کنید! از MsSql برای ذخیره و بازیابی داده های این برنامه استفاده میکنیم. از EF Core بعنوان ORM به منظور دسترسی به اطلاعات استفاده خواهیم کرد. این وب سایت قرار است بتواند در های مختلف نمایش دهد و را فراهم کند. به منظور سادگی روند توضیح و تمرکز روی معماری، قرار نیست این سایت داده های مربوط به کاربران را نگهداری کند، پس فرض ما این است که لاگین و ثبت نامی وجود ندارد. سه تیبل برای اینکار باید ایجاد کنیم. نوشته ها در تیبل Posts نگهداری میشوند. دسته بندی ها در تیبل Categories نگهداری میشوند. نظرات در تیبل Comments نگهداری میشوند. پس نیاز به ایجاد سه مدل برای این سه جدول داریم. دامین ما نیز بر اساس همین سه جدول ایجاد میشوند. برای شروع کار باید هسته مرکزی معماری را ایجاد کنیم. اسم سولوشن خود را Cms یا Content Management System میگذاریم. یک پروژه در سولوشن ویژوال استودیو به نام Core ایجاد میکنیم که درون آن لایه مرکزی Domain وجود دارد. میتوانیم لایه دامین را به شکل یک پروژه جدا ایجاد کنیم ولی الزامی برای اینکار وجود ندارد، چون دسترسی هر لایه به Domain باید امکانپذیر باشد و این دسترسی اگر از مسیر Core بگذرد ساختار Clean به هم نمی‌ریزد. در ساختار دامین کلاس ها را به شکل زیر در فولدر دامین ایجاد میکنیم. شروع کار از بخش دامین باعث میشود بیشتر روی بیزنس پروژه فکر کنیم و ایرادات احتمالی را زودتر متوجه شویم. بخاطر بسپرید که دامین میتواند حاوی لاجیک هم باشد. یک فولدر به نام Services ایجاد میکنیم که حاوی اینترفیس ها و سرویس هاست. مثلا اینترفیس PostService را ایجاد می‌کنیم که قرار است پیاده سازی های آن با دامین Post کار کند. (الزامی به اینکه یک سرویس فقط با یک دامین یا مدل کار کند وجود ندارد) چیزی که سرویس ها برمیگردانند یا از جنس دامین هستند یا Dto . مثلا فرض کنید سرویسی داریم که قرار است پست ها را با تاریخ شمسی ثبت آن پست و به همراه مدت زمانی که صرف کویری گرفتن شد برگرداند. در اینجا باید از یک Dto یا Viewmodel استفاده کنیم. در الگوی MVC گاها از مفهوم Dto و ViewModel به جای هماستفاده میکنند اما تفاوت هایی وجود دارد. در پیاده سازی Clean Architecture میتوانیم با هم قراردادی بگذاریم که آنچه بین لایه ها رد و بدل میشود را Dto بنامیم و آنچه از سمت پرزنتیشن می آید یا به سمت آن میرود را ویومدل بگوییم. (یعنی در تعریفی "من‌در‌آوردی"، مدلی که مورد استفاده View است را ویومدل نامگذاری می‌کنیم. کلاس پیاده سازی این سرویس را هم مینویسیم. فعلا دیتایی نداریم که برگردانیم و صرفا یک نمونه از تایپ خروجی میسازیم تا پروژه خطا ایجاد نکند. حالا لایه مربوط به api را میسازیم که مستقیما با UI در تماس است و متدهای GET , POST و. در آن پیاده سازی شده اند. یک پروژه ASP . NET Core Web Application درست میکنیم و نوع آن را API انتخاب میکنیم. کنترلر Post را به شکل زیر برای تست درست میکنیم. برای اینکه متغیر result مقدار داشته باشد، یک آبجکت تستی در سرویس مینویسیم. با اجرای پروژه و صدا زدن متد Get خطایی به شکل زیر میبینیم متن خطا میگوید که در کانستراکتور کنترلر اینترفیسی وجود دارد که پروژه وب ما نمیداند با چه سرویسی آن را Resolve کند. یعنی new کردن کلاس پیاده سازی اینترفیس به شکل خودکار انجام نشده است، دلیل این موضوع این است که ما تنظیمات دپندنسی اینجکشن را انجام ندادیم. یعنی نگفتیم IPostService با چه کلاسی باید جایگزین یا Resolve شود. اینکار را در فایل startup به شکل زیر انجام میدهیم. با اجرای پروژه خروجی به شکل زیر خواهد بود. قبل از پیاده سازی بقیه api ها بهتر است لایه infrastructure یا database یا gateway را بسازیم. این لایه یک لایه است و با دیتابیس در ارتباط است. قرار است لایه Core از وجود این لایه یا هر لایه دیگر بی‌خبر باشد. دو فولدر Entity و Repository را میسازیم. ریپازتوری در لایه infrastructue قرار دارد. یعنی عملیات روی دیتابیس از طریق همین ریپازیتوری انجام میشود. اگر قرار باشد لایه Core از ریپازیتوری استفاده کند، باید این لایه را ببینید که خلاف روند Clean Architecture است. راه حل این است که کلاس های Repository خودشان در لایه infra باشند ولی اینترفیس آنها در لایه Core قرار گیرد. برای سرویس های Core همین کافیست که با اینترفیس ها کار کنند. (بر اساس Dependency Injection ) ما میتوانیم این اینترفیس ها را با کلاس های متناظرشان در لایه Infra به سادگی resolve کنیم بدون آنکه لایه Core خبر داشته باشد! پس لایه Infrastructure به جز Entity های مربوط به دیتابیس و مایگریشن، حاوی پیاده سازی ریپازیتوری ها نیز هست. در این لایه ابتدا Entity های خود را - که معمولا شبیه دامین است - ایجاد میکنیم و به کمک Fluent Api کانفیگ می کنیم و با ایجاد DbContext و معرفی کانکشن استرینگ مایگریشن را انجام میدهیم. فرض می‌کنیم می‌خواهیم برای جدول Post دو وظیفه یا رفتار در ریپازیتوری تعریف کنیم: اطلاعات یک post را برگرداند (همه ی ریلیشن ها هم بدهد) اطلاعات یک post را برگرداند (بدون اینکه ریلیشن ها را بدهد) دو متد فوق را ابتدا در IPostRepository در لایه Core تعریف میکنیم و سپس پیاده سازی آنها را در لایه Infrastructure انجام میدهیم. به پیاده سازی ها دقت کنید. هدف در پیاده سازی ها صرفا آموزش است همچنین در اینجا مپ کردن Entity به روی Domain به شکل دستی انجام شده که پیشنهاد میکنم از استفاده کنید. حالا کافیست به سرویس برویم و از ریپازیتوری استفاده کنیم. و در نهایت api خود را بنویسیم. فراموش نکنید که ریپازیتوری ها و کانتکست هم مثل سرویس باید در startup معرفی شوند. چون به شکل اینجکت در کانستراکتور ها از آن استفاده شده و فریم ورک نمی‌داند چطور آنها را ارضا (کلمه مناسب تر پیدا نکردم) کند. اما لایه پرزنتر ( presenter ) کجاست؟ به سادگی میتوان یک پرزنتر یکسان اختصاصی برای خروجی با فرمت جیسون در BaseController ساخت. اما BaseController کجاست؟ یک کلاس به نام BaseController میسازیم و از این به بعد کنترلرهایمان را از این کلاس ارث بری میکنیم. در این کلاس متدهایی را مینویسیم که قرار است در دسترس همه اکشن های درون کنترل ما قرار گیرند. این کلاس میتواند یک ایده برای ایجاد پرزنترهای پیچیده تر باشد. میتوان اکشن ها را به شکل زیر نوشت. دو متد CustomOk و CustomError را ما در BaseController خودمان ساختیم که فرمت یکسان در خروجی ها به UI بدهیم. برای تست، ItemNotFoundException هم یک خطای کاستوم است که خودمان درست کردیم. از این اکسپشن، در سرویس مربوط برای هندل کردن خطا استفاده کردیم و نهایتا م چ آن در اکشن گرفته میشود (به دلیل وجود try / catch در مرحله آخر) این ساده ترین راه برای کنترل خطاهاست. خروجی سالم: خروجی ناسالم: اما نکته آخر در مورد Infrastructure که فعلا فقط دیتا را از دیتابیس گرفته وتوسط ریپازیتوری به لایه Core می‌دهد. این لایه چه کارهای دیگری میتواند انجام دهد؟ مثال اول: فرض کنید بخواهیم یک سرویسی از بیرون بگیریم. مثلا دمای هوا. باید در همین Infrastructure اینکار را انجام دهیم و اینترفیس آن را در لایه Core قرار دهیم. با این روش در معماری Clean Architecture ساختار داده ای ما کاملا جدا از Core خواهد بود و لایه Core ما صرفا دستور دریافت اطلاعات را به لایه ی Infra ارسال می‌کند، بدون آنکه اطلاعی از نحوه فراهم آمدن دیتا داشته باشد. (فراهم آوردن دیتا از دیتابیس یا فایل یا وب یا . برای لایه Core هیچ اهمیتی ندارد) مثال دوم: فرض کنید می‌خواهید یک رمز تولید کنید و برای اینکار نیاز به salt دارید یا مشخصه ای منحصر به فرد از ماشین سرور فعلی خود را میخواهید که رمزی قوی تولید کنید. با اینکه مولد رمز بیزنس موجود در لایه Core است، ولی آن مشخصه منحصر به فردی که برای تولید رمز نیاز است از لایه Infra تامین میشود. پس برای اینکار هم باید اینترفیسی در Core وجود داشته باشد که پیاده سازی آن در لایه Infra انجام شود. امیدوارم با این دو مثال لایه Infrastructure برای شما هویت معنی دار و مشخص تری پیدا کرده باشد. به ساختار سولوشن نگاه کنید. پروژه Core که در دل خود Domain را دارد مرکز لایه هاست و بقیه پروژه ها از آن استفاده میکنند. این موضوع مفهوم کلیدی Clean Architecture است که رعایت شده است. این پروژه را به شکل کامل در بینید. چند نکته: این روش یکی از ساده ترین و کاربردی ترین روش ها برای پروژه های پیچیده است. اگر فکر می‌کنید ریپازیتوری جنریک به شما کمکی نمیکند و گیجتان می‌کند، درگیر آن نشوید. با اینکه برای انجام CRUD (که بیشتر پنل های ادمین به آن نیاز دارند) چشم پوشی از ریپازیتوری جنریک کمی شما را به زحمت می اندازد. بطور کلی چیزهایی به Clean Architecture اضافه کنید که باری را از روی دوش شما بر می‌دارد. الکی پروژه تمپلیت خود را شاخ و برگ ندهید. سرویس ها دو وظیفه مهم دارند. این آماده سازی فقط بر روی داده انجام میشود، یعنی چگونگی نمایش (اینکه جیسون باشد یا Xml و در قالب یک فرمت یکسان داده ای حاوی data , message , status و . ربطی به سرویس ندارد و باید در لایه presenter انجام شود) اینکه داده ای از ورودی گرفته شود و به شکل دلخواه تغییر کند و با فلان داده که از طریق ریپازیتوری از دیتابیس گرفته شده ترکیب شود و نتیجه در جایی ذخیره شود همه منطق برنامه نویسی است که باید در سرویس ها پیاده سازی شود. مفهوم لایه را با پروژه های درون سولوشن قاطی نکنید. تفکیک dll ها به منظور ساده سازی آپدیت ها و یا احیانا حضور آنها در مکان های مختلف و دسترسی ها و. انجام میشود. شما میتوانید معماری Clean را در یک پروژه و با فولدر بندی صحیح انجام دهید. در همین مقاله هم دیدیم که لایه Domain درون پروژه Core بود یا لایه Presenter درون پروژه Api وجود داشت. در لایه Core برای مپ کردن Dto (ویا ViewModel ها) با Domain میتوانید از Automapper استفاده کنید. در لایه Infra برای مپ کردن Entity های مربوط به دیتابیس با Domain میتوانید از Automapper استفاده کنید. (بهتر است) چیزی که لایه Infra به لایه Core میدهد باید از نوع Domain و در برخی موارد Dto باشد ( Entity نباید باشد چون لایه Core کلاس های موجود در Infra را نمی شناسد) همچنین ورودی به لایه Infra از سمت Core حتما Domain یا Dto است. (چون لایه Infra میتواند Core را ببینید و این کلاس ها در لایه Core وجود دارند) چیزی که لایه Core به لایه Api میدهد Dto یا ViewModel است و چیزی که لایه Api به Core میدهد نیز Dto یا ViewModel است. دقت شود که ViewModel ها و Dto ها و Domain در لایه Core قرار دارند. دقت شود که Entity های مربوط به جداول دیتابیس در لایه Infrastructure قرار دارند. این ها همان مدل هایی هستند که Migration درگیر آن است. بهتر است اکسپشن های برنامه توسط یک میدل ویر ( middleware ) هندل شوند و خروجی خطا با فرمت مشخص و status code مشخص ارسال شود. بهتر است اکسپشن های ما به شکل custom در لایه Core وجود داشته باشند. در این مثال تنها یک مورد را به منظور آموزش ایجاد کردیم. فایل startup چون باید از EF و Context موجود در لایه Infra استفاده کند، مجبور است به لایه Infra دسترسی داشته باشد. یعنی لایه api به لایه Infra دسترسی دارد. این به معنی ارتباط لایه api یا پرزنتیشن با دیتابیس نیست و نباید در کنترلر ها مستقیما از Infra استفاده کرد چون طبق Clean Architecture قرار است لایه پرزنتر به Core دسترسی داشته باشد و کسی مستقیما از Infra استفاده نکند. در ذهن خود فایل های startup و program را خارج از چارچوب معماری و به عنوان راه اندازی کل پروژه ها در نظر بگیرید. اگر روی ظاهر معماری حساسیت دارید (با اینکه حساسیت شما بی‌منطق است) می‌توانید دسترسی startup را به Infra قطع کنید. یک پروژه مشترک مثلا به نام Common بسازید که همه لایه ها بتوانند آن را در خود داشته باشند (رفرنس دهند) استارتاپ را به آن لایه منتقل کنید. حالا لایه api از آن استفاده میکند و ارتباط آن لایه با infra و دیگر لایه ها منفک از معماری ما خواهد بود. اینکار یک روش دلخواه است و معماری کلین نه این مورد را سفارش کرده و نه با انجام ندادن آن چیزی نقض میشود. برای ساده سازی در این مثال، Repository بدون حضور UnitOfWork انجام شد و چون از EF Core استفاده می‌کنیم، بر اساس آنچه در مقالات قبل گفتیم لزومی به پیاده سازی ریپازیتوری همراه UOW نیست ولی در پروژه های خاص پیاده سازی اینها لازم است. امیدوارم این مقاله به درد شما خورده باشد. در نوشته ی بعدی سعی میکنم همین پروژه را با استفاده از UseCase ها توسعه دهم. UseCase ها میتوانند در مواردی، جایگزین سرویس ها باشند.