IBPMS یک سیستم یکپارچه مدیریت فرآیندهای کسب و کار است که امکانات پیشرفتهای برای مدیریت و بهبود فرآیندهای سازمانی ارائه میدهد. این سیستم امکان ساخت و ویرایش فرآیندها،فرم ها،گزارش ها،سیستم مانیتورینگ قدرتمند، مدیریت صفحات، کاربران و سازمانها را با استفاده از رابطهای گرافیکی فراهم میکند
برای اینکه بتوانیم در هر سیستم عاملی پروژه های ASP.net Core با استفاده از Docker انتشار دهیم می بایست مراحل زیر را دنبال نماییم.
برای شروع باید ابتدا یک پروژه ی ASP.net Core ایجاد میکنیم.
پس از نصب SDK نرم افزار Visual Studio را باز کنید. با صفحه شروع روبرو می شوید برروی گزینه ی Create a new project که در تصویر زیر مشخص است کلیک کنید:
سپس در صفحه بعد گزینه ی ASP.net Core Web Application را همانند تصویر زیر جستجو و انتخاب میکنیم.
پس از آن در صفحه بعد مشخصات کلی پروژه خود یعنی نام، محل ذخیره سازی را وارد کرده و روی دکمه ی Create کلیک کنید.
در صفحه بعد، در بالای صفحه نسخه ی ASP.NET Core مورد نظر خود را انتخاب و از لیست نشان داده شده همانند تصویر زیر گزینه ی API را انتخاب کنید.
توجه نمایید که دو گزینه ی Configure for HTTPS و Enable Docker Support غیر فعال باشد زیرا نمی خواهیم از این قابلیت ها استفاده کنیم. سپس روی دکمه ی Create کلیک کنید.
پس از انجام مراحل فوق، فایل های اولیه ی پروژه ی ASP.net Core شما ایجاد می شود. ساختار فایل ها و پوشه ها را می توانید در بخش Solution Explorer همانند تصویر زیر مشاهده کنید.
به صورت مختصر فایل ها و پوشه های مهم عبارتند از:
Program.cs : داخل این فایل، تابع اصلی یعنی main برنامه قرار دارد و اولین تابعی است که پس از شروع پروژه، اجرا می شود. در این فایل عموما تنظیماتی که به ندرت تغییر می کنند و مربوط به کلیات پروژه هستند نظیر تنظیمات Logها قرار می گیرد.
Startup.cs : داخل این فایل کلاس Startup قرار دارد. ما با این فایل در فرآیند توسعه پروژه های ASP.net Core خود خیلی کار خواهیم داشت. پروژه های ASP.net Core مبتنی بر سرویس و میان افزار هستند که همه ی آن ها در کلاس Startup تعریف می شوند.
Controllers : درون این پوشه کنترلرهای برنامه ما قرار دارد. همانطور که اطلاع دارید ASP.net Core از معماری MVC برای توسعه برنامه ها استفاده می کند.
نصب Docker روی سیستم عامل
حال Docker را روی سیستم عامل خود نصب کنید.
ایجاد Dockerfile
خوب حالا نیاز به یک فایل با عنوان Dockerfile بدون هیچ پسوند و پیشوندی دارید. این فایل را در پوشه پروژه ی خود ایجاد کنید. Dockerfile به منظور ایجاد Imageهای docker مورد استفاده قرار می گیرد و حاوی دستورالعمل ها و مراحل ایجاد Image دلخواه است.
Imageهای docker عملا نسخه های سبک شده ای از سیستم عامل های مختلف به همراه ملزمات دیگری هستند که برای اهداف خاص استفاده می شوند. برای مثال یک image داکر برای راه اندازی دیتابیس mysql وجود دارد که حاوی یک نسخه ی سبک شده از سیستم عامل لینوکس به همراه دیتابیس mysql که روی آن نصب شده است می باشد.
ما در این آموزش فرض می کنیم نام پروژه ی شما (همانند source code ارائه شده برای این آموزش DockerASPnetCore است. اکنون دستورالعمل های زیر را در Dockerfile که ایجاد کردید قرار دهید:
FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch
RUN mkdir /app
WORKDIR /app
COPY DockerASPnetCore.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o out
EXPOSE 5000/tcp
CMD ["dotnet", "out/DockerASPnetCore.dll"]
در این دستورالعمل ها ما از Image پایه ی mcr.microsoft.com/dotnet/core/sdk:2.2-stretch استفاده کردیم که مربوط بهASP.net Core 2.2 است. اگر از نسخه ی دیگری از ASP.net Core استفاده می کنید لطفا Image مناسب را انتخاب و در فایل Dockerfile خود جایگیزین کنید. حجم این Image تقریبا 1.7GB است پس لطفا برای pull شدن آن کمی صبر کنید.
در قدم بعد با استفاده از دستور
RUN mkdir /app
یک پوشه با نام app درون Image مورد نظر ایجاد می شود. سپس با دستور
WORKDIR /app
پوشه ی جاری را به app تغییر می دهد. آنگاه فایل DockerASPnetCore.csproj پروژه ی ما را با دستور بعدی در Image کپی می کند. پس از آن با اجرای دستور
RUN dotnet restore
ملزومات لازم برای پروژه ما از Nuget گرفته می شود.
در قدم بعد نیز کل فایل های پروژه ما درون پوشه ی app در ایمیج کپی می شود و پس از آن با اجرای دستور
RUN dotnet publish -c Release -o out
پروژه ما build شده و در پوشه ی out منتشر می شود.
سپس با اجرای دستور
EXPOSE 5000/tcp
امکان ارتباط پورت 5000 مربوط به Image با بیرون وجود خواهد داشت. و در نهایت با اجرای فایل out/DockerASPnetCore.dll توسط فرمان dotnet که به محض ایجاد یک container از روی image اجرا می شود پروژه ی ما را اجرا می کند.
ایجاد Docker Image
در قدم بعد باید از روی Dockerfile یک Image داکر ایجاد کنیم. بدین منظور دستور زیر را در خط فرمان در حالی که در پوشه ی پروژه ی خود هستید اجرا کنید:
docker build -t aspnet-app:2.2 .
به جای aspnet-app:2.2 می توانید از هرنام و tag دیگری برای نام image خود استفاده کنید. پس از Image مورد نظر توسط داکر ایجاد شده و تمامی فرامین شما در Dockerfile به ترتیب اجرا می شود. اکنون می توانید با اجرای فرمان docker images در خط فرمان لیست imageهای خود را مشاهده کنید.
ایجاد Container و اجرای پروژه
اکنون که Image شما ایجاد شد، می توانید با اجرای دستور زیر در خط فرمان یک Container از روی آن ایجاد کرده و پروژه ی خود را اجرا کنید:
docker run -d -t -p 80:5000 --name app aspnet-app:2.2
در صورتی که پروژه ی شما به صورت پیش فرض روی پورتی به جز 80 در حال اجراست لطفا مقدار 80 را به شماره پورت پروژه ی خود تغییر دهید. همچنین به جای app می توانید از هر نام دیگری برای container خود استفاده کنید.
نکته: در برخی موارد نیاز است برای اجرای برخی از دستورات docker خط فرمان خود را به صورت Run asadminstrator باز کنید کاربران linux نیاز به استفاده از دستور sudo دارند
برای مشاهده ی log پروژه ی خود می توانید دستور docker logs app را در خط فرمان اجرا کنید. اکنون پروژه در حال اجراست. با مراجعه به آدرسhttp://localhost:5000/api/Values از صحت اجرای پروژه اطمینان حاصل کنید.
C# یک زبان برنامهنویسی مدرن، شیءگرا و چندمنظوره است که توسط مایکروسافت به عنوان بخشی از .NET Framework توسعه داده شده است. این زبان تحت هدایت آندرس هجلزبرگ (Anders Hejlsberg) ایجاد شد و اولین بار در سال 2000 معرفی شد. هدف از ایجاد C# ارائه زبانی قدرتمند، انعطافپذیر و ایمن برای توسعهدهندگان بود که بتوانند برنامههای کاربردی مدرن را به راحتی و با کارایی بالا توسعه دهند.
فلسفه به وجود آمدن C#
C# با اهداف و فلسفههای خاصی طراحی و توسعه یافت:
سادگی و راحتی استفاده: یکی از اهداف اصلی C# سادهسازی فرآیند توسعه نرمافزار بود. این زبان سعی کرده است که از پیچیدگیهای موجود در برخی زبانهای دیگر کاسته و ویژگیهایی ارائه دهد که توسعهدهندگان را در نوشتن کدهای خوانا و نگهداری آسانتر یاری کند.
ایمنی و امنیت: C# به شدت بر ایمنی نوع (type safety) تاکید دارد تا از خطاهای زمان اجرا و آسیبپذیریهای امنیتی جلوگیری کند. ویژگیهایی مانند مدیریت خودکار حافظه (garbage collection) و بررسیهای زمان کامپایل (compile-time checks) این اهداف را محقق میکنند.
کارایی و عملکرد: C# طراحی شده است تا بهینهسازیهای مختلفی را فراهم کند که باعث افزایش عملکرد برنامهها میشوند. این زبان به گونهای طراحی شده که توسعهدهندگان بتوانند از تمامی امکانات سختافزاری موجود بهرهبرداری کنند.
مدرنیته و انعطافپذیری: با توجه به تغییرات سریع تکنولوژی و نیازهای توسعهدهندگان، C# به گونهای طراحی شده که به راحتی قابلیت ارتقاء و افزودن ویژگیهای جدید را داشته باشد.
پشتیبانی از توسعه تحت پلتفرمهای مختلف: با معرفی .NET Core و سپس .NET 5 و نسخههای بعدی، C# قابلیت اجرای برنامهها روی سیستمعاملهای مختلف مانند ویندوز، لینوکس و macOS را دارا شده است.
آخرین نسخه حال حاضر C#
تا تاریخ دانش من (جولای 2023)، آخرین نسخه رسمی C#، نسخه 11 است. البته، برای اطلاع از آخرین نسخه دقیق، بهتر است به منابع رسمی مایکروسافت مراجعه کنید.
ویژگیهای کلیدی آخرین نسخه C#
ویژگیهای زیر نمونههایی از قابلیتهای جدیدی هستند که در نسخههای اخیر C# اضافه شدهاند:
Records: نوع جدیدی برای تعریف کلاسهای دادهمحور (immutable data classes).
public record Person(string FirstName, string LastName);
Top-level statements: نوشتن برنامههای سادهتر بدون نیاز به تعریف صریح کلاس و متد Main.
Console.WriteLine("Hello, World!");
Pattern matching enhancements: بهبود قابلیتهای تطابق الگو، شامل switch expressions و property patterns.
if (person is { FirstName: "John", LastName: "Doe" })
{
// Do something
}
Nullable reference types: قابلیت جدید برای جلوگیری از null reference exceptions با استفاده از نشانهگذاری نالپذیر.
string? nullableString = null;
Async streams: پشتیبانی از بازگشت دادهها به صورت غیرهمزمان با استفاده از IAsyncEnumerable<T>.
await foreach (var item in GetItemsAsync())
{
Console.WriteLine(item);
}
Init-only properties: امکان تعریف ویژگیهای فقط برای مقداردهی اولیه.
public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Record structs: نوعی struct که ویژگیهای مشابهی با records دارند.
public readonly record struct Point(int X, int Y);
Enhanced interpolated strings: بهبود در رشتههای interpolated برای انجام عملیاتهای پیچیدهتر.
var name = "World";
var message = $"Hello, {name.ToUpper()}!";
فرق Tracking و AsNoTracking در Entity Framework (EF) :
در Entity Framework (EF)، دو روش اصلی برای بازیابی دادهها از پایگاه داده وجود دارد: Tracking و AsNoTracking. این دو روش تاثیر مهمی بر عملکرد و رفتار پرس و جوها دارند. تفاوتهای کلیدی بین این دو روش عبارتند از:
Tracking (ردیابی)
عملکرد: وقتی از ردیابی استفاده میکنید، Entity Framework موجودیتهای بازیابی شده از پایگاه داده را در ChangeTracker ردیابی میکند. این به این معنی است که هر تغییری که در این موجودیتها انجام شود، توسط EF شناسایی شده و هنگام ذخیره تغییرات (SaveChanges) به طور خودکار به پایگاه داده اعمال میشود.
مزایا:
قابلیت پیگیری تغییرات: اگر میخواهید تغییراتی که روی موجودیتها انجام میشود را ردیابی و در نهایت به پایگاه داده اعمال کنید، ردیابی مناسب است.
همگامسازی خودکار: EF به طور خودکار تغییرات را به پایگاه داده اعمال میکند.
معایب:
عملکرد پایینتر: ردیابی موجودیتها نیازمند حافظه و پردازش بیشتری است که میتواند در پرس و جوهای بزرگ یا پیچیده منجر به افت عملکرد شود.
AsNoTracking (بدون ردیابی)
عملکرد: وقتی از AsNoTracking استفاده میکنید، EF موجودیتهای بازیابی شده از پایگاه داده را در ChangeTracker ردیابی نمیکند. این باعث میشود که پرس و جوها سریعتر اجرا شوند و حافظه کمتری مصرف کنند.
مزایا:
عملکرد بهتر: چون EF موجودیتها را ردیابی نمیکند، پرس و جوها سریعتر اجرا میشوند و منابع کمتری مصرف میشود.
مناسب برای سناریوهای فقط خواندنی: اگر تنها به خواندن دادهها نیاز دارید و نمیخواهید آنها را به روزرسانی کنید، AsNoTracking انتخاب بهتری است.
معایب:
عدم پیگیری تغییرات: تغییراتی که روی موجودیتها انجام میشود توسط EF شناسایی نمیشود و باید به صورت دستی مدیریت شوند.
نیاز به مدیریت دستی: اگر بعداً تصمیم به اعمال تغییرات داشتید، باید موجودیتها را مجدداً به ChangeTracker اضافه کنید.
مثال
در اینجا مثالی از استفاده از هر دو روش آورده شده است:
Tracking
using (var context = new MyDbContext())
{
var customers = context.Customers.ToList(); // By default, tracking is enabled
var customer = customers.First();
customer.Name = "Updated Name";
context.SaveChanges(); // Changes will be saved to the database
}
AsNoTracking
csharpCopy codeusing (var context = new MyDbContext())
{
var customers = context.Customers.AsNoTracking().ToList(); // No tracking
var customer = customers.First();
customer.Name = "Updated Name";
// Changes will not be saved to the database automatically
}
نکات نهایی
استفاده از AsNoTracking برای سناریوهای فقط خواندنی (read-only) بسیار مناسب است زیرا عملکرد را بهبود میبخشد.
در صورت نیاز به ردیابی و اعمال تغییرات به پایگاه داده، باید از ردیابی استفاده کنید.
Entity Framework Core به طور پیشفرض از ردیابی Tracking استفاده میکند مگر اینکه به طور صریح از AsNoTrackingاستفاده کنید.
تفاوت بین IQueryable و IEnumerable :
در اصل متد IQueryable از متد IEnumerable ارث بری میکنه و طبیعتا تمام ویژگی های آن را دارد اما در اصل IQueryable سمت SQL ریکوئستی را ارسال نمیکند و زمانی که دستور ()ToList را بهش میدهیم میره و اطلاعات را برای ما از سمت SQL فراخوانی میکنه به گ.نه ای که میتوان بارها با دستور where کوئری را مفصل تر نمود و در نهایت بعد از زدن ()ToList میره و اطلاعات را از SQL میاره ولی IEnumerable پس از هر بار where زدن می بایست دستور ()ToList را نوشت و هر بار دیتا را برای ما از سمت sql میاره
توضیحات تکمیلی:
متد زیر را که یکی از اشتباهات رایج حین استفاده از LINQ خصوصا جهت Binding اطلاعات است، در نظر بگیرید:
IQueryable<Customer> GetCustomers()
این متد در حقیقت هیچ چیزی را Get نمیکند! نام اصلی آن GetQueryableCustomers و یا GetQueryObjectForCustomers است. IQueryable قلب LINQ است و تنها بیانگر یک عبارت (expression) از رکوردهایی میباشد که مد نظر شما است و نه بیشتر.
برای مثال زمانیکه یک IQueryable را همانند مثال فوق فیلتر میکنید نیز هنوز چیزی از بانک اطلاعاتی یا منبع دادهای دریافت نشده است. هنوز هیچ اتفاقی رخ نداده است و هنوز رفت و برگشتی به منبع دادهای صورت نگرفته است. به آن باید به شکل یک expression builder نگاه کرد و نه لیستی از اشیاء فیلتر شدهی ما. به این مفهوم، deferred execution (اجرای به تاخیر افتاده) نیز گفته میشود (باید دقت داشت که IQueryable هم یک نوع IEnumerable است به علاوه expression trees که مهمترین وجه تمایز آن نیز میباشد. برای مثال در عبارت زیر تنها در زمانیکه متد ToList فراخوانی میشود، کل عبارت LINQ ساخته شده، به عبارت SQL متناظر با آن ترجمه شده، اطلاعات از دیتابیس اخذ گردیده و حاصل به صورت یک لیست بازگشت داده میشود:
IList<Competitor> competitorRecords = competitorRepository
.Competitors
.Where(m => !m.Deleted)
.OrderBy(m => m.countryId)
.ToList(); //فقط اینجا است که اس کیوال نهایی تولید میشود
دو سطر فوق به این معنا است: لطفا ابتدا به بانک اطلاعاتی رجوع کن و تمام رکوردهای محصولات موجود را بازگشت بده. سپس بر روی این حجم بالای اطلاعات، محصولاتی را که قیمت بالای 25 دارند، فیلتر کن.
اگر همین دو سطر را با IQueryable بازنویسی کنیم چطور؟
در سطر اول تنها یک عبارت LINQ ساخته شده است و بس. در سطر دوم نیز به همین صورت. در طی این دو سطر حتی یک رفت و برگشت به بانک اطلاعاتی صورت نخواهد گرفت. در ادامه اگر این اطلاعات به نحوی Select شوند (یا ToList فراخوانی شود، یا در طی یک حلقه برای مثال Iteration ایی روی این حاصل صورت گیرد یا موارد مشابه دیگر)، آنگاه کوئری SQL متناظر با عبارت LINQ فوق ساخته شده و بر روی بانک اطلاعاتی اجرا خواهد شد. بدیهی است این روش منابع کمتری را نسبت به حالتی که تمام اطلاعات ابتدا دریافت شده و سپس فیلتر میشوند، مصرف میکند (حالت بازگشت تمام اطلاعات ممکن است شامل 20000 رکورد باشد، اما حالت دوم شاید فقط 5 رکورد را بازگشت دهد).
سوال: پس IQueryable بسیار عالی است و از این پس کلا از IEnumerable ها دیگر نباید استفاده کرد؟ خیر! توصیه اکید طراحان این است که لطفا تا حد امکان متدهایی که IQueryable بازگشت میدهند ایجاد نکنید! IQueryable یعنی اینکه این نقطهی آغازین کوئری در اختیار شما، بعد برو هر کاری که دوست داشتی با آن در طی لایههای مختلف انجام بده و هر زمانیکه دوست داشتی از آن یک خروجی تهیه کن. خروجی IQueryable به معنای مشخص نبودن زمان اجرای نهایی کوئری و همچنین مبهم بودن نحوهی استفاده از آن است. به همین جهت متدهایی را طراحی کنید که IEnumerable بازگشت میدهند اما در بدنهی آنها به نحو صحیح و مطلوبی از IQueryable استفاده شده است. به این صورت حد و مرز یک متد کاملا مشخص میشود. متدی که واقعا همان فیلتر کردن محصولات را انجام میدهد، همان 5 رکورد را بازگشت خواهد داد؛ اما با استفاده از یک لیست یا یک IEnumerable و نه یک IQueryable که پس از فراخوانی متد نیز به هر نحو دلخواهی قابل تغییر است.
Left Join در Entity Framework با استفاده از LINQ :
برای انجام یک عملیات Left Join در Entity Framework (EF)، میتوانید از روشهای مختلفی استفاده کنید. یکی از رایجترین روشها استفاده از LINQ (Language Integrated Query) است. در ادامه، مثالی از نحوه اجرای یک Left Join در EF با استفاده از LINQ آمده است.
فرض کنید دو جدول Customersو Ordersداریم و میخواهیم یک Left Join بین این دو جدول انجام دهیم تا تمام مشتریان و سفارشهای آنها (در صورت وجود) را به دست آوریم.
تعریف کلاسهای مدل
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public ICollection<Order> Orders { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
public string ProductName { get; set; }
public Customer Customer { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
}
اجرای Left Join با استفاده از LINQ
در این مثال، یک Left Join بین Customers و Orders انجام میدهیم تا تمامی مشتریان و سفارشهای آنها (در صورت وجود) را بازیابی کنیم.
using (var context = new MyDbContext())
{
var query = from customer in context.Customers
join order in context.Orders
on customer.CustomerId equals order.CustomerId into customerOrders
from order in customerOrders.DefaultIfEmpty()
select new
{
CustomerId = customer.CustomerId,
CustomerName = customer.Name,
OrderId = order != null ? order.OrderId : (int?)null,
ProductName = order != null ? order.ProductName : null
};
var result = query.ToList();
foreach (var item in result)
{
Console.WriteLine($"Customer: {item.CustomerName}, Order ID: {item.OrderId}, Product: {item.ProductName}");
}
}
توضیح کد
تعریف کوئری:
از عبارت from و join برای تعریف جوین بین Customers و Orders استفاده میکنیم.
از into customerOrders برای تعریف گروه جوین استفاده میکنیم.
از from order in customerOrders.DefaultIfEmpty() برای انجام Left Join استفاده میکنیم. DefaultIfEmpty() اطمینان حاصل میکند که اگر هیچ سفارشی برای یک مشتری وجود نداشته باشد، مقدار order برابر با null خواهد بود.
انتخاب دادهها:
در قسمت select، اطلاعات مشتری و سفارش (در صورت وجود) را انتخاب میکنیم.
اگر سفارشی وجود نداشته باشد، مقادیر مربوط به سفارش برابر با null خواهند بود.
اجرا و نمایش نتیجه:
کوئری با استفاده از ToList() اجرا میشود.
نتیجه کوئری چاپ میشود.
این روش سادهترین و رایجترین روش برای انجام Left Join در Entity Framework با استفاده از LINQ است.
Protected Intenal : داخل آن کلاس، زیرکلاس هایش و assembly
Access Modifiers سطح مشخصی از مجوز برای دسترسی به خصوصیات و متدها می باشد. با اعلام تغییر دسترسی یک متغیر تعریف می کنیم یا حتی می توانیم از assembly به داخل کلاس دسترسی داشت.
Public
با استفاده از public یک رویداد یا متغیر می تواند از خارج از کلاس قابل دستیابی باشد. جایی که متعلق به آن است. و همینطور خارج از assembly
class ClassTest
{
//Public method
public void MethodPublic()
{
// defination of MethodPublic
}
}
// to access the method
class Program
{
static void Main(string[] args)
{
ClassTest objClassTest = new ClassTest();
objClassTest.MethodPublic(); // valid code to access.
}
}
Private
استفاده از متدها و متغیرها را تنها در کلاس خودش محدود می کند و خارج از کلاس نمی تواند استفاده شود. همانطور که یک سازنده private از یک کلاس را اعلان می کنید. که نمی تواند خارج از کلاس قابل استفاده باشد. و نمی توان از آن کلاس یک شی ایجاد کنید.
مثالی از کلیدواژه Private
class ClassTest
{
//Private method
private void MethodPrivate()
{
// defination of MethodPrivate
}
}
// to access the method
class Program
{
static void Main(string[] args)
{
ClassTest objClassTest = new ClassTest();
objClassTest.MethodPrivate(); // invalid code to access.
}
}
مثالی از private Constructor
class ClassTest
{
private ClassTest() { } // private constructor
}
// to access the method
class Program
{
static void Main(string[] args)
{
// invalid code. can't create an object of this class
ClassTest objClassTest = new ClassTest();
}
}
Protected
protected اجازه دسترسی متغیرها و متدها از کلاس و زیرکلاس را می دهد. بدین معنا که متدها می توانند از داخل کلاس و کلاسی که از آن ارث بری کرده است قابل دسترسی باشند.
class ClassTest
{
//Protected variable
protected int _a;
}
class ClassTest2 : ClassTest
{
ClassTest2()
{
this._a = 10; // can access from this class
}
}
class ClassTest3
{
ClassTest3()
{
this._a = 10; // can't access from this class
}
}
Internal
internal در #C معرفی شده است. در جاوا ما این سطح دسترسی را نداریم. این دستیابی بعد از Protected می باشد. protected اجازه دستیابی از کلاس و زیرکلاس را می دهد. assembly را به آن اضافه می کند. بدین معنا که متغیرها ومتدها می توانند درون assemly جایی که کلاس به آن تعلق دارد قابل دسترسی باشند. assembly و NameSpace اندکی متفاوت می باشند. یک assembly می تواند بیش از یک namespace را نگهداری کند. assemblyها در واقع Dllهای یک پروژه می باشند.
class ClassTest
{
internal void MethodInternal()
{
// do your code
}
}
// to access the method
class Program
{
static void Main(string[] args)
{
ClassTest objClassTest = new ClassTest();
objClassTest.MethodInternal(); // valid code to access.
}
}
Protected Internal
اجازه دسترسی از کلاس و زیرکلاس را می دهد. هچنین اجازه دسترسی از همان assembly را می دهد. بدین معنا که در protected اگر کلاس ارث بری کرده باشد، super class و متد یا متغیر protected می باشند، سپس دسترسی assembly مهم نیست. اما در Internal اگر کلاس ارث بری کرده باشد assembly مهم می باشد. در زیر می بینید که چرا از Protected Internal استفاده می کنیم.
class ClassTest
{
protected internal string name; // protected internal
public void print()
{
Console.WriteLine("\nMy name is " + name);
}
}
// to access the method
class Program
{
static void Main(string[] args)
{
ClassTest objClassTest = new ClassTest();
// Accepting value in protected internal variable
objClassTest.name = "Arka";
objClassTest.print();
}
}
معرفی مفاهیم 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 شما بروز شود.