IBPMS یک سیستم یکپارچه مدیریت فرآیندهای کسب و کار است که امکانات پیشرفتهای برای مدیریت و بهبود فرآیندهای سازمانی ارائه میدهد. این سیستم امکان ساخت و ویرایش فرآیندها،فرم ها،گزارش ها،سیستم مانیتورینگ قدرتمند، مدیریت صفحات، کاربران و سازمانها را با استفاده از رابطهای گرافیکی فراهم میکند
معرفی مفاهیم virtual, override, abstract و sealed در #C :
همانطور که مستحضر هستید کلمهی Polymorphism به معنای چندریختی است. در برنامهنویسی شیءگرا پلی مورفیسم اغلب به عنوان یک تغییردهنده رفتار یا یک واسط چند متدی مطرح میشود.
در زبان برنامهنویسی #C پلی مورفیسم به سه شکل ممکن پیادهسازی میشود:
1- استفاده از متدهای virtual و override کردن آنها در کلاس فرزند
2- بهرهگیری از متدهای abstract در کلاس والد
3- بهرهگیری از قابلیت واسطها یا Interfaceها ( واسطها در فصول بعدی به تفصیل توضیح داده خواهند شد)
متدهای virtual
فرض کنید یک کلاس والد به نام Shape دارید که در آن متدی به نام Draw تعریف شده است. متد Draw وظیفهی ترسیم یک شیء یا شکل را به عهده دارد. این متد در تمام کلاسهایی که از این کلاس مشتق میشوند قابل استفاده است. بنابراین کلاس Shape را به صورت زیر مینویسم:
public class Shape
{
public void Draw()
{
Console.WriteLine("Drawing the shape!");
}
}
حال باید کلاسهای فرزند مرتبط با این کلاس را تعریف کنیم بنابراین سه کلاس به نامهای Triangle و Rectangle و Circle را تعریف میکنیم که هر سه از کلاس والد یعنی Shape مشتق شده اند:
public class Rectangle : Shape
{
}
public class Triangle : Shape
{
}
public class Circle : Shape
{
}
هر سه کلاسی که در فوق تعریف کردیم میتوانند از متد Draw استفاده کنند زیرا این متد در کلاس اصلی والد تعریف شده است. حال اگر شیءای بسازیم آنگاه:
var rect = new Rectangle();
var tri = new Triangle();
var circ = new Circle();
rect.Draw();
tri.Draw();
circ.Draw();
Console.ReadKey();
در نهایت خروجی دستورات به صورت زیر است:
Drawing the shape!
Drawing the shape!
Drawing the shape!
اما این خروجی مورد پسند نیست و باید برای هر کلاس یک شکل کشیده شود در نهایت برای رفع این مشکل متد موجود در کلاس والد را به صورت virtual تعریف کرده و آن را درون کلاس فرزند override میکنیم. بنابراین تغییراتی در کلاس والد داده و متد Draw را به صورت زیر تعریف میکنیم:
public virtual void Draw()
{
Console.WriteLine("Drawing the shape!");
}
حال همین تغییرات را درون کلاسهای فرزند انجام میدهیم:
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing rectangle!");
}
}
حال خروجی دستورات فوق به صورت زیر اصلاح میشوند:
Drawing rectangle!
Drawing the shape!
Drawing the shape!
همچنین اگر از کلمهی کلیدی base درون متد کلاس فرزند استفاده کنیم محتوا و متد کلاس پایه نمایش داده خواهد شد:
public override void Draw()
{
base.Draw();
}
استفاده از روش virtual و override تنها به متدها ختم نمیشود بلکه میتوان برای خصوصیات یا Property ها نیز این کار را انجام داد:
public class Human
{
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual string FullSpecification
{
get { return FirstName + " " + LastName; }
}
}
public class Employee : Human
{
public string JobPosition { get; set; }
public override string FullSpecification
{
get { return base.FullSpecification + " with job position: " + JobPosition; }
}
}
معرفی کلاسها و متدهای abstract و sealed
هنگامیکه یک برنامه را مینویسیم و کلاسها و اعضای آن را مشخص میکنیم باید برای استفاده از کلاسها یک سری محدودیت بگذاریم. مثلا یک کلاس پایه داریم که این کلاس نباید توسط سایر کلاس ها مورد استفاده قرار بگیرد و تنها اعضای آن کلاس و کلاس هایی که از آن به ارث بردهاند، توانایی دسترسی به متدها و اعضای آن را دارند یا مثلا کلاسی را ایجاد کردهایم که با اعمال محدودیتهایی اجاره ایجاد کلاس فرزند از آن را نمیدهیم. برای اعمال همچین محدودیتهایی از کلاسها یا متدهایی با فرم abstract یا sealed استفاده میشود.
کلاسها و اعضاء abstract
به مثال قبلی باز میگردیم که یک کلاس والد به نام Shape داشتیم و از روی این کلاس سه فرزند ساخته بودیم:
public class Shape
{
public void Draw()
{
Console.WriteLine("Drawing the shape!");
}
}
public class Rectangle : Shape
{
}
public class Triangle : Shape
{
}
public class Circle : Shape
{
}
اگر به مثال بالا مراجعه کنید متوجه میشوید که کلاس Shape عملا برای ما کاربردی ندارد. به عبارت دیگر در طول برنامهی اصلی از آن استفاده نشده است. یعنی باید محدودیتی اعمال کنیم که از روی کلاس Shape شیءای ایجاد نشود. برای اینکار کافیست کلاس Shape را از نوع abstract تعریف کنیم. بنابراین طی یک تعریف کلی برای abstract داریم:
اگر کلاسی به صورت abstract ایجاد شود، در طول برنامه نمیتوان از روی آن شیءای ساخت.
به مثال زیر توجه کنید:
public abstract class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing the shape!");
}
}
حال اگر بخواهیم از روی کلاس Shape یک شیء ایجاد کنیم با خطای زیر مواجه میشویم:
Cannot create an instance of the abstract class
یعنی نمیتوان از روی کلاسهایی که به صورت abstract تعریف شدهاند شیءای ایجاد کرد.
اما کاربردهای بیشتری از کلاس abstract انتظار میرود. در کلاسهای abstract میتوان به طور مشابه متدهایی را تعریف کرد که به صورت abstract باشند. این متدها تنها شامل signature هستند یعنی بدنهای نداشته و پس از تعریف آنها به علامت ; ختم میشوند. درصورتیکه یک متد به صورت abstract تعریف شود بدین گونه است که حتما باید آن را جهت استفاده در طی برنامه یا کلاس دیگر override کنند. برای مثال یکبار دیگر کلاس Shape را بازنویسی میکنیم:
public abstract class Shape
{
public abstract void Draw();
}
همانطور که ملاحظه میکنید متدی به نام Draw وجود دارد که بدنهای ندارد. علت این امر تعریف این متد به صورت abstract است. حال اگر کلاس فرزندی از کلاس Draw به ارث ببرد باید همواره متد درون آن به صورت abstract قرار بگیرد. بنابراین داریم:
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing rectangle!");
}
}
کلاسها و اعضاء sealed
در بخش وراثت آموزش دادیم که همواره میتوان یک زنجیرهی وراثت در اختیار داشت مثلا کلاس B از کلاس A و کلاس C از کلاس B مشتق شود. حال درنظر بگیرید که میخواهیم به نحوی این زنجیرهی وراثت را قطع کنیم. برای اینکار باید کلاس موردنظر را از نوع sealed تعریف کنیم. بنابراین در طی یک تعریف کلی داریم:
کلاسهایی که به صورت sealed مورد استفاده قرار میگیرند، باعث حذف زنجیره وراثت میشوند.
به مثال زیر توجه کنید:
public class A
{
}
public sealed class B : A
{
}
در این کد کلاس B از نوع sealed تعریف شده است و این موضوع بدین معناست که هیچ کلاس دیگری نمیتواند از کلاس B مشتق شود و یا متدها و ویژگیهایی را به ارث ببرد. به عنوان مثال اگر کد زیر را پیاده سازی کنیم:
public class A
{
}
public sealed class B : A
{
}
public class C:B
{
}
آنگاه با خطای زیر روبهرو میشویم:
Cannot inherit from sealed class 'B'
بدین معنیست که شما نمیتوانید از یک کلاس که به صورت sealed تعریف شده است ویژگی یا متدی را به ارث ببرید.
یکی دیگر از کاربردهای عبارت sealed جلوگیری از override کردن یک متد است. به مثال زیر توجه کنید:
public abstract class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing the shape!");
}
}
public class Rectangle : Shape
{
public sealed override void Draw()
{
Console.WriteLine("Drawing rectangle!");
}
}
این مثال بدین صورت عمل میکند که اگر کلاسی از کلاس Rectangle مشتق شد، دیگر قابلیت override کردن متد Draw را نداشته باشد زیر در متد موجود در کلاس Rectangle، متد به صورت sealed تعریف شده است.
فرق Class و Struct در #C :
Struct در سی شارپ یک جایگزین سبک حجم برای کلاس ها هستند. پس زمانی که می خواهیم نسخه های زیادی از یک داده را مقداردهی کنیم از Struct در سی شارپ استفاده می کنیم. تو این قسمت در مورد مفهوم Struct حرف بزنیم و این که Struct چه فرقی با Class دارد، برای ایجاد Struct ابتدا یک Class اضافه می کنیم
class ClassA
{
}
بعد از ایجاد کلاس نام کلیدی کلاس را پاک می کنیم و می توانیم نامی که برای کلاسمان در نظر گرفتیم هم پاک کنیم و نام دلخواه خودمان بگذاریم.
struct MyStruct
{
}
و مثل کلاس می توانیم فیلد و تابع داشته باشیم:
using System.Windows.Forms;
//—————————————–
struct MyStruct
{
public int id;
public void Show()
{
MessageBox.Show(id.ToString());
}
}
داخل پنجره فرم شده و از Struct نمونه می سازیم:
private void button1_Click(object sender, EventArgs e)
{
MyStruct S = new MyStruct();
S.id = 10;
S.Show();
}
سوال : اگر Struct تمام قابلیت های Class را دارد و هر دو مساوی هستند چرا هر دو را داخل زبان سی شارپ گذاشتند؟
این دو تا یک تفاوت هایی با هم دارند که الان با هم بررسی می کنیم:
در Struct لازم نیست برای ایجاد شی جدید از دستور new استفاده کنیم، می توانم مثل متغیر ساده int آن را تعریف کنم و بعد به آن مقدار بدهیم:
برنامه را اجرا می کنیم و روی button1 کلیک می کنیم انتظار می رود عدد 10 را نمایش دهد.
امّا مشاهده می کنیم که عدد 20 را نمایش می دهدچون class رفرنسی است یعنی می رود به آدرسش نگاه می کند و چون خاصیت ارث بری هم دارد مقدار خودش را به آدرسش می دهد .
این بار برنامه را اجرا می کنیم و روی button2 کلیک می کنیم تا کد Struct اجرا شود:
مشاهده می کنیدکه عدد 10 را نمایش می دهد چون Struct از نوع Value تایپ است یعنی در Struct هر کدام مقدار خودشان را نمایش می دهند.
تفاوتStruct و class
1- در class به محض ساخته شدن شی فضایی به آن اختصاص داده می شود ولی در Struct حتی با وجود ساخته شدن شی فضایی به آن اختصاص داده نمی شود تا زمانی که مقداری داخل آن فیلد قرار گیرد.
2- در Struct لازم نیست که برای شی از کلمه ی new استفاده شود.
3- class رفرنس تایپ است اما value , Struct تایپ است در نتیجه چون کلاس از فیلد رفرنس استفاده می کند بنابراین حافظه بیشتری اشغال می کند. ولی Struct چون از فیلد رفرنس استفاده نمی کند حافظه کمتری اشغال می کند .
4- کلاس می تواند وراثت داشته باشد امّا Struct نمی تواند وراثت داشته باشد .
5- Struct نمی تواند تابع مخرب داشته باشد.
شباهت Struct و class
هر دو می توانند تابع سازنده داشته باشند.( البته خود Struct دارای تابع سازنده پیش فرض می باشد ولی اگر برای آن یک تابع سازنده تعریف کردیم باید تمام فیلدها را در آن مقداردهی اولیه کنیم. و تابع سازنده در Struct باید حداقل یک پارامتر داشته باشد.)
یعنی اگر کلاس A داشتیم و خواستیم بعد مدتی گسترشش بدیم و ی سری ویژگی ها بهش اضافه کنیم،پس باید ی کلاس دیگه بسازیم به اسم B که از کلاس A رو اکستند (extend) کنه (کلاس B ارث بری کنه از A) باید این قدر جامع باشه که هر رفتاری که کلاس B انجام میدهد از کلاس A مشتق شده باشد
کلاس ها نباید مجبور باشن متد هایی که به آن ها احتیاج ندارند رو پیاده سازی کنند ، مثلا اگر دو کلاس از یک Interface پیروی میکنند که در اون متدی وجود دارد که نیازی به یکی از آن ها را ندارد پس نباید از آن ارث بری کند و باید یک Interface جدا براش در نظر گرفته شود.
Dependency Inversion Principle (اصل تفکیک رابط) :
کلاس ها ی سطح بالا نباید به کلاس های پایین دسترسی داشته باشند.
اصول SOLID مجموعهای از پنج قانون طراحی شیگرا هستند که به توسعهدهندگان کمک میکنند تا کدهای خود را با کیفیت بالاتری بنویسند. در اینجا این پنج اصل را با توضیحات و مثالهای کد C# توضیح میدهیم:
1. Single Responsibility Principle (SRP)
هر کلاس فقط باید یک مسئولیت داشته باشد و این مسئولیت کاملاً مشخص باشد.
مثال:
public class Employee
{
public string Name { get; set; }
public double Salary { get; set; }
public double CalculateBonus()
{
return Salary * 0.1;
}
}
public class EmployeeRepository
{
public void Save(Employee employee)
{
// کد مربوط به ذخیرهسازی کارمند در پایگاه داده
}
}
در این مثال، کلاس Employee فقط مسئول اطلاعات و عملیات مربوط به کارمند است و کلاس EmployeeRepository مسئول ذخیرهسازی کارمند است.
2. Open/Closed Principle (OCP)
کلاسها باید برای توسعه باز و برای تغییر بسته باشند. به عبارت دیگر، باید بتوانیم کلاسها را بدون تغییر در کد موجود توسعه دهیم.
مثال:
public abstract class Employee
{
public string Name { get; set; }
public double Salary { get; set; }
public abstract double CalculateBonus();
}
public class PermanentEmployee : Employee
{
public override double CalculateBonus()
{
return Salary * 0.1;
}
}
public class TemporaryEmployee : Employee
{
public override double CalculateBonus()
{
return Salary * 0.05;
}
}
در این مثال، کلاس Employee برای اضافه کردن نوعهای جدید از کارمندان باز است، اما نیازی به تغییر در کلاسهای موجود ندارد.
3. Liskov Substitution Principle (LSP)
کلاسهای مشتق باید بتوانند جایگزین کلاسهای پایه خود شوند بدون اینکه رفتار برنامه تغییر کند.
مثال:
public abstract class Shape
{
public abstract double Area();
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
public void DisplayArea(Shape shape)
{
Console.WriteLine(shape.Area());
}
در این مثال، میتوانیم از هر کلاس مشتق شده از Shape استفاده کنیم و متد Area بدون مشکل کار خواهد کرد.
4. Interface Segregation Principle (ISP)
کلاسها نباید مجبور به پیادهسازی اینترفیسهایی باشند که از آنها استفاده نمیکنند. اینترفیسها باید کوچک و ویژه باشند.
مثال:
public interface IWorker
{
void Work();
}
public interface IEater
{
void Eat();
}
public class Worker : IWorker, IEater
{
public void Work()
{
Console.WriteLine("Working...");
}
public void Eat()
{
Console.WriteLine("Eating...");
}
}
public class Robot : IWorker
{
public void Work()
{
Console.WriteLine("Working...");
}
}
در این مثال، کلاس Robot فقط نیاز به پیادهسازی متد Work دارد و نیازی به پیادهسازی متدهای غیرضروری ندارد.
5. Dependency Inversion Principle (DIP)
ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند. هر دو باید به انتزاعها وابسته باشند. انتزاعها نباید به جزئیات وابسته باشند، بلکه جزئیات باید به انتزاعها وابسته باشند.
مثال:
public interface IMessageSender
{
void SendMessage(string message);
}
public class EmailSender : IMessageSender
{
public void SendMessage(string message)
{
// کد مربوط به ارسال ایمیل
}
}
public class Notification
{
private readonly IMessageSender _messageSender;
public Notification(IMessageSender messageSender)
{
_messageSender = messageSender;
}
public void Notify(string message)
{
_messageSender.SendMessage(message);
}
}
در این مثال، کلاس Notification به کلاسهای سطح پایین (مانند EmailSender) وابسته نیست، بلکه به اینترفیس IMessageSenderوابسته است. این باعث میشود تا به راحتی بتوان کلاسهای جدید برای ارسال پیام ایجاد کرد و از آنها استفاده کرد بدون نیاز به تغییر در کلاس Notification.
زمانی که تصمیم داریم برنامه ای را به صورت شئ گرا بنویسیم، باید شروع به تحلیل سیستم و شناسایی موجودیت های آن کنیم. در بالا مثالی را در مورد برنامه کتابخانه بررسی کردیم. شئ عضو را در نظر بگیرید، شاید این عضو خصوصیت های بسیاری داشته باشد، مانند رنگ چشم، رنگ مو، قد، وزن، رنگ پوست و … . اما آیا تمامی این خصوصیات در سیستم به کار می آید؟ در مورد رفتارهای یک شئ نیز همین موضوع صدق می کند. مفهوم Abstraction به ما می گوید زمان بررسی یک موجودیت، تنها خصوصیات و رفتارهایی باید در تعریف موجودیت لحاظ شوند که مستقیماً در سیستم کاربرد دارند. در حقیقت Abstraction مانند فیلتری عمل می کنند که تنها خصوصیات و رفتارهای مورد استفاده در برنامه ای که قصد نوشتن آن را داریم از آن عبور می کنند.
Encapsulation
فرض کنید ماشین جدیدی خریداری کرده اید، پشت فرمان ماشین می نشینید و ماشین را استارت می زنید. استارت زدن ماشین خیلی ساده است، قرار دادن سوئیچ و چرخاندن آن و روشن شدن ماشین. اما آیا پروسه ای که داخل ماشین طی شده برای روشن شدن نیز همینقدر ساده است؟ صد در صد، عملیات های بسیار دیگری اتفاق می افتد تا ماشین روشن شود. اما شما تنها سوئیچ را چرخانده و ماشین را روشن میکنید. در حقیقت پیچیدگی عملیات روشن شدن ماشین از راننده ماشین پنهان شده است. به این عملیات Encapsulation یا پنهان سازی پیچیدگی پیاده سازی عملیات های درون یک شئ می گویند.
Inheritance
می توان گفت Inheritance یا وراثت اصلی ترین مفهوم در برنامه نویسی شئ گرا است. زمانی که شما خوب این مفهوم را درک کنید ۷۰ درصد از مفاهیم برنامه نویسی شئ گرا را درک کرده اید. برای درک بهتر این مفهوم مثالی میزنیم. تمامی انسان های متولد شده بر روی کره خاکی از یک پدر و مادر متولد شده اند. در حقیقت این پدر و مادر والدین انسان هستند. زمانی که انسانی متولد می شود یکسری خصوصیات و ویژگی ها را از والدین خود به ارث می برد، مانند رنگ چشم، رنگ پوست یا برخی ویژگی های رفتاری. در برنامه نویسی شئ گرا نیز به همین صورت می باشد. زمانی که شما موجودیت را طراحی می کنید، می توانید برای آن یک کلاس Base یا والد در نظر بگیرید که شئ فرزند تمامی خصوصیات و رفتارهای شئ والد را به ارث خواهد برد. مهمترین ویژگی وراثت، استفاده مجدد از کدهای نوشته شده است که حجم کدهای نوشته شده را به صورت محسوسی کاهش می دهد.
Polymorphism
در فرهنگ لغت این واژه به معنای چند ریختی ترجمه شده است. اما در برنامه نویسی شئ گرا چطور؟ خیلی از افراد با این مفهوم مشکل دارند و درک صحیحی از آن پیدا نمی کنند. مفهوم Polymorphism رابطه مستقیمی با Inheritance دارد. یعنی شما ابتدا نیاز دارید مفهوم وراثت را خوب درک کرده و سپس به یادگیری Polymorphism بپردازید. باز هم برای درک مفهوم Polymorphism یک مثال از دنیای واقعی میزنیم. در کره خاکی ما انسان های مختلفی در کشور های مختلف و شهر های مختلف با گویش های مختلف زندگی می کنند. اما تمامی این ها انسان هستند. در اینجا انسان را به عنوان یک شئ والد و انسان چینی، انسان ایرانی و انسان آمریکایی را به عنوان اشیاء فرزند که از شئ انسان مشتق شده اند یا والد آنها کلاس انسان می باشد را در نظر بگیرید. کلاس انسان رفتاری را تعریف می کند به نام صحبت کردن. اما اشیاء فرزند آن، به یک صورت صحبت نمی کنند، انسان ایرانی با زبان ایرانی، چینی با زبان چینی و آمریکایی با زبان آمریکایی صحبت می کند. در حقیقت رفتاری که در شئ والد تعریف شده، در شئ های فرزند مجدد تعریف می شود یا رفتار آن تغییر می کند. این کار مفهوم مستقیم Polymorphism می باشد. در زبان های برنامه نویسی شئ گرا، Polymorphism به تغییر رفتار یک شئ در اشیاء فرزند آن گفته می شود. در زبان سی شارپ این کار با کمک تعریف متدها به صورت virtual و override کردن آنها در کلاس های فرزند انجام می شود. همچنین Polymorphism با کمک Interface ها قابل پیاده سازی است
راه اندازی Clean Architecture و پیاده سازی DDD ، دو اقدام اساسی هستند که با کمک آنها، یک سیستم ساختارمند و قدرتمند حاصل میشود. در بخش اول این مقاله، به بررسی نحوه راه اندازی Clean Architecture و لایههای مختلف آن میپردازیم و پس از آن، به سراغ پیادهسازی Domain Driven Design میرویم. درنهایت، به شما روشی را معرفی میکنیم که با کمک آن میتوانید از صحت معماری پیادهسازیشده، مطمئن شوید.
DDD (Domain Driven Design)چیست ؟
DDD مخفف عبارت Domain-Driven Design است که به معنای “طراحی مبتنی بر دامنه” است. DDD یک رویکرد برای طراحی و توسعه نرمافزار است که تاکید بر مدلسازی و درک دامنه (domain) کسبوکار و ارتباطات آن دارد. این روش توسط اریک ایوانز (Eric Evans) معرفی شد و در کتابی با عنوان “Domain-Driven Design: Tackling Complexity in the Heart of Software” در سال 2003 به تفصیل توضیح داده شده است.
اصول و مفاهیم کلیدی DDD
دامنه (Domain):
دامنه شامل مفاهیم، قوانین، و منطق کسبوکاری است که نرمافزار باید آنها را پشتیبانی کند. درک کامل دامنه اصلیترین هدف DDD است.
مدل دامنه (Domain Model):
مدل دامنه یک نمایش انتزاعی از دامنه است که شامل اشیا، موجودیتها (Entities)، ارزشها (Value Objects)، و سرویسهای دامنه (Domain Services) است.
موجودیتها (Entities):
اشیای دارای هویت متمایز که با گذشت زمان تغییر میکنند. هر موجودیت یک شناسه منحصر به فرد دارد.
اشیای ارزش (Value Objects):
اشیای بدون هویت که فقط بر اساس ارزشهایشان مقایسه میشوند. این اشیا باید تغییرناپذیر (Immutable) باشند.
خدمات دامنه (Domain Services):
عملیاتهایی که به یک موجودیت خاص تعلق ندارند ولی به منطق دامنه مربوط میشوند.
مجموعهها (Aggregates):
گروهی از موجودیتها و اشیای ارزش که به عنوان یک واحد واحد در نظر گرفته میشوند. یک موجودیت ریشهای به نام “ریشهی مجموعه” (Aggregate Root) وجود دارد که دسترسی به سایر اعضای مجموعه را کنترل میکند.
محدودهی همدست (Bounded Context):
یک محدوده مشخص که در آن یک مدل دامنه خاص معتبر است. هر محدودهی همدست میتواند یک مدل دامنه متفاوت داشته باشد که نیازهای آن محدوده خاص را برآورده میکند.
زبان مشترک (Ubiquitous Language):
زبانی که توسط تیم توسعه و متخصصان دامنه استفاده میشود تا ارتباطات و مستندات را تسهیل کند. این زبان باید در سراسر پروژه به صورت یکپارچه استفاده شود.
مزایای استفاده از DDD
تسهیل درک دامنه:
با استفاده از مدل دامنه و زبان مشترک، توسعهدهندگان و متخصصان دامنه به راحتی میتوانند نیازهای کسبوکار را درک و مستند کنند.
افزایش قابلیت نگهداری:
کدهای مبتنی بر DDD به دلیل داشتن ساختار واضح و منظم، آسانتر نگهداری و توسعه مییابند.
هماهنگی بهتر بین تیمها:
با استفاده از محدودههای همدست، تیمها میتوانند به طور مستقل بر روی بخشهای مختلف دامنه کار کنند بدون اینکه با یکدیگر تداخل داشته باشند.
قابلیت توسعهپذیری:
ساختار مدل دامنه و استفاده از الگوهای طراحی باعث میشود که سیستم به راحتی قابل توسعه و تغییر باشد.
نتیجهگیری
DDD یک روش قدرتمند برای طراحی و توسعه نرمافزار است که به کمک آن میتوان سیستمهایی پیچیده و بزرگ را با استفاده از مدلسازی دقیق دامنه کسبوکار و استفاده از اصول و مفاهیم مشخص ساخت. این روش به توسعهدهندگان کمک میکند تا نرمافزارهایی با کیفیت و قابل نگهداری بالا بسازند که به خوبی نیازهای کسبوکار را برآورده کنند.
Clean Architectureچیست ؟
Clean Architecture یا “معماری تمیز” یک الگوی معماری نرمافزاری است که توسط رابرت سی. مارتین (Robert C. Martin)، معروف به Uncle Bob، معرفی شده است. هدف اصلی این معماری جداسازی نگرانیها (Separation of Concerns) و ایجاد سیستمهای نرمافزاری با ساختاری منعطف، قابل نگهداری، و قابل تست است. معماری تمیز به توسعهدهندگان کمک میکند تا نرمافزارهایی بسازند که به راحتی قابل تغییر و توسعه باشند و وابستگیها به حداقل برسند.
اصول و مفاهیم کلیدی Clean Architecture
لایههای معماری:
Clean Architecture سیستم را به چندین لایه تقسیم میکند که هر کدام مسئولیتهای خاص خود را دارند. این لایهها شامل موارد زیر هستند:
Entities : شامل موجودیتهای دامنه و منطق اصلی کسبوکار. این لایه کاملاً مستقل از سایر لایهها است و میتواند در هر پروژهای استفاده شود.
Use Cases : شامل منطق کاربردی سیستم است. این لایه از موجودیتها استفاده میکند و مسئول اجرای عملیاتهای کسبوکار است.
Interface Adapters : شامل کدهایی است که ورودی و خروجی سیستم را به فرمت قابل فهم برای Use Cases و Entities تبدیل میکند. این لایه شامل کنترلرها، پریزنتیشن، و مبدلهای داده است.
Frameworks and Drivers : شامل کدهای زیرساختی و جزئیات پیادهسازی است که وابسته به فریمورکها و ابزارهای خاص هستند. این لایه شامل پایگاهداده، رابطهای کاربری، و سایر سیستمهای خارجی است.
Dependency Rule (قاعده وابستگی):
در Clean Architecture، وابستگیها باید از لایههای بیرونی به سمت لایههای درونی باشد و لایههای درونی نباید هیچ وابستگی به لایههای بیرونی داشته باشند. این اصل باعث میشود که منطق اصلی سیستم به جزئیات پیادهسازی وابسته نباشد و قابل تست و نگهداری باشد.
جداسازی نگرانیها (Separation of Concerns):
با تقسیم سیستم به لایههای مختلف، هر لایه مسئول یک جنبه خاص از سیستم است. این جداسازی باعث میشود که تغییرات در یک بخش سیستم تأثیری بر بخشهای دیگر نداشته باشد.
تستپذیری (Testability):
با جدا کردن منطق کسبوکار از جزئیات پیادهسازی، تست واحد و تستهای یکپارچگی سادهتر میشود. میتوان به راحتی منطق کسبوکار را بدون وابستگی به پایگاهداده یا سایر سیستمهای خارجی تست کرد.
انعطافپذیری (Flexibility):
Clean Architecture امکان تغییر و جایگزینی اجزای مختلف سیستم بدون تأثیر بر سایر اجزا را فراهم میکند. این انعطافپذیری باعث میشود که سیستم به راحتی قابل توسعه و نگهداری باشد.
ساختار کلی Clean Architecture
Clean Architecture معمولاً به صورت مجموعهای از دایرههای هممرکز نمایش داده میشود که هر دایره نمایانگر یک لایه است:
دایره مرکزی (Entities): شامل موجودیتهای دامنه و منطق اصلی کسبوکار است.
دایره دوم (Use Cases): شامل موارد استفاده و منطق کاربردی است.
دایره سوم (Interface Adapters): شامل مبدلها و کنترلرها است.
دایره خارجی (Frameworks and Drivers): شامل پایگاهداده، رابطهای کاربری، و سایر سیستمهای خارجی است.
مزایای استفاده از Clean Architecture
افزایش قابلیت نگهداری:
به دلیل جداسازی نگرانیها و کاهش وابستگیها، سیستمهای مبتنی بر Clean Architecture به راحتی قابل نگهداری و توسعه هستند.
افزایش تستپذیری:
منطق کسبوکار مستقل از جزئیات پیادهسازی است و میتوان به راحتی تستهای واحد و یکپارچگی را انجام داد.
انعطافپذیری و مقیاسپذیری:
امکان تغییر و جایگزینی اجزای مختلف سیستم بدون تأثیر بر سایر اجزا فراهم است، که به انعطافپذیری و مقیاسپذیری سیستم کمک میکند.
مراحل راه اندازی Clean Architecture
منظور از راه اندازی Clean Architecture به معنای واقعی کلمه این است که یک Solution خالی در Visual Studio شروع کرده و به سمت ساختار کامل Clean Architecture پیش بروید.
۱– ایجاد پوشه حاوی پروژهها
برای آغاز راه اندازی Clean Architecture ، ابتدا باید یک پوشه Solution خالی ایجاد کنید که در نهایت حاوی همه پروژههای آینده خواهد بود.
۲– ایجاد لایه Domain
شما از هسته معماری Clean Domain نام دارد، روند راه اندازی Clean Architecture را شروع میکنید. نامی که برای این لایه درنظر میگیرید، میتواند Domain یا ترکیبی از نام پروژه و عبارت Domain باشد. شما باید به گونهای لایهها را نامگذاری کنید که با نگاهی سریع متوجه کارکرد آن لایه بشوید. داخل Solution ایجاد شده یک پروژه Dot net 8 از نوع Class Library ایجاد میکنیم. این پروژه، حاوی کلاسی به اسم دیفالت ایجاد شده Class1 هست که میتوانیم آن را پاک کنیم.
آنچه معمولاً در پروژه Domain تعریف میکنید، قوانین اصلی مربوط به کسب و کار، Enumerations ،Value Object ها، Custom Exception و چنین مواردی است. توجه کنید که در این آموزش، تمام این موارد انجام نمیشوند؛ بلکه تنها روی Setup ساختار پروژه براساس Clean Architecture تمرکز خواهد شد.
۳– ساخت لایه Application
لایه بعدی که باید تعریف کنیم Application نام دارد. برای این کار، مجدداً یک پروژه Dot net 8 و از نوع Class Library لازم است. ضمن اینکه لازم است کلاس پیشفرض حذف شود. برای درک بهتر نتیجه، به تصویر زیر توجه کنید.
اساساً، لایه Domain مجوز ارجاع داشتن به هیچ کدام از لایههای بیرونی را ندارد و این موضوع، یک قانون مهم در معماری Clean محسوب میشود. در حالی که لایه Application ، امکان برقراری ارتباط با لایه Domain را دارد. در انتهای مقاله، روشی بررسی میشود که چنین قیدهایی را برای ما در نظر بگیرد.
پروژه Application یک Orchestrator از سایر لایهها و Use Case ها تلقی میشود. این یعنی در این لایه، ماژولهای مختلف فراخوانی و مورد استفاده قرار میگیرند و هیچ منطقی مرتبط با کسب و کار تعریف نخواهد شد. همچنین، در این لایه میتوانید Service های گوناگون را فراخوانی و بهکار ببرید.
معمولاً برای ارتباط بین لایه Entry Point و Application از mediator استفاده میشود. Mediator یک Design Pattern است که بهواسطه آن، Couple-less بودن لایههای مختلف پروژه تضمین خواهد شد. اما چرا باید ماژولهای مختلف به هم وابستگی نداشته باشند؟
فرض کنید روزی تصمیم گرفته شود تا یکی از لایههای پروژه، به سرویس دیگری از یک میکروسرویس بزرگتر منتقل شود. در این شرایط، اگر وابستگیهای زیادی بین دو لایه وجود داشته باشند، جداسازی آنها دشوار است و دیگر نمیتوانید یک لایه خاص را به میکروسرویس بزرگتر منتقل کنید.
در NET. ، یک NuGet Package اسم MediatR وجود دارد که در ادامه، آن را نصب و استفاده خواهیم کرد.
dotnet add package MediatR –version 12.1.1
پس از نصب MediatR، میتوان Use Case ها را ایجاد کرد. لازم است هر Use Case، بهعنوان یک کلاس مستقلی پیادهسازی شود که از MediatR.RequestHandler<TRequest ,TResponse> ارثبری میکند. پارامتر TRequest نشاندهنده شی درخواستی خاصی است که به Use Case ارسال و پارامتر TResponse نمایانگر شی پاسخی است که از Use Case برگردانده میشود.
به منظور درک کارایی این پکیج، یک Entry Point یا API ایجاد کرده و طبق آن، یک کاربر را ثبتنام خواهیم کرد. منظور از Entry Point یا نقطه ورودی پروژه، جایی است که درخواستها در ابتدا به آن ارسال میشوند و سپس، از آن جا به سایر لایهها هدایت خواهند شد. این ورودی میتواند یک WebApi یا یک Console Application باشد. در این آموزش، گزینه اول، یعنی WebApi را انتخاب میکنیم.
۴– ایجاد لایه Presentation
لازم است لایه جدیدی به نام Presentation از .Net 8 و از نوع ASP.Net Core Web Application بسازید.
به منظور نوشتن یک API برای ثبت کاربر، کنترلر (Controller) نیاز است. برای انجام این کار، یک پوشه به نام Controllers بسازید و در داخل آن، کلاسی با نام UserController ایجاد کنید. در ادامه، یک Command ایجاد خواهیم کرد که با کمک آن، مشخصات کاربر جدید دریافت بشوند. سپس، یک کلاس Handler میسازیم که این درخواست را پردازش کرده و کاربر جدید را ایجاد کند.
بهصورت کلی، Command به فرآیندی گفته میشود که طی آن، تغییری در وضعیت سیستم بهوجود میآید. در مقابل، اگر بخواهیم از وضعیت یک سرویس یا سیستم مطلع شویم،Query استفاده میشود. لایه اپلیکیشن جایی هست که Command ها و کوئریها پیادهسازی خواهند شد.
در لایه Application، یک پوشه به اسم User ایجاد کنید و در داخل پوشه User، پوشه دیگری به نام Commands بسازید. سپس، لازم است یک کلاس به نام CreateUserCommand ایجاد شود. در تصویر زیر، نتیجه قابل مشاهده است.
هر Command ای که مربوط به کاربر باشد، درون پوشه Commands قرار میگیرد. بهعنوان مثال، حذف یا ویرایش مشخصات کاربر درون این پوشه هستند.
میتوان پوشهبندیهای مختلفی مطرح کرد. بهعنوان مثال، یک نوع مرسوم پوشهبندی در پروژهها، دارا بودن دو پوشه به نامهای Commands و Queries است؛ بهطوری که تمامی تغییرات در پوشه Commands و تمامی درخواستها در پوشه Queries نگهداری شوند. برای درک بهتر، به تصویر زیر توجه کنید.
مشکل این روش این است که اگر تعداد Command ها یا کوئریها بیش از اندازه باشند، احتمالاً امکان پیدا کردن هرکدام آنها، از میان انبوه کلاسها دشوار خواهد بود. بنابراین، میتوان این روش را در پروژههای کوچک استفاده کرد. در ادامه، از روش اول پوشهبندی استفاده خواهد شد؛ زیرا این رویکرد، برای پروژههای بزرگ کارایی مناسبی دارد.
Convention در نام گذاری Command ها و کوئری ها
در صورتی که بخواهیم یک Command برای ایجاد کاربر داشته باشیم، ابتدای نام مربوطه، تسکی که قرار است انجام دهد یعنی Create)، سپس نام فیچر (یعنی User) و درنهایت، عبارت (Command آورده میشود.
به منظور درک نحوه پیادهسازی کلاس CreateUserCommnad، به قطعه کد زیر توجه کنید.
public class CreateUserCommand : IRequest<bool>
{
public string Name { get; set; }
public string Family { get; set; }
public string Email { get; set; }
}
این کلاس از <IRequest<T ارثبری میکند که یکی از اینترفیسهای پکیج MediatR است. پارامتر T در آن نمایانگر نوع پاسخ، بعد از ایجاد کاربر است. این شی در اینجا، از نوع Boolean خواهد بود؛ یعنی، زمانی که مقدار آن True باشد، کاربر با موفقیت ثبت شده است. البته میتوان برای پروژههای مختلف مدلهای گوناگونی ایجاد کرد. در این مثال، برای سادگی در بیان، آن را بولین در نظر گرفتهایم.
در مرحله بعدی، یک هندلر تحت عنوان CreateUserCommandHandler ، برای این Command ایجاد میکنیم. محل نگهداری این کلاس، داخل پوشه Commands (زیرمجموعه پوشه User) خواهد بود. به Convention ای که برای نامگذاری کلاسهای هندلر استفاده میکنیم، دقت کنید. در این Convention، نام Command بههمراه عبارت Handler در انتها قرار داده شده است. این نوع نامگذاریها باعث میشوند تا سایر برنامهنویسان بدون صرف زمان زیادی، بتوانند کلاسهای مدنظرشان را پیدا کنند.
در ادامه، قطعه پیادهسازی هندلر مربوط به ایجاد کاربر قرار داده شده است.
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, bool>
{
public Task<bool> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
}
کلاس CreateUserCommandHandler از <IRequestHandler<TRequest, TResponse ارث بری میکند.
توجه کنید که TRequest نمایانگر نوع درخواست، TResponse نمایانگر نوع پاسخ و IRequestHandler یکی از اینترفیسهای پکیج MediatR محسوب میشوند.
در متد Handle، میتوان کاربر جدید را ایجاد و در دیتابیس مربوط به آن ذخیره کرد. اما در این مرحله، هنوز لایه مربوط به دیتابیس را ایجاد نکردهایم و فقط به بازگرداندن مقدار True اکتفا میکنیم. این امکان وجود دارد که به جای مقدار Boolean، یک GUID برگردانیم. GUID، نشاندهنده شناسه کاربر در دیتابیس است.
اکنون در این مرحله، میتوانیم به منظور ایجاد کاربر، این Handler را در یک کنترلر استفاده کنیم.
مشابه قطعهکد زیر، در لایه Presentation و کنترلر User قرار گرفته و اقدامات لازم برای ایجاد یک کاربر با استفاده از MediatR را لحاظ کنید:
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly ISender _sender;
public UserController(ISender sender)
{
_sender = sender;
}
[HttpPost]
public async Task<IActionResult> Create(string name, string family, string email)
{
var command = new CreateUserCommand()
{
Name = name,
Family = family,
Email = email
};
var response = await _sender.Send(command);
return Ok(response);
}
}
اینترفیس ISender برای پکیج MediatR است و به منظور ارسال درخواست توسط این پکیج بهکار میرود. ضمن اینکه درخواستهای ارسالشده توسط هندلرهای مرتبط با آن، دریافت و پردازش میشوند.
در این متد، ما یک درخواست CreateUserCommand را از HTTP دریافت میکنیم و آن را به MediatR میفرستیم. سپس، MediatR به جستجو برای Handler مناسب میپردازد و پس از یافتن آن، متد Handle را فراخوانی میکند. پس از ایجاد شدن کاربر جدید توسط هندلر، مقدار True در پاسخ HTTP برگردانده میشود.
این فقط یک مثال ابتدایی از نحوه پیادهسازی Use Case ها در Clean Architecture است. شما میتوانید سایر موارد را با الگوبرداری از این روش پیادهسازی کنید.
نصب و استفاده از پکیج Fluent Validation
فرض کنید باید خالی بودن مقادیر ورودی بررسی شود. محل مناسب برای پیادهسازی این منطق کجاست؟ بررسی خالی بودن مقدار یک فیلد، از موضوعات مرتبط با Business نیست. موارد مرتبط با کسب و کار، مانند تکراری بودن نام یا ایمیل، در لایه Domain پیادهسازی میشوند.
به منظور اعتبارسنجی مقدار یک فیلد، یک پکیج شناختهشده به نام Fluent Validation وجود دارد.
برای نصب Fluent Validation در لایه Application، قطعه کد زیر را تایپ کنید:
حال برای بررسی مقادیر ورودیها، در داخل پوشه Commands، یک کلاس به اسم CreateUserValidator ایجاد میکنیم که ازAbstractValidator<T> ارثبری میکند. T نمایانگر نوع شی است که اعتبارسنجی خواهد شد و AbstractValidator یکی از کلاسهای پکیج Fluent Validation برای پیادهسازی Rule های مختلف بهکار میبریم.
به مثال زیر توجه کنید:
public class CreateUserValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserValidator()
{
RuleFor(x => x.Name)
.NotNull()
.NotEmpty();
RuleFor(x => x.Family)
.NotNull()
.NotEmpty();
RuleFor(x => x.Email)
.NotNull()
.NotEmpty();
}
}
در قطعه کد بالا، این قاعده را تعیین کردهایم که مقدار Name نمیتواند Null و Empty باشد. همین قاعده درخصوص سایر پارامترها نیز بررسی شدهاند.
به ازای همه پکیجها و کلاسهای استفادهشده در این پروژه، باید Instance های مختلفی از آنها تعریف شود و در بخشهای گوناگون پروژه بهکار برده شوند. Net Core. از یک IoC درونی پشتیبانی میکند و استفاده از آن به برنامهنویس کمککننده است.
استفاده از Dependency Injection
در ادامه این مقاله راه اندازی Clean Architecture ، موضوع Dependency Injection در پروژههای Clean بررسی میشوند. بهطور کلی، لازم است یک کلاس به نام DependencyInjection در تمام لایهها ایجاد شود. هرکدام از کلاسها یک متد دارند و نام متد، ترکیبی از عبارت Add و نام لایه است.
بهعنوان مثال، کلاس DependencyInjection لایه Application، بهصورت زیر تعریف میشود:
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
}
}
کلاس مذکور یک کلاس Static است و فقط یک متد به نام AddApplication دارد. این تابع، یک Extension Method برای IServiceCollection محسوب میشود. به این ترتیب، میتوانیم تمامی Dependency Injection های مربوط به لایه Application را در این متد قرار دهیم. همین موضوع میتواند برای سایر لایهها صادق باشد. درنهایت، میتوان در کلاس Program.cs ، تمام Dependency Injection های پروژه را اضافه کرد:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplication();
builder.Services.AddPresentation();
builder.Services.AddDomain();
به این ترتیب، هر یک از Configuration های مربوط به هر لایه از هم تفکیک شدند.
برای نمونه، متد مربوط به AddApplication تکمیل شد. این متد باید امکان رجیستر کردن دو پکیج نصبشده (MediatR و FluentValidation) را داشته باشد.
پکیج FluentValidation، یک متد Extension برای Injection دارد که با کمک این اکستنشن،register متد تسهیل مییابد.
دستور زیر را برای نصب اکستنشن Dependency Injection بهکار ببرید:
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
var assembly = typeof(DependencyInjection).Assembly;
services.AddMediatR(configuration =>
configuration.RegisterServicesFromAssemblies(assembly));
services.AddValidatorsFromAssembly(assembly);
return services;
}
}
ورژنهای جدید پکیجهای MediatR و FluentValidation، به Assembly پروژه برای رجیستر کردن interface ها و پیادهسازی آنها نیاز دارند. بنابراین، ابتدا یک متغیر به نام assembly تعریف کرده و آن را مقداردهی میکنیم. حال باید این متغیر در اختیار متدهای RegisterServicesFromAssemblies و AddValidatorsFromAssembly قرار گیرد. این متدها، اینترفیسهای مشخص و از پیش تعریف شده خود را در داخل assembly داده شده جستجو میکنند. اگر کلاسی وجود داشته باشد که از این اینترفیسها ارثبری کرده باشد، آن کلاس بهعنوان پیادهسازی اینترفیس مذکور Register میشود.
مقدار بازگشتی متدهایی که برای Register کردن Dependency ها تعریف کردیم، از نوع IServiceCollection بودند.
به این ترتیب، میتوان فراخوانی آنها در کلاس Program.cs را بهصورت زیر تغییر داد:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddPresentation()
.AddApplication()
.AddPresentation()
.AddDomain();
در این بخش از راه اندازی Clean Architecture ، Dependency Injection بررسی شد. در ادامه، لایه آخر یعنی Infrastructure و اهمیت آن در راه اندازی Clean Architecture شرح داده میشود.
۵– ساخت لایه Infrastructure
در لایه آخر، تمرکز روی پیادهسازیهای مربوط به دیتابیس و ارتباط با سرویسهای خارجی است. البته میتوان پیادهسازیهای مرتبط با اتصال به دیتابیس را در لایه درونیتر، یعنی Persistence، قرار داد.
مجدداً یک Class Library با Net 8. ایجاد کرده و کلاس پیشفرض آن را حذف کنید. توجه کنید که یک کلاس به نام DependecyInjection نیز باید به آن اضافه شود. نتیجه در شکل زیر قابل مشاهده است:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddPresentation()
.AddApplication()
.AddPresentation()
.AddDomain()
.AddInfrastructure();
این یک نمونه از راه اندازی Clean Architecture در .Net 8است. در این مطلب، لایههای مختلف معماری Clean ، ازجمله لایههای Presentation ،Infrastructure ،Domain و Application قرار دارند که همگی در زمان اجرا به یکدیگر متصل و اجرا میشوند.
راه اندازی Clean Architecture مشابه مثال فوق، شروع مناسب و ساختارمندی برای معماری محسوب میشود و شما میتوانید آن را با گذر زمان و تغییر نیازمندیهای معماری تکمیل کنید. البته راه اندازی Clean Architecture الزاماً پاسخگوی تمام مشکلات سیستم شما نیست و در کنار آن، لازم است سایر فاکتورهای مؤثر نیز بررسی شوند. در سالهای اخیر، روشهایی مانند Layered Architecture وSlice Architecture نیز محبوبیت یافتهاند.
مدل تکمیلی به شکل ذیل می باشد:
وظایف و نقش Persistence در معماری Clean
ذخیرهسازی دادهها:
مدیریت عملیاتهای ذخیرهسازی دادهها در پایگاهداده یا سایر منابع داده. این شامل عملیاتهای Create، Read، Update و Delete (CRUD) میشود.
بازیابی دادهها:
بازیابی دادهها از پایگاهداده یا منابع دیگر و تبدیل آنها به اشیای دامنهای (Entities) که در لایههای داخلی استفاده میشوند.
استفاده از الگوهای Repository و DAO:
استفاده از الگوهای طراحی مثل Repository و Data Access Object (DAO) برای جداسازی منطق دسترسی به دادهها از منطق کسبوکار و کاربرد.
استفاده از ORM:
استفاده از ابزارهای ORM (Object-Relational Mapping) مثل Entity Framework در داتنت، Hibernate در جاوا یا سایر ابزارهای مشابه برای مدیریت مپینگ بین اشیای دامنه و جداول پایگاهداده.
نحوه پیادهسازی Persistence در معماری Clean
ایجاد Interfaceهای Repository در لایه Use Cases یا Domain:
تعریف Interfaceهایی که عملیاتهای ذخیرهسازی و بازیابی دادهها را تعریف میکنند. این Interfaceها باید مستقل از جزئیات پیادهسازی خاص پایگاهداده باشند.
پیادهسازی Interfaceهای Repository در لایه Persistence:
پیادهسازی این Interfaceها در لایه Persistence که شامل جزئیات پیادهسازی خاص پایگاهداده است. این پیادهسازیها میتوانند از ORM یا تکنیکهای دسترسی مستقیم به پایگاهداده استفاده کنند.
public class CustomerRepository : ICustomerRepository
{
private readonly AppDbContext _context;
public CustomerRepository(AppDbContext context)
{
_context = context;
}
public Customer GetById(int id)
{
return _context.Customers.Find(id);
}
public void Add(Customer customer)
{
_context.Customers.Add(customer);
_context.SaveChanges();
}
public void Update(Customer customer)
{
_context.Customers.Update(customer);
_context.SaveChanges();
}
public void Delete(Customer customer)
{
_context.Customers.Remove(customer);
_context.SaveChanges();
}
}
تزریق وابستگیها (Dependency Injection):
استفاده از تزریق وابستگیها برای ارسال پیادهسازیهای Repository به Use Cases. این کار باعث میشود که لایههای داخلی به پیادهسازیهای خاص پایگاهداده وابسته نباشند.
public class GetCustomerUseCase
{
private readonly ICustomerRepository _customerRepository;
public GetCustomerUseCase(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public Customer Execute(int id)
{
return _customerRepository.GetById(id);
}
}
مزایای جداسازی Persistence در معماری Clean
جداسازی نگرانیها:
با جداسازی منطق دسترسی به دادهها از منطق کسبوکار، هر لایه میتواند به طور مستقل تغییر کند و توسعه یابد.
قابلیت تستپذیری:
با تعریف Interfaceهای Repository و استفاده از Mockها یا Fakeها، میتوان به راحتی لایههای داخلی را بدون نیاز به وابستگی به پایگاهداده واقعی تست کرد.
انعطافپذیری:
امکان تغییر یا جایگزینی تکنولوژی ذخیرهسازی (مثل مهاجرت از یک پایگاهداده به دیگری) بدون تأثیر بر لایههای داخلی فراهم است.
نتیجهگیری
لایه Persistence در معماری Clean نقش مهمی در جداسازی منطق دسترسی به دادهها از منطق کسبوکار و کاربرد دارد. با استفاده از اصول معماری Clean و الگوهای طراحی مناسب، میتوان به سیستمهایی قابل نگهداری، تستپذیر و انعطافپذیر دست یافت که به راحتی قابل توسعه و تغییر باشند.
برنامه های ما امکان دارد به طور غیرمنتظره ای در پاسخ به تغییرات با مشکل روبه رو شوند. از این رو، تست خودکار بعد از تغییرات در تمام برنامه ها مورد نیاز است.
تست دستی، کندترین، کم اعتبارترین و گرانترین راه برای آزمایش یک برنامه است. اما اگر برنامه ها به گونه ای طراحی نشده اند که قابل تست باشند، تست دستی ممکن است تنها راه در دسترس ما باشد.
بنابراین اولین قدم این است که مطمئن شوید برنامه به گونه ای طراحی شده است که قابل آزمایش باشد. برنامه هایی که از اصول معماری خوب پیروی می کنند مانند تفکیک نگرانی ها (Seperation of Concerns)، وارونگی وابستگی، مسئولیت منفرد (Single Responsibility)، Don’t repeat yourself (DRY) و غیره به راحتی قابل آزمایش هستند. ASP.NET Core از واحد خودکار، یکپارچه سازی و تست عملکرد پشتیبانی می کند.
یک تست واحد به معنی تست یک قسمت واحد از منطق برنامه ما است. کد ما امکان دارد وابستگیهایی بر روی لایه های پایین ترو کامپوننتهای خارجی داشته باشد. اما آنها با تستهای واحد اعتبارسنجی نمیشوند. تست واحد به طور کامل در حافظه و در فرآیند اجرا می شود. با سیستم فایل، شبکه یا پایگاه داده ارتباط برقرار نمی کند.
تست های واحد فقط باید کد ما را تست کنند.
با توجه به این دلایل، تست های واحد باید بسیار سریع اجرا شوند و ما باید بتوانیم آنها را به طور مکرر اجرا کنیم. در حالت ایدهآل، ما باید تستهای واحد را قبل از ارسال هر تغییر به source control repository و همچنین با هر build خودکار روی build server اجرا کنیم.
راه اندازی Framework تست واحد
framework های تست زیادی در market امروزی وجود دارد. با این حال، برای این مقاله، قصد دادریم از xUnit استفاده کنیم که یک framework تست خیلی معروفی است. حتی تستهای ASP.NET Core و EF Core توسط آن نوشته شده اند.
ما میتوانیم با استفاده از xUnit Test Project template یک پروژه تستxUnit در ویژوال استودیو اضافه کنیم که در ویژوال استودیو در دسترس میباشد:
ما همیشه باید تستهای خود را به سبکی ثابت نامگذاری کنیم، با نامهایی که مشخص کند که هر تست چه کاری انجام میدهد. یک رویکرد خوب این است که کلاس ها و متدهای آزمایشی را با توجه به کلاس و متدی که در حال آزمایش هستند نامگذاری کنید. این کاملاً مشخص می کند که هر آزمایش چه مسئولیتی دارد.
ما می توانیم رفتاری را که در حال آزمایش است را به نام هر test method اضافه کنیم. این باید شامل رفتار مورد انتظار و هر ورودی یا فرضیه هایی باشد که باید این رفتار را نشان دهد.
بنابراین، هنگامی که یک یا چند تست با شکست مواجه می شوند، از نام آنها مشخص است که چه مواردی شکست خورده اند. ما زمانیکه تستهای واحد را در قسمت بعدی ایجاد میکنیم از این قواعد نام گذاری پیروی خواهیم کرد.
بنابراین یک NuGet package reference برای Moq اضافه کنیم که یک فریم ورک آبجکت ساختگی است. این امر، آبجکتهای آزمایشی که آبجکتهای ساختگی یا مجموعه ای از ویژگی ها و رفتارهای متد از پیش تعیین شده برای آزمایش هستند را در اختیار ما قرار می دهد.
تستهای واحد در متدهای کنترلر
بگوییم که یکEmployeeController با یک متد ()Index و ()Add تعریف کرده ایم:
public class EmployeeController : Controller
{
private readonly IDataRepository<Employee> _dataRepository;
public EmployeeController(IDataRepository<Employee> dataRepository)
{
_dataRepository = dataRepository;
}
public IActionResult Index()
{
IEnumerable<Employee> employees = _dataRepository.GetAll();
return View(employees);
}
[HttpPost]
public IActionResult Add(Employee employee)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_dataRepository.Add(employee);
return RedirectToAction(actionName: nameof(Index));
}
}
حالا نحوه نوشتن تستهای واحد برای این اکشن متدها را بررسی کنیم. کنترلر ما از تزریق وابستگی برای گرفتن مقدار برای dataRepository_ استفاده میکند. این باعث میشود که این امکان را برای unit testing framework فراهم کند تا یک آبجکت ساختگی را ارائه داده و متد ها را تست کند.
الگوی (Arrange, Act, Assert) AAA یک شیوه ی رایج برای نوشتن تستهای واحد است و ما همین الگو را اینجا دنبال خواهیم کرد.
قسمت Arrange یک متد تست واحد، آبجکتها را مقداردهی اولیه میکند و مقادیر را برای ورودیهایی که به متد مورد آزمایش پاس داده میشوند set میکند.
قسمت Act، متد مورد آزمایش را به همراه پارامترهای Arrange فراخوانی میکند.
قسمت Assert تأیید می کند که اکشن متد مورد آزمایش، طبق انتظار عمل می کند.
آزمایش متد ()Index
حالا تستها را برای متد ()Index بنویسیم:
[Fact]
public void Index_ReturnsAViewResult_WithAListOfEmployees()
{
// Arrange
var mockRepo = new Mock<IDataRepository<Employee>>();
mockRepo.Setup(repo => repo.GetAll())
.Returns(GetTestEmployees());
var controller = new EmployeeController(mockRepo.Object);
// Act
var result = controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<List<Employee>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
private IEnumerable<Employee> GetTestEmployees()
{
return new List<Employee>()
{
new Employee()
{
Id = 1,
Name = "John"
},
new Employee()
{
Id = 2,
Name = "Doe"
}
};
}
ما اول با استفاده از متد ()GetTestEmployees ، سرویس IDataRepository<Employee> را mock کردیم. ()GetTestEmployees یک لیست از دو آبجکت ساختگی را ایجاد کرده و برمیگرداند.
سپس متد Index() اجرا شده و موارد زیر را روی نتیجه assert میکند:
یک ViewResult برمیگرداند
نوع مدل برگشتی List<Employee> میباشد
دو آبجکت Employee در مدل برگشتی وجود دارد
هر test method با یک ویژگی [Fact] مزین شده است که نشان می دهد این یک متد واقعی است که باید توسط test runner اجرا شود.
آزمایش متد ()Add
حالا تستها را برای متد ()Add بنویسیم:
[Fact]
public void Add_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IDataRepository<Employee>>();
var controller = new EmployeeController(mockRepo.Object);
controller.ModelState.AddModelError("Name", "Required");
var newEmployee = GetTestEmployee();
// Act
var result = controller.Add(newEmployee);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public void Add_AddsEmployeeAndReturnsARedirect_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IDataRepository<Employee>>();
mockRepo.Setup(repo => repo.Add(It.IsAny<Employee>()))
.Verifiable();
var controller = new EmployeeController(mockRepo.Object);
var newEmployee = GetTestEmployee();
// Act
var result = controller.Add(newEmployee);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
private Employee GetTestEmployee()
{
return new Employee()
{
Id = 1,
Name = "John"
};
}
اولین تست تأیید می کند که زمانیکه ModelState.IsValid برابر با false است، اکشن متد یک ViewResult 400 Bad Request را با داده های مناسب برمی گرداند. میتوانیم با اضافه کردن خطاها با استفاده از متد ()AddModelError وضعیت مدل نامعتبر را آزمایش کنیم.
تست دوم تأیید میکند که وقتی ModelState.IsValid برابر با true است، متد ()Add در repository فراخوانی میشود و RedirectToActionResult با آرگومانهای صحیح بازگردانده میشود.
متدهای Mock که ما آنها را فراخوانی نمی کنیم معمولا نادیده گرفته می شوند، اما با فراخوانی ()Verifiable همراه با تنظیمات، می توانیم تایید کنیم که متدهای Mock فراخوانی شده اند. ما میتوانیم این را با استفاده از mockRepo.Verify تأیید کنیم، که اگر متد مورد انتظار فراخوانی نشده باشد، آزمایش مورد نظر شکست میخورد.
اجرای تستها
ما میتوانیم تستها را با استفاده از Test Explorer در Visual Studio اجرا کنیم:
در Test Explorer میتوانیم تمام تستهای موجود را بهصورت گروهبندی شده بر اساس Solution، Project، Class و غیره مشاهده کنیم. ما می توانیم تست ها را با اجرا یا debug کردن آن ها انتخاب و اجرا کنیم. هنگامی که تست ها را اجرا می کنیم، می توانیم یک علامت تیک سبز یا یک علامت متقاطع قرمز را در سمت چپ نام متد تست ببینیم که نشان دهنده موفقیت یا شکست در آخرین اجرای متد است. در سمت راست، میتوانیم زمان صرف شده برای اجرای هر تست را ببینیم.
SignalR یک کتابخانه در ASP.NET Core است که امکان برقراری ارتباط بلادرنگ بین سرور و کلاینتها را فراهم میکند. این کتابخانه به طور خاص برای برنامههایی طراحی شده است که نیاز به تبادل اطلاعات سریع و بهروز بین سرور و کلاینت دارند، مانند چت آنلاین، برنامههای بازی، اعلانهای بلادرنگ، و غیره.
ویژگیها و عملکرد SignalR
ارتباط بلادرنگ: SignalR از پروتکلهای مختلفی مانند WebSockets، Server-Sent Events، و Long Polling برای برقراری ارتباط بلادرنگ استفاده میکند. انتخاب پروتکل به مرورگر و سرور بستگی دارد و به صورت خودکار توسط SignalR مدیریت میشود.
پشتیبانی از هابها: SignalR از مفهومی به نام “هاب” استفاده میکند که به شما امکان میدهد تا متدهای سرور را از کلاینت صدا بزنید و بالعکس. هابها به عنوان واسطهای بین سرور و کلاینت عمل میکنند و ارتباطات بلادرنگ را سادهتر میکنند.
مدیریت اتصالات: SignalR به طور خودکار اتصالات را مدیریت میکند و امکان پیگیری کلاینتهای متصل، ارسال پیام به تمام کلاینتها یا گروههای خاصی از کلاینتها را فراهم میکند.
مقیاسپذیری: SignalR از مقیاسپذیری در سطح سرور و کلاینت پشتیبانی میکند و میتواند با استفاده از تکنولوژیهایی مانند Redis ، Azure SignalR Service، و غیره، ترافیک را مدیریت کند.
در این آموزش، مقدمات ساخت یک برنامه Real-Time با استفاده از SignalR قرار داده شده است.
در این آموزش خواهید آموخت که چگونه :
یک پروژه وب ایجاد کنید.
کتابخانه سمت کلاینت SignalR را اضافه کنید.
یک SignalR Hub ایجاد کنید.
پروژه را برای استفاده از SignalR کانفیگ کنید.
کد ارسال پیام از یک کلاینت به تمامی کلاینت های متصل را اضافه کنید.
در آخر، شما یک برنامه چت خواهید داشت :
ساخت برنامه چت با SignalR در ASP.NET Core
پیش نیاز ها
ویژوال استودیو 2019 با ابزار مختص به توسعه وب ( ASP.NET and web development workload )
نسخه 3 NET Core SDK. یا بالاتر
ساخت یک پروژه وب
از منو، File و سپس New Project را انتخاب کنید.
در پنجره Create a new project، نوع ASP.NET Core Web Application را انتخاب کرده و سپس برروی Next کلیک کنید.
در پنجره Configure your new project، نام پروژه را SignalRChat قرار داده و برروی Create کلیک کنید.
در پنجره Create a new ASP.NET Core Web Application، ورژن را برروی ASP.NET Core 3 و نوع را برروی NET Core. قرار دهید.
ساخت پروژه Web Application در Visual Studio
اضافه کردن کتابخانه سمت کلاینت SignalR
کتابخانه سمت سرور SignalR به صورت پیشفرض در فریمورک ASP.NET Core قرار دارد.
امَا کتابخانه سمت کلاینت مختص جاوا اسکریپت به صورت پیشفرض در پروژه قرار داده نمیشود.
در این آموزش، از Library Manager یا همان LibMan جهت دریافت کتابخانه سمت کلاینت از unpkg استفاده میشود.
این unpkg که یک Content Delivery Network یا همان CDN است برای ما این امکان را فراهم میکند که از هرچیزی که داخل npm یا همان Node.js Package Manager وجود دارد استفاده کنیم.
در Solution، برروی پروژه خود راست کلیک کرده و از بخش Add گزینه ی Client-Side Library را انتخاب کنید.
در پنجره Add Client-Side Library، ارائه دهنده یا همان Provider را برروی unpkg قرار دهید.
در بخش Library نیز متن زیر را قرار دهید :
@microsoft/signalr@latest
رادیو باتن را برروی Choose specific files قرار داده و در پوشه dist/browser فایل های signalr.js و signalr.min.js را علامت بزنید.
همچنین، Target Location را نیز برروی wwwroot/js/signalr قرار دهید.
افزودن کتابخانه سمت کلاینت SignalR به پروژه ASP.NET Core
لیب من ( LibMan ) به طور خودکار پوشه ی wwwroot/js/signalr را ایجاد کرده و فایل های انتخاب شده را داخل آن قرار میدهد.
ایجاد کردن یک SignalR Hub
هاب ( Hub ) کلاسی است که به عنوان یک Pipeline سطح بالا، ارتباط های سرور و کلاینت را هندل میکند.
در پوشه ی پروژه خود ( SignalRChat )، یک پوشه به نام Hubs ایجاد کنید.
در پوشه ی Hubs، یک فایل ChatHub.cs ایجاد کرده و کد زیر را داخل آن قرار دهید.
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
کلاس ChatHub از کلاس Hub ارث بری کرده است. کلاس Hub کانکشن ها، گروه ها و پیغام هارا مدیریت میکند.
متد SendMessage میتواند با یک کلاینت متصل شده ارتباط برقرار کند تا یک پیام را به تمامی کلاینت ها ارسال کند.
در ادامه آموزش، خواهد دید که چگونه با کد جاوا اسکریپت از سمت کلاینت با این متد ارتباط برقرار میکنیم.
کد SignalR به صورت Asynchronous است تا بتواند حداکثر مقیاس پذیری را ارائه دهد.
پیکربندی SignalR
سرور SignalR باید کانفیگ شده باشد تا بتواند ریکوئست های SignalR را به خود SignalR پاس دهد.
کدهای زیر را به فایل Startup.cs خود اضافه کنید :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExcepti();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/chatHub");
});
}
}
}
این تغییرات، SignalR را به سیستم های مسیریابی ( Routing ) و تزریق وابستگی ( Dependency Injection ) هسته ASP.NET Core اضافه میکند.
اضافه کردن کد سمت کلاینت SignalR
محتوای Pages/Index.cshtml را با کد زیر جایگزین کنید :
تکست باکس هایی را برای نام و متن پیام + یک دکمه ارسال را ایجاد میکند.
یک لیست با آیدی messageList برای نمایش پیام هایی که از SignalR Hub دریافت میشوند را ایجاد میکند.
شامل رفرنس کدهای جاوا اسکریپت SignalR و chat.js هست که در قدم های بعدی آن را اضافه خواهیم کرد.
در پوشه wwwroot/js، یک فایل chat.js ایجاد کرده و کد زیر را داخل آن قرار دهید :
"use strict"
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
//Disable send button until connection is established
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList")(li);
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
این کد :
یک کانکشن را ایجاد و استارت میکند.
به دکمه ی submit یک هندلر اضافه میکند که پیام را به هاب ( Hub ) ارسال کند.
به آبجکت connection یک هندلر اضافه میکند که پیام ها را از هاب ( Hub ) دریافت و آنهارا به لیست اضافه کند.
اجرای برنامه
با Ctrl + F5 برنامه بدون Debugging اجرا میشود.
لینک را از آدرس بار مرورگر خود کپی کرده و در یک مرورگر یا تب باز شده دیگر پیست کنید.
یکی از مرورگرها را انتخاب، نام و پیام خود را وارد، سپس برروی دکمه Send Message کلیک کنید.
نام و پیام در صفحه ی دیگر به سرعت نمایش داده میشود :
برنامه چت با SignalR و ASP.NET Core
اگر برنامه کار نکرد، Developer Tools مرورگر خود را باز کرده ( با F12 ) و به قسمت Console بروید. باید ارورهایی را در رابطه با کد HTML و جاوا اسکریپت خود مشاهده کنید. برای مثال، ممکن است فایل signalr.js را در یک پوشه دیگر قرار داده باشید. در این حالت به آن فایل رفرنسی داده نخواهد شد و شما ارور 404 را در Console مرورگر خود مشاهده خواهید کرد.
خطای 404 در Console مرورگر
اگر شما در مرورگر کروم خود ارور ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY را مشاهده کردید، این دستورات را در Command Prompt خود اجرا کنید تا Development Certificate شما بروز شود.
AutoMapper یک کتابخانه در داتنت است که برای نگاشت (Map) خودکار اشیاء استفاده میشود. این کتابخانه به شما اجازه میدهد تا بهطور خودکار دادهها را بین دو نوع مختلف از اشیاء انتقال دهید. این کار میتواند بسیار مفید باشد زمانی که میخواهید دادهها را از مدلهای دامنه (Domain Models) به مدلهای نمایش (View Models) یا DTO ها (Data Transfer Objects) منتقل کنید.
ابتدا باید از طریق nuget آن را به پروژه خود اضافه کنید
اگر از معماری clean architecture استفاده میکنید باید آن را به پروژه Domain خود اضافه کنید. این مقاله اگر Automapper نصب ندارین ،زمانی که پکیج بالا نصب میکنید همراهش Automapper نصب میکنه.
مرحله دوم این هست که باید بریم در فایل startup.cs و در بخش ConfigureServices اتومپر (Automapper) رو صدا بزینم.
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper();
services.AddMvc();
}
مرحله سوم اضافه کردن profile ها ست . یک profile در واقع نقشه (map) هست که میگه چه کلاسی باید به چه کلاسی تبدیل کنی ! حالا ما یک کلاس به نام DomainProfile داریم که در سازندش یک mapping تنظیم شده:
public class DomainProfile : Profile
{
public DomainProfile()
{
CreateMap<DomainUser, UserViewModel>();
}
}
وقتی application اجرا بشه Automapper از طریق کد شما تمام کلاس هایی که از کلاس Profile ارث بری شده پیدا می کند و configuration آنها را load میکند. به همین سادگی به همین خوشمزگی!
مرحله چهار استفاده از IMapper . وقتی پیکربندی (configuration) پروفایل های شما بارگذاری (load) شد. باید interface (واسط) IMapper مانند DI Framework های دیگر (Dependency Injection) تزریق (Inject) کنید. که این کار رو از طریق سازنده (constructor) انجام میدیم و به همه اشیا نقشه ها دسترسی خواهیم داشت:
public class HomeController : Controller
{
private readonly IMapper _mapper;
public HomeController(IMapper mapper)
{
_mapper = mapper;
}
public IActionResult Index()
{
//Do some work here.
//And when we want to map do something like the following.
var result = _mapper.Map<TypeIWantToMapTo>(originalObject);
}
}
دوباره بنظر میرسد کار خارق العاده ای اتفاق افتاده است. ما هیچ وقت IMapper را در ServiceCollection ثبت (register) نکردیم . اما پکیجی با نام AddAutoMapper که در بخش ConfigureServices ثبت کردیم به صورت اتوماتیک هندل میشود.
دامنه (Scopes)
آخرین چیزی که در نهایت میماند دامنه های داخل Automapper چگونه کار میکنند. پیکربندی های شما (مانند Profile های Automapper ) سینگلتون singleton هستند. به این دلیل که در اجرای برنامه فقط یکبار load میشوند.
واسط IMapper خودش Scoped هست. از نظر ASP.net ، به این معنی است که برای هر درخواست منحصر بفرد(individual) ، یک IMapper جدید ایجاد می شود اما در سرتاسر برنامه برای آن درخواست به اشتراک گذاشته می شود (همون Scope در aspnetcore DI هست) (بر عکس transient که یک درخواست منحصربفرد در سرتاسر برنامه هرزمان آن را تقاضا کند برای آن درخواست نمونه جدید میسازد).بنابراین اگر از IMapper در داخل یک controller و در داخل یک service برای یک درخواست واحد (single request) استفاده کنید ، آنها از همان یک نمونه IMapper استفاده می کنند.
اما چیزی که واقعا مهم است اینکه همه چیز از IValueResolver ، ITypeConverter یا IMemberValueResolver مشتق(derives) شده است، transient scope هستند.آنها همچنین با استفاده از .NET Core Service Collection DI ایجاد می شوند.
کد زیر را ببینید،برای resolve کردن یک mapping خاص استفاده می شود که به وسیله آن می خواهیم یک username براساس id مدل، map سراسری کنیم.برای این کار
public class UsernameResolver : IValueResolver<DomainUser, UserViewModel, string>
{
private readonly IUserRepository _userRepository;
public UsernameResolver(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public string Resolve(DomainUser source, UserViewModel destination,string destMember,
esolutionContext context)
{
return _userRepository.GetById(source.Id).Username;
}
}
از آنجا که این به عنوان نمونه transient با استفاده از .NET Core’s Service Collection ،مخزن ما (repository) نیز به درستی resolve شد و ما قادر خواهیم بود map کردن هایی را انجام دهم که بیش از map کردن سراسری مقادیر ساده باشند.
حال در گیت هاب automapper یک پکیجی را قرار داده که Automapper روی efcore سوار می کنه
این پکیج با پکیجی که قبل تر معرفی کردیم سازگار است و همچنین با DI خود netcore.
به عنوان مثال:
var services = new ServiceCollection();
services
.AddEntityFrameworkInMemoryDatabase()
.AddDbContext<DB>();
services.AddAutoMapper((serviceProvider, automapper) =>
{
automapper.AddCollectionMappers();
automapper.UseEntityFrameworkCoreModel<DB>(serviceProvider);
}, typeof(DB).Assembly);
var serviceProvider = services.BuildServiceProvider();
توجه: expressionهای تعریف شده توسط کاربر ، expression های اصلی کلید (primary key) را بازنویسی می کند.
در مورد مقایسه با یک Entity موجود برای بروزرسانی چه می توان گفت؟
پکیج Automapper.Collection.EntityFrameworkCore این کار را از طریق extension method از DbSet انجام می دهد.
ترجمه برابری بین Data Transfer Object (مخفف شدش Dto) و شی EF (همون Entity Framework) به یک expression فقط EF با استفاده از مقادیر dto به عنوان ثابت ها است.
توجه: این کار با تبدیل OrderDTO به Expression <Func <Order، bool ”و استفاده از آن برای پیدا کردن نوع تطبیق در پایگاه داده انجام می شود. همچنین می توانید شیء ها را به expression ها نیز map کنید.
قبل ار اینکه به پیاده سازی CQRS بپردازیم کمی به علت استفاده از آن میپردازیم. هدف از استفاده از الگوی CQRS (Command and Query Responsibility Segregation) کدنویسی بهینه تر در بخشهایی از پروژه که دارای پیچیدگی زیادی دارند می باشند.
قبل ار اینکه به پیاده سازی CQRS بپردازیم کمی به علت استفاده از آن میپردازیم. هدف از استفاده از الگوی CQRS (Command and Query Responsibility Segregation) کدنویسی بهینه تر در بخشهایی از پروژه که دارای پیچیدگی زیادی دارند می باشند.لذا در این بخش بصورت خیلی ساده و به دور از هرگونه توضیحات اضافه که فقط باعث سختتر شدن فهم مطالب برای شما هنرجویان میشود ، خواهیم پرداخت. در این سناریو پیاده سازی عملیات CRUD برای موجودیت Product با استفاده از CQRS و Mediator انجام خواهد شد. دقت داشته باشید هنگام پیاده سازی عملیات CRUD با استفاده از الگوی CQRS ، عملیات خواندن داده ها (Read) را درون پوشه Query قرار می دهیم و سایر دستورات ( Insert / Update یا Delete ) را درون پوشه Comman قرار می دهیم لذا در این مثال پس از ایجاد پروژه یک پوشه به نام CQRS ایجاد میکنیم و سپس درون آن پوشه ای به نام ProductCommandQuery و درون آن دو پوشه به نامهای Command و Query ایجاد میکنیم. در این مثال موجودیت Product با صفات زیر را در نظر بگیرید.
public class Product{
[Key]
public int Id { get; set; }
public string ProductName { get; set; }
public long Price { get; set; }
}
و کلاس Context را بصورت زیر در نظر میگیریم.
public class EshopDbContext : DbContext{
public EshopDbContext(DbContextOptions options):base(options)
{
}
public DbSet<Product> Products =>Set<Product>();
}
}
نکته : تنظیمات رشته اتصال درون app.setting و عملیات Migration انجام شود. برای فراخوانیهای سرویسهای درون CQRS از یک واسط به نام Mediator استفاده میکنیم لذا در این قسمت از برنامه از طریق nuget کتابخانه مورد نظر را به پروژه اضافه میکنیم.
MediatR.Extensions.Microsoft.DependencyInjection
پیاده سازی الگوی ریپازیتوری(Repository) : درون پوشه ای به نام Repositositories کلاسی به نام IRepository ایجاد میکنیم و اینترفیس IProductRepository را بصورت زیر ویرایش میکنیم.
البته بر اساس نیاز میتوانید امضای متدهای بیشتری را درون آن قرار دهید. سپس درون پوشه Repositories کلاس ProductRepositories را بصورت زیر درج میکنیم.
internal class ProductRepositories : IProductRepository
{
private readonly EshopDbContext context;
public ProductRepositories(EshopDbContext context)
{
this.context = context;
}
public async Task<Product> GetAsync(int id)
{
return await context.Products.FindAsync(id);
}
public Task<List<Product>> GetAllAsync()
{
throw new NotImplementedException();
}
public async Task<int> InsertAsync(Product product)
{
await context.AddAsync(product);
return product.Id;
}
}
پیاده سازی UnitOfwork :
جهت جدا سازی عملیات ذخیره سازی داده ها از الگوی Repository باید از مفهوم UnitOfWork استفاده نماییم. برای پیاده سازی آن ابتدا یک اینترفیس به نام IUnitOfwork بصورت زیر ایجاده میکنیم.
public interface IUnitOfWork:IDisposable
{
Task<int> SaveChangesAsync();
}
سپس کلاس UnitOfwork را بصورت زیر درج میکنیم.
public class UnitOfWork : IUnitOfWork
{
private readonly EshopDbContext context;
public UnitOfWork(EshopDbContext context)
{
this.context = context;
}
public void Dispose()
{
context.Dispose();
}
public async Task<int> SaveChangesAsync()
{
return await context.SaveChangesAsync();
}
}
برای پیاده سازی عملیات ذخیره سازی داده با استفاده از CQRS کلاسی به نام SaveProductCommand درون پوشه Command ایجاد میکنیم.
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Application.CQRS.ProductCommandQuery.Command;
public class SaveProductCommand : IRequest<SaveProductCommandResponse>
{
public string ProductName { get; set; }
public int CategoryId { get; set; }
public long Price { get; set; }
public string Description { get; set; }
}
public class SaveProductCommandResponse
{
public int ProductId { get; set; }
}
public class SaveProductCommandHandler : IRequestHandler<SaveProductCommand, SaveProductCommandResponse>
{
private readonly IProductRepository repository;
private readonly IUnitOfWork unitOfWork;
public SaveProductCommandHandler(IProductRepository repository,IUnitOfWork unitOfWork)
{
this.repository = repository;
this.unitOfWork = unitOfWork;
}
public async Task<SaveProductCommandResponse> Handle(SaveProductCommand request, CancellationToken cancellationToken)
{
var product = new Product
{
ProductName = request.ProductName,
Price= request.Price
};
await repository.InsertAsync(product);
await unitOfWork.SaveChangesAsync();
var response = new SaveProductCommandResponse
{
ProductId = product.Id
};
return response;
}
کلاس SaveProductCommand برای دریافت داده ها از ورودی مورد استفاده قرار میگیرد لذا باید از این کلاس جهت پر کردن مقادیر ورودی استفاده کنیم. کلاس SaveProductCommandResponse جهت بازگرداندن خروجی مورد استفاده قرار میگیرد لذا شما بر اساس نیاز پروژه فیلدهای مورد نظر را تعریف نمایید. کلاس SaveProductCommandHandler جهت اجرای دستورات با استفاده از mediator مورد استفاده قرار میگیرد. که با فراخوانی متد Handle آن دستورات اجرا شده و داده ها درون دیتابیس ذخیره میشود. اکنون یک Api به نام ProductController جهت استفاد و اجرای برنامه بصورت زیر ایجاد میکنیم.
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly IMediator mediator;
public ProductCQRSController(IMediator mediator)
{
this.mediator = mediator;
}
[HttpPost]
public async Task <IActionResult> Create(SaveProductCommand saveProductCommand)
{
var result=await mediator.Send(saveProductCommand);
return Ok(result);
}
}
دقت نمایید با استفاده از mediator سرویس مورد نظر را درون متد Send اجرا میکنیم.