آشنایی با Clean Architecture : پیاده سازی بر اساس سرویس ها (قسمت پنجم )

قسمت اول : مقدماتقسمت دوم : مبانی معماریقسمت سوم : نگاهی به معماری سنتی سه لایهقسمت چهارم : اجزای Clean Architecture قسمت پنجم : پیاده سازی بر اساس سرویس ها (شما در حال خواندن این مقاله هستید)قسمت ششم : پیاده سازی بر اساس UseCase هاقسمت هفتم : آشنایی با CQRSدر مقاله قبل با بخش های مختلف Clean Architecture بصورت تفکیک شده آشنا شدیم. در این مقاله سعی میکنم یک پروژه بر این اساس ایجاد کنم.دوباره این تصویر را ببینیم و در طول مقاله به آن مراجعه کنید.لایه مرکزی Entities یا Domain است و مربوط به مدل هایست که بیزنس در گیر آن است( و نه الزاما مدل های دیتابیس) لایه بیرونی UseCase هاست که در این مقاله با جایگزین ساده تری به نام سرویس ها توصیف شده اند.فرض کنید میخواهیم یک برنامه مدیریت محتوا تولید کنیم. این برنامه تحت وب است. پروژه در اینجا قابل دسترسی است.از دات نت کور برای تولید این برنامه استفاده میکنیم ولی مفاهیم تقریبا یکسانی برای ایجاد یک معماری Clean در ابزارهای مختلف توسعه نرم افزار وجود دارد؛ پس اگر با جاوا یا پایتون یا نود جی اس یا زبان های دیگر کار می‌کنید پیشنهاد نمی‌کنم که بقیه مطلب را رها کنید!از MsSql برای ذخیره و بازیابی داده های این برنامه استفاده میکنیم.از EF Core بعنوان ORM به منظور دسترسی به اطلاعات استفاده خواهیم کرد.این وب سایت قرار است بتواند محتواهایی در دسته بندی های مختلف نمایش دهد و امکان نظردهی افراد را فراهم کند.به منظور سادگی روندِ توضیح و تمرکز روی معماری، قرار نیست این سایت داده های مربوط به کاربران را نگهداری کند، پس فرض ما این است که لاگین و ثبت نامی وجود ندارد.سه تیبل برای اینکار باید ایجاد کنیم.نوشته ها در تیبل Posts نگهداری میشوند.دسته بندی ها در تیبل Categories نگهداری میشوند.نظرات در تیبل Comments نگهداری میشوند.پس نیاز به ایجاد سه مدل برای این سه جدول داریم. دامین ما نیز بر اساس همین سه جدول ایجاد میشوند.برای شروع کار باید هسته مرکزی معماری را ایجاد کنیم.اسم سولوشن خود را Cms یا Content Management System میگذاریم.یک پروژه در سولوشن ویژوال استودیو به نام Core ایجاد میکنیم که درون آن لایه مرکزی Domain وجود دارد.میتوانیم لایه دامین را به شکل یک پروژه جدا ایجاد کنیم ولی الزامی برای اینکار وجود ندارد، چون دسترسی هر لایه به Domain باید امکانپذیر باشد و این دسترسی اگر از مسیر Core بگذرد ساختار Clean به هم نمی‌ریزد.در ساختار دامین کلاس ها را به شکل زیر در فولدر دامین ایجاد میکنیم.مدل هایی که قرار است با آن کار کنیم.شروع کار از بخش دامین باعث میشود بیشتر روی بیزنس پروژه فکر کنیم و ایرادات احتمالی را زودتر متوجه شویم.بخاطر بسپرید که دامین میتواند حاوی لاجیک هم باشد. در این سلسله مقالات چون تمرکز روی ساختار Clean است از پیچیده کردن موضوع پرهیز کرده ام و این پروژه قابل مقایسه با پروژه هایی که ماه ها روی معماری آن کار شده نیست.یک فولدر به نام Services ایجاد میکنیم که حاوی اینترفیس ها و سرویس هاست.سرویس ها حاوی منطق(لاجیک) برنامه نویسی هستند و متدها وظیفه های خاص به عهده دارند.مثلا اینترفیس PostService را ایجاد می‌کنیم که قرار است پیاده سازی های آن با دامین Post کار کند. (الزامی به اینکه یک سرویس فقط با یک دامین یا مدل کار کند وجود ندارد)چیزی که سرویس ها برمیگردانند یا از جنس دامین هستند یا Dto. مثلا فرض کنید سرویسی داریم که قرار است پست ها را با تاریخ شمسی ثبت آن پست و به همراه مدت زمانی که صرف کوئری گرفتن شد برگرداند. در اینجا باید از یک Dto یا Viewmodel استفاده کنیم.در الگوی MVCگاها از مفهوم Dto و ViewModel به جای هماستفاده میکنند اما تفاوت هایی وجود دارد.دی تی اُ (Dto) ها کلاس هایی شامل پراپرتی ها هستند و هیچ گونه متدی ندارند. (یعنی رفتار خاصی را بروز نمیدهد).در حقیقت Dto نباید بیزنس لاجیک پیاده کند و صرفا همانطور که از نامش پیداست انتقال دهنده دیتاست (Data Transfer Object) .ویو مدل (ViewModel) ها شامل متدها و پراپرتی هایی هستند که علاوه بر کار Dto ممکن است بیزنسی را هم در خود داشته باشند. این موضوع بیشتر در الگوی MVVM دیده میشود. در الگوی MVC ، ویومدل، مدلی است که نهایتا در ویو و بخش پرزنتیشن نشان داده میشود یا از آن دریافت میشود.در پیاده سازی Clean Architecture میتوانیم با هم قراردادی بگذاریم که آنچه بین لایه ها رد و بدل میشود را Dto بنامیم و آنچه از سمت پرزنتیشن می آید یا به سمت آن میرود را ویومدل بگوییم.(یعنی در تعریفی "من‌‎در‌آوردی"، مدلی که مورد استفاده View است را ویومدل نامگذاری می‌کنیم.)کلاس پیاده سازی این سرویس را هم مینویسیم.فعلا دیتایی نداریم که برگردانیم و صرفا یک نمونه از تایپ خروجی میسازیم تا پروژه خطا ایجاد نکند.حالا لایه مربوط به api را میسازیم که مستقیما با UI در تماس است و متدهای GET,POST و... در آن پیاده سازی شده اند. یک پروژه ASP.NET Core Web Application درست میکنیم و نوع آن را API انتخاب میکنیم.کنترلر Post را به شکل زیر برای تست درست میکنیم.اینترفیس مربوط به سرویس(ها) مورد نیاز را به عنوان ورودی برای کانستراکتورِ کنترلر تعریف می‌کنیم. یعنی هر موقع آبجکتی از این کنترلر در ران تایم ساخته شد حتما باید یک آبجکت از یکی از پیاده سازی های اینترفیس IPostService به آن پاس داده شود تا به شکل postService_ در کنترلر قابل استفاده باشد.برای اینکه متغیر result مقدار داشته باشد، یک آبجکت تستی در سرویس مینویسیم.با اجرای پروژه و صدا زدن متد Getخطایی به شکل زیر میبینیممتن خطا میگوید که در کانستراکتور کنترلر اینترفیسی وجود دارد که پروژه وب ما نمیداند با چه سرویسی آن را Resolve کند. یعنی new کردن کلاس پیاده سازی اینترفیس به شکل خودکار انجام نشده است، دلیل این موضوع این است که ما تنظیمات دپندنسی اینجکشن را انجام ندادیم. یعنی نگفتیم IPostService با چه کلاسی باید جایگزین یا Resolve شود.اینکار را در فایل startup به شکل زیر انجام میدهیم.اینکه چرا AddScoped استفاده کردیم و AddTransient یا AddSingleton به درد ما نخورد، به این مقاله ارتباطی ندارد!با اجرای پروژه خروجی به شکل زیر خواهد بود.خروجی جیسون که با توجه به پاس دادن مقدار 1(تستی) به متد GET بدست آمدقبل از پیاده سازی بقیه 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 انجام میدهیم.اینترفیس ریپازیتوری که در لایه Domain ایجاد میشود.به پیاده سازی ها دقت کنید.پیاده سازی ریپازیتوری در لایه Infra انجام میشود.هدف در پیاده سازی ها صرفا آموزش است؛ همچنین در اینجا مپ کردن Entity به روی Domain به شکل دستی انجام شده که پیشنهاد میکنم از AutoMapper استفاده کنید. نکته مهم اینکه الزامی به چک کردن نال بودن مقادیر خروجی در ریپازیتوری نیست ولی حتما در یک گلوگاه اینکار را انجام دهید و اکسپشن مناسب را تولید کنید و حتما اکسپشن ها را به شکل متمرکز به کمک middleware یا به شکل پراکنده در اکشن های خود به یک خروجی تروتمیز تبدیل کنید.می‌توانید نال بودن را در لایه سرویس کنترل کنید که انعطاف پذیری ریپازیتوری های شما دستخوش کد های لاجیک چک کردن و تولید اکسپشن نشود.حالا کافیست به سرویس برویم و از ریپازیتوری استفاده کنیم.برای استفاده از ریپازیتوری، اینترفیس آن را در کانستراکتور معرفی(یا اینجکت) میکنیم و مقدار آن را در یک متغیر عمومی در سطح کلاس می‌ریزیم که در اینجا postRepository_ است. حالا در متدها میتوانیم به آن دسترسی داشته باشیم.و در نهایت api خود را بنویسیم.فراموش نکنید که ریپازیتوری ها و کانتکست هم مثل سرویس باید در startup معرفی شوند. چون به شکل اینجکت در کانستراکتور ها از آن استفاده شده و فریم ورک نمی‌داند چطور آنها را ارضا (کلمه مناسب تر پیدا نکردم) کند.اما لایه پرزنتر(presenter) کجاست؟به سادگی میتوان یک پرزنتر یکسان اختصاصی برای خروجی با فرمت جیسون در BaseController ساخت. اما BaseController کجاست؟ یک کلاس به نام BaseController میسازیم و از این به بعد کنترلرهایمان را از این کلاس ارث بری میکنیم. در این کلاس متدهایی را مینویسیم که قرار است در دسترس همه اکشن های درون کنترل ما قرار گیرند.کلاس و enum را همینجا تعریف کردم که به راحتی قابل بررسی باشد. بهتر است فایل های جدا در لایه Core برای این مدل "ساختارهای همه جا استفاده شو" درست کنید!این کلاس میتواند یک ایده برای ایجاد پرزنترهای پیچیده تر باشد.میتوان اکشن ها را به شکل زیر نوشت . دو متد CustomOk و CustomError را ما در BaseController خودمان ساختیم که فرمت یکسان در خروجی ها به UI بدهیم.اصلاح اکشن برای تولید خروجی هایی با فرمت یکسان و ساختارمند!برای تست، ItemNotFoundException هم یک خطای کاستوم است که خودمان درست کردیم.در لایه Core میتوانید برای اکسپشن های کاستوم خودتان فولدر جدا درست کنید.از این اکسپشن، در سرویس مربوط برای هندل کردن خطا استفاده کردیم و نهایتا مُچ آن در اکشن گرفته میشود(به دلیل وجود try/catch در مرحله آخر)در لایه سرویس نال بودن مقادیر خروجی متدهای موجود در ریپازیتوری را چک میکنیم و خطای مناسب تولید میکنیم.این ساده ترین راه برای کنترل خطاهاست.خروجی سالم :از یک اکستنشن به نام JsonView در کروم برای مرتب کردن خروجی جیسون استفاده کردم که به شکل درختی نمایش داده شود.خروجی ناسالم! : ساختار خطا، که شبیه به ساختار خروجی سالم است و زحمت برنامه نویس Front-End یا داکیومنت نویس Api را با اینکار کم کرده ایم.اما نکته آخر در مورد Infrastructure که فعلا فقط دیتا را از دیتابیس گرفته وتوسط ریپازیتوری به لایه Core می‌دهد. این لایه چه کارهای دیگری میتواند انجام دهد؟مثال اول:فرض کنید بخواهیم یک سرویسی از بیرون بگیریم. مثلا دمای هوا. باید در همین Infrastructure اینکار را انجام دهیم و اینترفیس آن را در لایه Core قرار دهیم. با این روش در معماری Clean Architecture ساختار داده ای ما کاملا جدا از Core خواهد بود و لایه Core ما صرفا دستور دریافت اطلاعات را به لایه ی Infra ارسال می‌کند، بدون آنکه اطلاعی از نحوه فراهم آمدن دیتا داشته باشد.(فراهم آوردن دیتا از دیتابیس یا فایل یا وب یا ... برای لایه Core هیچ اهمیتی ندارد)مثال دوم:فرض کنید می‌خواهید یک رمز تولید کنید و برای اینکار نیاز به salt دارید یا مشخصه ای منحصر به فرد از ماشین سرور فعلی خود را میخواهید که رمزی قوی تولید کنید. با اینکه مولد رمز بیزنس موجود در لایه Core است، ولی آن مشخصه منحصر به فردی که برای تولید رمز نیاز است از لایه Infra تامین میشود. پس برای اینکار هم باید اینترفیسی در Core وجود داشته باشد که پیاده سازی آن در لایه Infra انجام شود.امیدوارم با این دو مثال لایه Infrastructure برای شما هویت معنی دار و مشخص تری پیدا کرده باشد.به ساختار سولوشن نگاه کنید.پروژه Core که در دل خود Domain را دارد مرکز لایه هاست و بقیه پروژه ها از آن استفاده میکنند. این موضوع مفهوم کلیدی Clean Architecture است که رعایت شده است.این پروژه را به شکل کامل در گیتهاب بینید.چند نکته:این روش یکی از ساده ترین و کاربردی ترین روش ها برای پروژه های پیچیده است.اگر فکر می‌کنید ریپازیتوری جنریک به شما کمکی نمیکند و گیجتان می‌کند، درگیر آن نشوید. با اینکه برای انجام CRUD (که بیشتر پنل های ادمین به آن نیاز دارند) چشم پوشی از ریپازیتوری جنریک کمی شما را به زحمت می اندازد. بطور کلی چیزهایی به Clean Architecture اضافه کنید که باری را از روی دوش شما بر می‌دارد. الکی پروژه تمپلیت خود را شاخ و برگ ندهید.سرویس ها دو وظیفه مهم دارند :انتقال داده از لایه infra به لایه api و سر و شکل دادن به داده و آماده سازی برای مشاهده در UI. این آماده سازی فقط بر روی داده انجام میشود، یعنی چگونگی نمایش (اینکه جیسون باشد یا 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 ها میتوانند در مواردی، جایگزین سرویس ها باشند.