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

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

تخمین مدت زمان مطالعه : 13 دقیقه
  • سطح مقاله : متوسطه
  • نویسنده : پوریا منتخب

در این مقاله مجموعه ای از دیزاین پترن ها را که امروزه در بیشتر وب اپلیکیشن ها استفاده می شود را بررسی می کنیم :
در حالت کلی دو دسته ی اصلی از وب اپلیکیشن ها بر اساس اینکه فایل نهایی HTML را  در مرورگر تولید می کنند موجود هستند :
رندر در سمت سرور یا Server-side rendered   : در این حالت عملیات در سرور صورت می گیرد . فایل نهایی Html  در قالب یک پاسخ یا Network response به مرورگر ارسال می شود .
رندر در سمت کلاینت یا Client-side rendered   : این حالت که به برنامه های SPAs یا همان single page application  ها معروف اند ، در مرورگر ها صورت می گیرند .

نحوه تبادل اطلاعات بین سرور و مرورگر در حالت Server-render

در این مقاله راجع به رندر در سمت سرور صحبت شده . اصطلاح Server-side rendering  گاهی اوقات در ارتباط با فریمورک های SPA که بر پایه جاوااسکریپت نیز هستند استفاده می شود ، این دو اصطلاح را با هم اشتباه نگیرید . ویژگی Server-side Rendering (SSR)  برنامه های تک صفحه ای جاوااسکریپتی به این معنی هست که فقط در اولین درخواست به سمت سرور ، صفحه مورد نظر در آنجا رندر می شود و در قالب نهایی خود به مرورگر ارسال می شود . در اینجا برنامه تک صفحه ای به اصطلاح بروز می شود ، به این معنی که هر گونه تعامل بیشتر با این صفحه در سمت کلاینت انجام می شود و از اینجا به بعد فقط داده ها از سمت سرور مجددا درخواست می شوند .

نحوه تبادل داده ها بین سرور و مرورگر

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

جدا کردن کد برنامه از UI

اکثر وب اپ هایی که در سمت سرور رندر می شوند امروزه از معماری MVC (model – view - controller) برای جدا کردن UI از سایر قسمت ها تبعیت می کنند . MVC همانطور که از نامش پیداست ، سه قسمت اصلی در این معماری وجود دارد.

  •  Model  ها وضعیت فعلی برنامه را مشخص می کنند و به عنوان ارتباط بین View  ها  و Controller ها استفاده می شوند .
  •  View ها UI  برنامه ما هستند . داده های ارائه شده از سمت مدل ها رو می خوانند و از آنجایی که صفحات وب هستند ، تعامل با آنها می توانددرخواست های جدیدی را به سمت سرور ارسال کند . 
  •  Controller ها درخواست های ارسال شده به سمت سرور را مدیریت می کنند ، به طور مثال زمانی که کاربران آدرس جدیدی را درخواست می کنند یا مثلا با صفحه ای که از قبل رندر شده کار می کنند و در نهایت این کنترلر ها هستند که ویو مربوطه را انتخاب و مدل مربوطه را به آن پاس می دهند تا صفحه مورد نظر رندر شود .

معماری MVC

در اکو سیستم .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 

رویکرد معماری که در بالا ذکر شد باعث می شود که ویو ها به صورت کامل از کنترلر ها و مابقی بخش های برنامه مثل لاجیک یا همان منطق برنامه ها ، بی خبر باشند . هر ویو فقط از مدلی که از سمت کنترلر دریافت می کند مطلع است و مسیر های برنامه ای که با آن تعامل دارد . از طرف دیگر کنترلر از ویویی که می خواهد ارائه دهد و مدل متناظر با آن مطلع است و همچنین مدلی که می تواند به صورت اختیاری به عنوان ورودی دریافت کند .

نحوه تبادل اطلاعات بین بلاک ها در MVC

هنوز بخش بزرگی از اکشن متد ها مانده که در موردشان صحبت نکرده ایم .
ممکن است در بدنه اکشن متد ها نیاز داشته باشیم برخی لاجیک ها یا منطق های خاصی را اجرا کنیم تا خروجی مورد نظر برای ارسال به سمت ویو آماده شود .در حالی که این منطق ها میتوانند به صورت مستقیم درون هر اکشن پیاده سازی شوند ، گزینه بهتر این است که منطق برنامه در یک لایه جداگانه که تحت عنوان 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 ، به دلیل اینکه حالا کنترلرفقط باید از پابلیک اینترفیس آن مطلع باشد ، نه از پیاده سازی آن .

نحوه تبادل اطلاعات بین اجزای MVC در حالت استفاده از Service ها

در میان همه این وابستگی سرویس ها ، 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 ها تنها جایی باشند که این عملیات را بر روی داده ها انجام می دهند .

نحوه دسترسی به داده ها در زمان استفاده از 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