مرتضی دلیل
مرتضی دلیل
خواندن ۱۳ دقیقه·۵ سال پیش

آشنایی با Clean Architecture : نگاهی به معماری سنتی سه لایه (قسمت سوم)

قسمت اول : مقدمات
قسمت دوم : مبانی معماری
قسمت سوم : نگاهی به معماری سنتی سه لایه (شما در حال خواندن این مقاله هستید)
قسمت چهارم : اجزای Clean Architecture
قسمت پنجم : پیاده سازی بر اساس سرویس ها
قسمت ششم : پیاده سازی بر اساس UseCase ها
قسمت هفتم : آشنایی با CQRS


اگر مدت کوتاهی از کار حرفه ای شما گذشته باشد حتما با معماری سه لایه سنتی آشنا هستید.
در این معماری لایه سطح پایین، دسترسی به دیتابیس شماست.
در این لایه صرفا ذخیره سازی یا خواندن اطلاعات از یک نگهدارنده مثل فایل یا دیتابیس انجام میشود. به این لایه data یا persistence هم میگویند.
لایه بعدی بیزنس لاجیک یا application layer است.
بیزنس لاجیک چگونگی رفتار با داده ها را تعیین میکند. اینکه داده های دریافتی از کاربر چگونه تغییر کنند یا داده های دریافتی از دیتابیس چطور تغییر کند.
لایه بیرونی هم پرزنتیشن است که مسئول نمایش داده های متناسب و یا دریافت اطلاعات از سمت کاربر است.
نکته مهم در تعیین لایه مرکزی هست.
در حالت سه لایه معمولا کل ساختار وابسته به لایه data است و این میتواند نقص محسوب شود.
برای اینکه مکانیزم کاری این سه لایه را درک کنید یک مثال روزمره را با هم بررسی میکنیم.

فرض کنید که ساختار سه لایه نرم افزار را در خانه خود بخواهید پیاده کنید.

شما در بخش پذیرایی (presentation layer) نشسته اید ، سفارش غذای خود را به آشپزخانه که در اینجا میز آماده سازی نام دارد، میگویید و غذا را از همانجا دریافت میکنید.(سفارش شما یک درخواست«request» است و دریافت سفارش یک پاسخ«response» نام دارد.)

مسئول میزآماده سازی (application layer) وظیفه دارد بر اساس خواسته شما غذا را آماده سازی کند و از منبع یخچال(data layer) استفاده کند، همچنین میتواند یخچال را بر اساس مواد غذایی جدید پر کند. مثلا لوازم کیک را از یخچال خارج کند کیک را بسازد و دوباره در یخچال قرار دهد.

بطور کلی این معماری به شکل زیر قابل خلاصه شدن است.

لایه های معماری سه لایه
لایه های معماری سه لایه

لایه زیرین و به عبارتی مرکزی، دیتا است که میتواند براساس فایل یا یک دیتابیس دلخواه باشد.

لایه اپلیکیشن در حقیقت لایه عملیات منطقی ما (یا همان logic یا business) است.

لایه بیرونی، لایه پرزنتیشن است که در حقیقت UI ما را میسازد.

نحوه تعاملِ این سه لایه بر اساس توضیحات خانه ای که توضیح دادیم ساده و تقریبا «تقسیم وظایف شده» است.

در معماری clean لایه مرکزی انتیتی ها هستند یعنی جایی که قلب نرم افزار است و روی طراحی آن فکر شده است. همچنین پرزنتیشن و دیتا (persistence) با اینکه در ساختار لایه ای وجود دارند ولی جزئیات محسوب میشوند. این موضوع را در مقالات بعدی توضیح خواهم داد.

فقط به یاد داشته باشید که در Clean Architecture تمرکز اصلی روی مدل های دامین (انتیتی ها یا همان کلاس های مدل) و لایه اپلیکیشن(منطق رفتارهای مختلف در برنامه) است.

معمولا این سه لایه را با همین نام ها یا نام های مشابه نامگذاری میکنند یعنی اگر دردیاگرامی اسم متفاوت دیدید فکر نکنید سه لایه جدید هستند!

میخواهیم به کمک دات نت یک معماری سه لایه ساده را پیاده سازی کنیم.

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

یک پروژه ساده را با معماری سه لایه سنتی پیاده میکنیم.

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

فرض کنید به کمک دات نت میخواهیم api هایی را به عنوان مثال پیاده سازی کنیم.

طرح مسئله میکنیم.

عنوان : apiهای مربوط به ثبت و خواندن خبر

توضیح : میخواهیم یک جدول در دیتابیس sql داشته باشیم به نام post و سه api داشته باشیم. که دو تای آن مربوط به خواندن خبر (یکی تک خبر و یکی همه خبرها) و یکی آن مربوط به ثبت خبر باشد.از EntityFramework به عنوان ORM استفاده خواهیم کرد و از این به بعد به آن EF میگوییم.

دامین مدل (انتیتیها) : Post

فرمت api ها :

GET : api/posts Input:- Output:list of PostVm
GET : api/posts/:id Input:id Output:PostVm
POST: api/posts Input: AddPostVm Output:-

ممکن است تمایل داشته باشید از ریپازیتوری پترن هم استفاده کنیم اما آیا وقتی EF داریم لازم است unitofwork و repository پیاده سازی شود؟

همیشه اینکار بهترین روش نیست.

  • بطور کلی EF ذاتا کد شما را نسبت به تغییرات دیتابیس ایزوله میکند.
    این موضوع را میتوان از پشتیبانی EF از چندین نوع دیتابیس فهمید.
    شما هر وقت بخواهید با کمترین تغییر میتوانید کانکشن خود را از SQL Server به دیتابیس دیگر مثل MySql تعویض کنید.. فقط کافیست provider خود را تغییر دهید.
  • آبجکت DbContext در EF نقش unit of work را بازی میکند.
  • آبجکت DbSet در EF نقش Repository را بازی میکند.
  • در EF Core ما provider برای ذخیره سازی in-memory داریم که یعنی امکان unit test هم فراهم است.

در مورد این موضوع افراد مختلفی نظر داده اند.

  • جیمی بوگارت خالق MediatR و Automapper میگوید وقتی از ORM استفاده میکنید ایجاد یک لایه ابسترکشن دیگر چندان مزیتی نخواهد داشت. البته استفاده مستقیم از ORM در لایه UI را توصیه نمی کند.
  • استیو اسمیت نویسنده داکیومنت ماکروسافت در مورد معماری وب اپلیکیشن های مدرن و همچنین مدرس بهترین دوره های pluralsight عقیده داره که شما به ریپازیتوری نیازی ندارید اما نمیشه از مزایایی که پیاده سازی ریپازیتوری داره چشم پوشی کرد.
نویسنده این کتاب اعتقاد دارد که پیاده سازی پترن ریپازیتوری با حضور EF لازم نیست.
نویسنده این کتاب اعتقاد دارد که پیاده سازی پترن ریپازیتوری با حضور EF لازم نیست.
  • جان اسمیت نویسنده کتاب EF Core in action هم عقیده دارد استفاده از Repository/UnitOfWork زمانی که از EF Core استفاده میکنید لزومی ندارد.
وقتی سه متخصص در یک موضوع نظر یکسان داشته باشند شما چه کار میکنید؟
نکته اینجاست که باید فکر کرد که کلا فلسفه پترن ها حل کردن یک مساله است.

آیا در پروژه ای که در حال انجام آن هستید Repository/unitofwork مشکلی را حل میکند؟
در ادامه پروژه خود را بدون این پترن ها و فقط با کمک EF و معماری سه لایه پیاده میکنیم.

در انجام مراحل با اینکه جزئیات توضیح داده شده است اما فرض این است که به EF و web api و Sql serve آشنایی دارید.

به کمک ویژوال استودیو ابتدا لایه data را میسازیم تا مدل یا مدل های خود رابسازیم.

انتخاب نوع پروژه
انتخاب نوع پروژه
تعیین نام پروژه و نام سولوشن
تعیین نام پروژه و نام سولوشن
ایجاد شدن پروژه با یک کلاس به نام class1
ایجاد شدن پروژه با یک کلاس به نام class1

فایل class1 را به Post تغییر میدهیم.

به کمک دستور زیر EF را نصب میکنیم،

dotnet add package Microsoft.EntityFrameworkCore

پنجره Console package manager را در ویژوال استودیو باز میکنیم.

خطایی که میبینید به این دلیل است که درون فولدر پروژه مربوطه نیستیم و باید محل فایل csproj را پیدا کنیم.

با دستورات کنسول داخل فولدر پروژه قرار میگیریم.
با دستورات کنسول داخل فولدر پروژه قرار میگیریم.

دستور زیر را دوباره تکرار میکنیم

dotnet add package Microsoft.EntityFrameworkCore

اینبار چون در فولدر پروژه مربوطه هستیم نصب انجام میشود و میتوان از طریق فایل Post.Data.csproj و یا بخش Nuget در پنجره سولوشن به وجود این لایبرری مطمئن شد.

چون قرار است از قSql serve استفاده کنیم باید دو کتابخانه زیر را نصب کنیم. مطمئن باشید که نسخه ای از sql server (مثلا express) روی سیستم شما نصب باشد که بتوانیم از طریق EF به آن متصل شویم.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools.DotNet

بعد از نصب در پنجره سولوشن و فایل csproj کتابخانه ها را خواهید دید

اگر علامت زرد رنگ مثل تصویر فوق دیده شد کافیست پروژه را یکبار unload و سپس reload کنید (با رایت کلیک روی Post.Data آیتم های گفته شده را خواهید دید)

کلاسی به نام PostContext میسازیم که از DbContext ارث بری کند. رفرنس Microsoft.EntityFrameworkCore را اضافه میکنیم.

وظیفه این کلاس برقراری ارتباط با دیتابیس است.
فعلا این کلاس را رها میکنیم و مدل Post در دیتابیس را تکمیل میکنیم.

حالا باید کلاس post خود را برای دیتابیس قابل فهم کنیم. یعنی بگویم فلان پراپرتی کلید اصلی آن است یا فلان پراپرتی required است و تعداد کاراکتر آن فلان قدر است.

از روش Data Annotation یا Fluent Api میتوانیم استفاده کنیم.

ترجیح، استفاده از Fluent Api است چون Entity را با منطق دیتابیس درگیر نمیکند.

در فایل Post.cs کلاس زیر را اضافه میکنیم

و در context به این کلاس اشاره میکنیم.

حالا با تعریف DbSet متناظر با entity تعریف شده و اضافه کردن کانکشن استرینگ مراحل مقدماتی آماده سازی دیتابیس را تکمیل میکنیم:

میتوانید کانکشن استرینگ را در appsetting تعریف کنید.
میتوانید کانکشن استرینگ را در appsetting تعریف کنید.

تا اینجای کار لایه دیتا را ایجاد کردیم که حاوی entity ها (که در اینجا یکی است) است.

دقت کنید استفاده از Fluent api این امکان را میدهد که بتوانیم این لایه را هم دو قسمت کنیم. یکی domain models یا همان entity ها که فقط شامل کلاس post.cs است (و کلاس های دیگر که مرتبط با بخش domain model هستند و بعدا اضافه میشوند) و دیگری لایه data که کانتکست در آن وجود دارد.
با اینکار انتظار دارم ذهن شما با معماری clean architecture که در مقالات بعدی توضیح خواهم داد آشنا شود.

یک پروژه دیگر به نام Entities درست میکنیم.

به اسم و مسیر ها دقت کنید که در کنار پروژه قبلی ساخته شود.

آدرس لوکیشن در داخل فولدر Post.Data میخواهد پروژه ما را بسازد. به فولدرها نگاه میکنیم.

موقع ساختن پروژه در ابتدای کار، به دلیل بی دقتی، اسم سولوشن Post.Data شده و داخل این فولدر دو پروژه ما سقرار خواهد گرفت.

نام فولدر اصلی ما یا همان سولوشن ما، Post.Data است. همچنین فایل اجرای سولوشن اشتباها Post.Data.Sln است که آنرا هم اصلاح میکنیم.

ویژوال استودیو را می‌بندیم و فولدر حاوی این دو پروژه را به PostThreeLayer تغییر میدهیم. الان باید دو فولدر PostThreeLayer تو در تو داشته باشید.

حالا فایل های داخل فولدر دوم را به همین فولدر منتقل کنید و فولدر را پاک کنید.

حالا اسم سولوشن را از Post.Data.sln به PostThreeLayer.sln تغییر دهید و سپس روی فایل sln کلیک کنید تا پروژه اجرا شود.

ذکر این مشکلات و جزئیات راه حل‌ها فقط برای آشنایی شما با ساختار سولوشن و پروژه هاست چون برای طراحی معماری بارها اینکارها را باید انجام دهید.

کلاس Post.cs را از لایه قبل به اینجا منتقل کنید.

در لایه قبل کلاس PostMap.cs به شکل فایل تبدیل کنید ، چون عملیات مربوط به دیتابیس است نباید به لایه Entities منتقل شود.

چون ما معماری سه لایه را پیاده میکنیم و در این معماری چیزی به نام Enitites نداریم بهتر است یک فولدر درست کنیم و این دو پروژه را با هم لایه Data بنامیم.

دقت کنید که این فولدر را در پنجره سولوشن با رایت کلیک ساخته ایم و یک فولدر فیزیکی نیست.

کلاس PostMap در پروژه Post.Data نمیتواند کلاس Post در پروژه Post.Entities را ببیند.

قبل از اصلاحِ این موضوع، نحوه نامگذاری پروژه ها را اصلاح میکنیم. حضور Post هم در namespace و هم در نام کلاس ممکن است مشکل ساز شود.روی پروژه کلیک کرده و F2 را فشار میدهیم تا rename کنیم. همچنین در تمامی کلاس ها namespace را به اسم جدید تغییر میدهیم.

همچنین بهتر است کلاس PostContext به Context تغییر نام دهد.( چون کانتکست مربوط به یک تیبل Post نیست و مربوط به همه تیبل های آینده هم هست)

حتما فولدرهای پروژه در explorer ویندوز را چک کنید و تغییر نام دهید، در انتها فایل sln را توسط یک ادیتور باز کنید و بررسی کنید مسیرها روی نام های جدید تنظیم شده باشد.

اسم فولدر Data در سولوشن هم اصلاح کنیم و به Persistence تغییر دهیم. بهتر است نام فولدر و پروژه ها شبیه به هم نباشد.

نام ها اصلاح شد. برای اینکه پروژه Data بتواند کلاس های پروژه Entities را ببیند باید رفرنس بدهیم.

روی Post رایت کلیک کنید

اگر گزینه آخر را می‌بینید که روی آن کلیک کنید، اگر نمیبینید در پروژه Data روی Dependencies رایت کلیک کنید و Add references را انتخاب کرده و از بخش Solution تیک مربوط به پروژه Entities را بزنید.

با اضافه شدن رفرنس Entities به بالای کلاس، دسترسی به Post برای کلاس PostMap ممکن خواهد شد.

حالا باید به کمک مایگریشن دیتابیس را بسازیم.

قدم اول نصب Microsoft.EntityFrameworkCore.Design روی پروژه Data است.

Install-Package Microsoft.EntityFrameworkCore.Design

مطمئن شوید که در پروژه Data هستید و سپس دستور فوق را اجرا کنید

حالا دستور زیر را بنویسید

احتمالا به مشکل برخورد خواهید کرد و فولدر مایگریشن ایجاد نخواهد شد، مگر آنکه یک پروژه با startup داشته باشد.
دستورات EF Core با پروژه های Startup بدون مشکل کار میکنند و با class library مشکل دارد. چون بطور معمول روی تمپلیت اصلی دات نت برای web api یا MVC کانتکستی که ایجاد میشود توسط دپندنسی اینجکشن است که در زمان runtime اتفاق می افتد.
اما میتوانید با ارث بری از IDesignTimeDbContextFactory در لایبرری خود اینکار را انجام دهید. در این حالت زمانی که دستورات کامندی EF مثل مایگریشن را مینویسید پیاده سازی شما دیده میشود و کانتکست شما ساخته میشود.

دقت کنید که متغیر connectionString که در Context پیش از این تعریف شده بود را استاتیک کردیم که این کلاس به آن دسترسی داشته باشد.

حالا دستور مایگریشن را با اشاره به پروژه ای که قرار است برای آن مایگریشن اجرا شود مینویسیم:

فولدر مایگریشن در پروژه Data ساخته شد که حاوی دستوراتی است که باعث اعمال مدل تعریف شده بر روی دیتابیس میشود.

به کمک دستور زیر اسنپ شات ها بر روی دیتابیس اعمال میشوند

و دیتابیس به شکل زیر ایجاد خواهد شد.

حالا لایه پرزنتیشن را ایجاد میکنیم. چون پروژه ما قرار است web api باشد یک پروژه به سولوشن اضافه میکنیم.

اسم پروژه را Presentation میگذاریم.

پروژه جدید به شکل زیر است و یک کنترلر تستی دارد.

همین کنترلر را به آنچه میخواهیم تبدیل میکنیم. کلاس کنترلر بر اساس api های خواسته شده چیزی شبیه تصویر زیر است

دو ویو مدل PostVm و AddPostVm هم بر اساس api خواسته شده ایجاد میکنیم.

حالا فقط لایه بیزنس لاجیک یا لایه اپلیکیشن باقی مانده. یک کلاس لایبرری جدید درست میکنیم به نام Application :

چون قرار نیست repository/unit of work درست کنیم کارمان راحت است. اگر قرار بود ریپازیتوری و uow داشته باشیم باید اینترفیس های آن را در لایه اپلیکیشن طراحی میکردیم و لایه data باید این اینترفیس ها را پیاده میکرد. با اینکار اگر در آینده لایه دیتا تغییر میکرد اتفاقی برای لایه های دیگر نمی افتاد و لایه دیتای جدید باز هم باید اینترفیس های موجود در لایه اپلیکیشن را پیاده سازی میکرد.

فقط یک سرویس مرتبط با api ها را میسازیم. با یک سرویس به نام PostService میتوانیم سه api را پیاده سازی کنیم.

فایل اینترفیس و کلاسش را میسازیم و هیچ متدی را فعلا نه به شکل اینترفیس و نه به شکل پیاده سازی نمینویسیم.

به سراغ api میرویم. لایه پرزنتیشن باید لایه اپلیکیشن را ببیند تا بتواند با اینجکت کردن اینترفیس ها به متدهای این لایه دسترسی پیدا کند.

پس در لایه پرزنتیشن add reference میکنیم و Application را اضافه میکنیم.

به کانستراکتور اینترفیسی که میخواهیم را اینجکت میکنیم

در فایل startup این اینجکشن را تعریف میکنیم

حالا api مربوط به لیست پست ها را پیاده سازی میکنیم

هنوز متد GetPosts که نوشتیم در اینترفیس IPostService و سرویس PostService وجود ندارد.

همچنین هنوز متد PostDto که خروجی سرویس است را پیاده نکردیم. PostDto میتواند همان Entity اصلی یعنی Post باشد. این بستگی به شما و متد شما دارد که چه سرویسی ارائه میدهد. معمولا در متدهای ساده میتوان از همان Entity استفاده کرد

متد Wrap خروجی Dto را به ویومدل مناسب این api تبدیل میکند.

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

با کلیک روی علامت حباب زرد رنگ میگوییم که برایمان متد GetPosts را در اینترفیس generate کند.

روی علامت حباب زرد رنگ روی PostDto هم کلیک میکنیم و میگوییم که یک کلاس برای این بخش درست کند. فعلا پراپرتی های آن برای ما مهم نیست.

در سرویس هم با همان حباب زرد رنگ پیاده سازی اینترفیس را اعمال میکنیم

برای پیاده سازی این متد نیاز به context داریم. لایه application باید به لایه data دسترسی داشته باشد.

کانتکست را اینجکت کردیم ولی یک resolver نیاز داریم که در لایه اپلیکیشن بتواند دپندنسی اینجکشن را هندل کند. از کتابخانه autofac استفاده میکنیم ولی کتابخانه های مشابه هم قابل استفاده هستند.(از اکستنشن دپندنسی اینجکشن مایکروسافت هم میتوانید استفاده کنید)

به کمک nuget در پروژه اپلکیشن کتابخانه autofac را نصب میکنیم و فایل ماژولی برای این لایه درست میکنیم.

باید به کمک RegisterType سرویس PostService و Context را برای constructor ها قابل فهم کنیم.

در پروژه Presentation هم باید در کلاس startup از ماژول applicationModule استفاده کنیم.

باید کتابخانه autofac.Extension.DependencyInjection را در پروژه presentation نصب کنیم.

خطوط زیر را به startup اضافه میکنیم)بر اساس راهنمای autofac)

حالا در api اصلاحات زیر را انجام میدهیم.

رپر(wrapper) را در ویومدل پیاده سازی کردیم که کپسوله باشد.

خروجی را به شکل کاستوم ایجاد کردیم.

میتوانیم یک BaseController به عنوان والد کنترلر ها بسازیم و از آن ارث بری کنیم و متدی برای تولید فرمت خروجی در آن قرار دهیم و به جای اینکه هر دفعه اینجا بنویسیم Ok ، یکبار در یک متد والد اینکار را کنیم و در اکشن ها هر بار آن متد را صدا بزنیم.

فایل PostVm.cs به شکل زیر است

متدهای دیگر هم به همین شکل پیاده میکنیم.

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

پروژه انجام شده را میتوانید در این گیت ببینید.

در مقاله بعدی به Clean Architecture میپردازیم.

برنامه نویسیclean architecture3 layer
برنامه نویس و علاقمند به برنامه نویسی، سینما، فلسفه و هر چیزی که هیجان انگیز باشد. در ویرگول از روزمرگیهای مرتبط با علاقمندیهام خواهم نوشت. در توئیتر و جاهای دیگر @mortezadalil هستم.
شاید از این پست‌ها خوشتان بیاید