معماری وب اپلیکیشن ها با استفاده از دیزاین پترن
در این مقاله مجموعه ای از دیزاین پترن ها را که امروزه در بیشتر وب اپلیکیشن ها استفاده می شود را بررسی می کنیم :
در حالت کلی دو دسته ی اصلی از وب اپلیکیشن ها بر اساس اینکه فایل نهایی HTML را در مرورگر تولید می کنند موجود هستند :
رندر در سمت سرور یا Server-side rendered : در این حالت عملیات در سرور صورت می گیرد . فایل نهایی Html در قالب یک پاسخ یا Network response به مرورگر ارسال می شود .
رندر در سمت کلاینت یا Client-side rendered : این حالت که به برنامه های SPAs یا همان single page application ها معروف اند ، در مرورگر ها صورت می گیرند .
در این مقاله راجع به رندر در سمت سرور صحبت شده . اصطلاح Server-side rendering گاهی اوقات در ارتباط با فریمورک های SPA که بر پایه جاوااسکریپت نیز هستند استفاده می شود ، این دو اصطلاح را با هم اشتباه نگیرید . ویژگی Server-side Rendering (SSR) برنامه های تک صفحه ای جاوااسکریپتی به این معنی هست که فقط در اولین درخواست به سمت سرور ، صفحه مورد نظر در آنجا رندر می شود و در قالب نهایی خود به مرورگر ارسال می شود . در اینجا برنامه تک صفحه ای به اصطلاح بروز می شود ، به این معنی که هر گونه تعامل بیشتر با این صفحه در سمت کلاینت انجام می شود و از اینجا به بعد فقط داده ها از سمت سرور مجددا درخواست می شوند .
این فقط برای مقدمه ای کوتاه بود و ما نمی خواهیم که نحوه کارکرد این گونه وب اپلیکیشن ها را بررسی نماییم .
جدا کردن کد برنامه از UI
اکثر وب اپ هایی که در سمت سرور رندر می شوند امروزه از معماری MVC (model – view - controller) برای جدا کردن UI از سایر قسمت ها تبعیت می کنند . MVC همانطور که از نامش پیداست ، سه قسمت اصلی در این معماری وجود دارد.
- Model ها وضعیت فعلی برنامه را مشخص می کنند و به عنوان ارتباط بین View ها و Controller ها استفاده می شوند .
- View ها UI برنامه ما هستند . داده های ارائه شده از سمت مدل ها رو می خوانند و از آنجایی که صفحات وب هستند ، تعامل با آنها می توانددرخواست های جدیدی را به سمت سرور ارسال کند .
- Controller ها درخواست های ارسال شده به سمت سرور را مدیریت می کنند ، به طور مثال زمانی که کاربران آدرس جدیدی را درخواست می کنند یا مثلا با صفحه ای که از قبل رندر شده کار می کنند و در نهایت این کنترلر ها هستند که ویو مربوطه را انتخاب و مدل مربوطه را به آن پاس می دهند تا صفحه مورد نظر رندر شود .
در اکو سیستم .Net ، فریمورک MVC در حال حاضرِ توصیه شده ، Asp.Net Core MVC است که بخشی از .Net Core محسوب می شود .اگر چه شباهت هایی با Asp.Net MVC که برای .Net framework است دارد ، اما هسته آن از ابتدا باز نویسی شده به صورتی که نمی توان برنامه ها را به راحتی بین این دو انتقال داد .
کد های کنترلر ها در Asp.Net Core MVC در داخل متد هایی به نام action قرار می گیرند که همگی در یک گروه کلاس کنترلر گروه بندی می شوند . به نمونه زیر دقت نمایید :
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
به صورت پیشفرض درخواست های HTTP به اکشن متناظرشان که در سیستم مسیریابی تعیین شده استناد می کنند .
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
در این قرارداد مشاهده می کنید که بخش ابتدایی مسیریابی توسط نام کنترلر تعیین شده و بخش دوم توسط اکشن مربوطه و مابقی نیز به عنوان آرگومان های ورودی برای متد اکشن مورد نظر که به صورت اختیاری تعریف شده است .همچنین مشاهده می کنید که مقداری را به صورت پیشفرض برای کنترلر و اکشن در نظر گرفته که به صورت خودکار اکشن Index از کنترلر Home اجرا خواهد شد . البته که میتوان این سیستم مسیریابی را به شکل دلخواه تان کانفیگ نمایید که در مقاله ای جداگانه آن را توضیح خواهم داد.
اکشن نمونه به سادگی ویو مربوطه را رندر می کند . ویو ی انتخاب شده مجددا از قراردادی که ما تعریف کرده ایم پیروی می کند .به صورت پیشفرض نام ویو باید مطابق با نام اکشن مربوطه باشد و در فولدر Views ، درون فولدری که همنام با کنترلر است باید قرار بگیرد .اکشن نمونه همیشه یک صفحه ساده و همسان با اکشن های دیگر را ارائه می دهد تا زمانی که ما داده هایی را به صورت داینامیک و توسط مدل به آن پاس دهیم . به طور مثال درانتهای اکشن به روش زیر عمل می نماییم :
return View(personModel);
و در ویو ی مربوطه نیز مدل از طریق پراپرتی Model قابل دسترسی می باشد :
<h4>@Model.FirstName @Model.LastName</h4>
برای پاسخ دادن به تعامل کاربر اکشن متد ها نیاز به ورودی دارند . در وب اپلیکیشن ها این ورودی ها میتوانند از طریق URL برای ما ارسال شوند و یا از طریق فیلد های درون فرم در قالب یک درخواست از نوع POST ، سپس مدل بایندر ها تمام ورودی ها را به اعضای متناظر آنها در قسمت ورودی های اکشن متد مربوطه ، نگاشت می کنند .
public ActionResult Edit(int id, PersonModel person)
{
// ...
}
همانطور که در مورد مسیریابی می دانید ، Asp.Net Core MVC قراردادهایی را شامل میشود که میتوانند به صورت خودکار داده های دریافتی از منابع مختلف را به آرگومان های ورودی اکشن متد ، بر اساس نامشان نگاشت کنند . مثلا مقداری را با عنوان id و همچنین Property name هایی از نوع Complex Type که در نقش آرگومان های ورودی هستند مانند PersonModel در مثال بالا .
ممکن است این روش شما را وسوسه کند که از مدل های اصلی به عنوان آرگومان های ورودی اکشن ها استفاده کنید، اما این روشی مناسب نیست زیرا این مساله موجب میشود که وابستگی بین این لایه ها افزایش یابد .داشتن مدل های اختصاصی برای این امر می تواند مزیت های متفاوتی را برای ما ارائه کند به طور مثال :
- میتوانیم مدل هایی را که به سمت ویو ارسال میکنیم بر اساس نیاز مان ساختاریافته تر باشند . این امر می تواند باعث کاهش میزان کد های ما در سمت ویو در صورت نیاز به مدل های جنریک شود .
- زمانی که از مدل های اختصاصی به عنوان ورودی آرگومان ها استفاده می کنیم میتوانم اعتبار سنجی بیشتری روی اعضای آن مدل داشته باشیم .اگر درون مدل موردنظر پراپرتی متناظر با درخواست وجود نداشته باشد بنابراین خطری برنامه مارا تهدید نمی کند در صورتی که در غیر این صورت می تواند در قالب یک درخواست پیش بینی نشده برنامه ما را تحت تاثیر قرار دهد .
بحثی که در این مورد باید در نظر بگیریم این هست که باید داده ها را از مدل های اختصاصی ساخته شده توسط ما ، به مدل اصلی یا entity مقصد نگاشت کنیم که توصیه می شود از کتابخانه های رایج مثل AutoMapper برای این کار استفاده شود در غیر این صورت امکان پیش آمد خطا بیشتر است .
تزریق وابستگی - Dependecy Injection
رویکرد معماری که در بالا ذکر شد باعث می شود که ویو ها به صورت کامل از کنترلر ها و مابقی بخش های برنامه مثل لاجیک یا همان منطق برنامه ها ، بی خبر باشند . هر ویو فقط از مدلی که از سمت کنترلر دریافت می کند مطلع است و مسیر های برنامه ای که با آن تعامل دارد . از طرف دیگر کنترلر از ویویی که می خواهد ارائه دهد و مدل متناظر با آن مطلع است و همچنین مدلی که می تواند به صورت اختیاری به عنوان ورودی دریافت کند .
هنوز بخش بزرگی از اکشن متد ها مانده که در موردشان صحبت نکرده ایم .
ممکن است در بدنه اکشن متد ها نیاز داشته باشیم برخی لاجیک ها یا منطق های خاصی را اجرا کنیم تا خروجی مورد نظر برای ارسال به سمت ویو آماده شود .در حالی که این منطق ها میتوانند به صورت مستقیم درون هر اکشن پیاده سازی شوند ، گزینه بهتر این است که منطق برنامه در یک لایه جداگانه که تحت عنوان Service class ها آن ها را می شناسیم ایجاد شوند نه درون هر اکشن متد .
public ActionResult Edit(int id, PersonModel person)
{
var personService = new PersonService();
var updatedPerson = personService.UpdatePerson(person);
return View(updatedPerson);
}
برای اینکه از افزایش وابستگی بین کنترلر و پیاده سازی یک سرویس خاص درون آن جلوگیری کنیم ، میتوانیم از Dependecy Injection ها استفاده کنیم . DI ها به کنترلرها اجازه می دهند که فقط به یک اینترفیس وابسته باشند که درون سازنده هر کنترلر قابل نمونه سازی هستند . این اینترفیس ها در حقیقت فقط یک تعریف از متد مورد نظر ما هستند و پیاده سازی آنها در کلاسی جداگانه صورت می گیرد .
public class PersonController : Controller
{
private readonly IPersonService personService;
public PersonController(IPersonService personService)
{
this.personService = personService;
}
public ActionResult Edit(int id, PersonModel person)
{
var updatedPerson = this.personService.UpdatePerson(person);
return View(updatedPerson);
}
}
Asp.Net Core به صورت خودکار از DI پشتیبانی می کند . از آنجایی که به راحتی میتوان از این قابلیت در Asp.Net Core استفاده کرد ، اکثر طراحان از همین ویژگی برای پیاده سازی وابستگی های خود استفاده می کنند . از طریق متد Configure Service درون کلاس Startup نیز قابل تنظیم می باشد .
services.AddScoped<IPersonService, PersonService>();
در این کانفیگ شما یک اینترفیس را مشاهده می کنید و سرویسی که متد های درون اینترفیس درون آن پیاده سازی شده اند و یک چرخه حیات برای سرویس مورد نظر ما که در این مثال از نوع Scoped در نظر گرفته شده است ، به این معنی که این سرویس برای هر درخواست به صورت جداگانه ایجاد شود . این روش به ما اطمینان می دهد که سرویس ها در صورت استفاده به صورت موازی ایزوله باشند .
Data Access
با حذف کردن business logic از کنترلر ، سرویس ها در نقش دیگری در معماری MVC ظاهر می شوند .اما کنترلر ها باید از وجود آنها مطلع باشند . در ابتدا ممنونیم از DI ، به دلیل اینکه حالا کنترلرفقط باید از پابلیک اینترفیس آن مطلع باشد ، نه از پیاده سازی آن .
در میان همه این وابستگی سرویس ها ، data access ها از توجه ویژه ای برخوردارند که به احتمال زیاد قرار است با ORM (object-relational mapper) خاصی مثل Entity Framework Core و یا Dapper پیاده سازی شوند .
اما چطور Data access library ها درون این معماری جای می گیرند ؟
الگوی معماریی که اغلب برای ارتباط با Data Access استفاده می شود Repository pattern و Unit Of Work ها هستند .
Repository pattern ها منطق برنامه ها را از جزییات پیاده سازی لایه Data Access جدا می کنند .
public interface IPersonRepository
{
IEnumerable<Person> List();
Person Get(int id);
Person Insert(Person person);
Person Update(Person person);
void Delete(Person person);
}
هر نوع کوئری دیگری که ممکن است نیاز به اجرا بر روی یک موجودیت را داشته باشد باید از طریق repository مخصوص خود پیاده سازی شود و این باعث می شود که repository ها تنها جایی باشند که این عملیات را بر روی داده ها انجام می دهند .
در یک اجرای ساده ، هر نوع عملیاتی از جمله اصلاح داده ها فورا در لایه زیرین خود اجرا می شوند . به هر حال در سناریو های پیچیده تر این روش به شیوه موثر کارآمد نخواهد بود ، زیرا اجازه نمی دهد چندین عملیات در قالب یک ترنزکشن صورت پذیرد (یعنی اینکه یا همه عملیات با موفقیت انجام می شود یا هیچ کدام صورت نمی گیرد).
یک گزینه ، افزودن روش ذخیره سازی جداگانه به ریپازیتوری است که میتواند فراخوانی شود تا تغییرات اجرا نشده از فراخوانی روش های دیگر در آن اعمال شود . اما باز هم مشکل به صورت کامل حل نمی شود . اگر تغییرات در ریپازیتوری های دیگر صورت گرفته باشد و بخواهیم در قالب یک transaction آن را اجرا کنیم ، آنوقت باید چه کرد ؟
این دقیقا جایی است که الگوی Unit of work (uow) به صحنه می آید . این الگو در حقیقت یک متد ذخیره سازی (Save) مشترک بین ریپازیتوری ها ایجاد می کند ، حالا ریپازیتوری ها از طریق Unit of work باید متد Save را فراخوانی کنند نه جداگانه .(متد Save نباید درون هر ریپازیتوری به صورت جداگانه تعریف شود).
public interface IUnitOfWork
{
IPersonRepository PersonRepository { get; }
IOrderRepository OrderRepository { get; }
// ...
void Save();
}
Entity Framework Core از مفهوم repository و Unit of work در پیاده سازی خود استفاده می کند .اما ممکن است یک سوال اساسی ایجاد شود .
آیا منطقی است که با وجود Entity Framework Core از این دو الگو برای دسترسی به داده ها استفاده کنیم ؟(Dapper و دیگر micro ORM ها این الگو ها را خودشان پیاده نمی کنند)
برای پاسخ دادن به این سوال باید در ابتدا به مزایایی که این الگو برای ما ایجاد می کند توجه کنیم و این که تا چه حد هنوز قابل اجراست ، اگر از Entity Framework Core به صورت مستقیم در لاجیک برنامه استفاده شود .
نتیجه :
در این مقاله برخی از الگو های معماری را که اغلب در برنامه های وب استفاده می شود را بررسی کرده ام.
با تعریف الگوی MVC شروع کرده ام و نشان داده ام چطور بخش کد برنامه از قسمت UI در اکثر وب اپ هایی که server-side render هستند جدا می شود . در ادامه Dependency Injection را توضیح دادم که چگونه در جداسازی این لایه ها موثر است و در قسمت پایانی مقاله توضیح داده ام الگوی Repository و Unit of wok چیست و چه کاربردی را برای ما ایجاد می کند .
این مقاله مروری است بر نوشته آقای ravi kiran از وبسایت dotnetcurry