قسمت اول : مقدمات
قسمت دوم : مبانی معماری
قسمت سوم : نگاهی به معماری سنتی سه لایه
قسمت چهارم : اجزای Clean Architecture
قسمت پنجم : پیاده سازی بر اساس سرویس ها (شما در حال خواندن این مقاله هستید)
قسمت ششم : پیاده سازی بر اساس UseCase ها
قسمت هفتم : آشنایی با CQRS
در مقاله قبل با بخش های مختلف Clean Architecture بصورت تفکیک شده آشنا شدیم. در این مقاله سعی میکنم یک پروژه بر این اساس ایجاد کنم.
دوباره این تصویر را ببینیم و در طول مقاله به آن مراجعه کنید.
فرض کنید میخواهیم یک برنامه مدیریت محتوا تولید کنیم. این برنامه تحت وب است. پروژه در اینجا قابل دسترسی است.
از دات نت کور برای تولید این برنامه استفاده میکنیم ولی مفاهیم تقریبا یکسانی برای ایجاد یک معماری 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 به شکل زیر انجام میدهیم.
با اجرای پروژه خروجی به شکل زیر خواهد بود.
قبل از پیاده سازی بقیه 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 دو وظیفه یا رفتار در ریپازیتوری تعریف کنیم:
دو متد فوق را ابتدا در IPostRepository در لایه Core تعریف میکنیم و سپس پیاده سازی آنها را در لایه Infrastructure انجام میدهیم.
به پیاده سازی ها دقت کنید.
هدف در پیاده سازی ها صرفا آموزش است؛ همچنین در اینجا مپ کردن Entity به روی Domain به شکل دستی انجام شده که پیشنهاد میکنم از AutoMapper استفاده کنید.
نکته مهم اینکه الزامی به چک کردن نال بودن مقادیر خروجی در ریپازیتوری نیست ولی حتما در یک گلوگاه اینکار را انجام دهید و اکسپشن مناسب را تولید کنید و حتما اکسپشن ها را به شکل متمرکز به کمک middleware یا به شکل پراکنده در اکشن های خود به یک خروجی تروتمیز تبدیل کنید.
میتوانید نال بودن را در لایه سرویس کنترل کنید که انعطاف پذیری ریپازیتوری های شما دستخوش کد های لاجیک چک کردن و تولید اکسپشن نشود.
حالا کافیست به سرویس برویم و از ریپازیتوری استفاده کنیم.
و در نهایت 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 است که رعایت شده است.
این پروژه را به شکل کامل در گیتهاب بینید.
چند نکته:
امیدوارم این مقاله به درد شما خورده باشد. در نوشته ی بعدی سعی میکنم همین پروژه را با استفاده از UseCase ها توسعه دهم. UseCase ها میتوانند در مواردی، جایگزین سرویس ها باشند.