معماری لایه بندی نرم افزار #3
نویسنده: میثم خوشبخت
تاریخ: ۱۳۹۲/۰۱/۰۲ ۱۷:۵۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Service Layer
نقش لایهی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایهی Facade نیز میشناسند. این لایه، دادهها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایهی Presentation فراهم میکند. کلاس View Model یک Strongly Typed محسوب میشود که نماهای خاصی از دادهها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه مینماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.
الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interfaceها و زیر سیستمهای پیچیده ارائه میکند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.
کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductViewModel
{
Public int ProductId {get; set;}
public string Name { get; set; }
public string Rrp { get; set; }
public string SellingPrice { get; set; }
public string Discount { get; set; }
public string Savings { get; set; }
}برای اینکه کلاینت با لایهی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه میشود و پارامترهای مورد نیاز را فراهم میکند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
using SoCPatterns.Layered.Model;
namespace SoCPatterns.Layered.Service
{
public class ProductListRequest
{
public CustomerType CustomerType { get; set; }
}
}در شی Response نیز بررسی میکنیم که درخواست به درستی انجام شده باشد، دادههای مورد نیاز را برای کلاینت فراهم میکنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال مینماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductListResponse
{
public bool Success { get; set; }
public string Message { get; set; }
public IList<ProductViewModel> Products { get; set; }
}به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما میتوانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمیباشند. بنابراین بهترین انتخاب، استفاده از Extension Methodها میباشد که باید برای کلاس Product و در لایهی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public static class ProductMapperExtensionMethods
{
public static ProductViewModel ConvertToProductViewModel(this Model.Product product)
{
ProductViewModel productViewModel = new ProductViewModel();
productViewModel.ProductId = product.Id;
productViewModel.Name = product.Name;
productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP);
productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice);
if (product.Price.Discount > 0)
productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount);
if (product.Price.Savings < 1 && product.Price.Savings > 0)
productViewModel.Savings = product.Price.Savings.ToString(“#%”);
return productViewModel;
}
public static IList<ProductViewModel> ConvertToProductListViewModel(
this IList<Model.Product> products)
{
IList<ProductViewModel> productViewModels = new List<ProductViewModel>();
foreach(Model.Product p in products)
{
productViewModels.Add(p.ConvertToProductViewModel());
}
return productViewModels;
}
}حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد مینماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductService
{
private Model.ProductService _productService;
public ProductService(Model.ProductService ProductService)
{
_productService = ProductService;
}
public ProductListResponse GetAllProductsFor(
ProductListRequest productListRequest)
{
ProductListResponse productListResponse = new ProductListResponse();
try
{
IList<Model.Product> productEntities =
_productService.GetAllProductsFor(productListRequest.CustomerType);
productListResponse.Products = productEntities.ConvertToProductListViewModel();
productListResponse.Success = true;
}
catch (Exception ex)
{
// Log the exception…
productListResponse.Success = false;
// Return a friendly error message
productListResponse.Message = ex.Message;
}
return productListResponse;
}
}کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال میکند. همچنین این لایه محل مناسبی برای Log کردن خطاها میباشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer میپردازیم.
Data Layer
برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:
برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده میکنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه میکنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.
از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.
اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SoCPatterns.Layered.Model;
namespace SoCPatterns.Layered.Repository
{
public class ProductRepository : IProductRepository
{
public IList<Model.Product> FindAll()
{
var products = from p in new ShopDataContext().Products
select new Model.Product
{
Id = p.ProductId,
Name = p.ProductName,
Price = new Model.Price(p.Rrp, p.SellingPrice)
};
return products.ToList();
}
}
}در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایهی Data هم به پایان رسید و در ادامه به کدنویسی لایهی Presentation و UI میپردازیم.
Presentation Layer
به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده میکنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service;
public interface IProductListView
{
void Display(IList<ProductViewModel> Products);
Model.CustomerType CustomerType { get; }
string ErrorMessage { set; }
}این Interface توسط Web Formهای ASP.NET و یا Win Formها باید پیاده سازی شوند. کار با Interfaceها موجب میشود تا تست Viewها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service;
namespace SoCPatterns.Layered.Presentation
{
public class ProductListPresenter
{
private IProductListView _productListView;
private Service.ProductService _productService;
public ProductListPresenter(IProductListView ProductListView,
Service.ProductService ProductService)
{
_productService = ProductService;
_productListView = ProductListView;
}
public void Display()
{
ProductListRequest productListRequest = new ProductListRequest();
productListRequest.CustomerType = _productListView.CustomerType;
ProductListResponse productResponse =
_productService.GetAllProductsFor(productListRequest);
if (productResponse.Success)
{
_productListView.Display(productResponse.Products);
}
else
{
_productListView.ErrorMessage = productResponse.Message;
}
}
}
}کلاس Presenter وظیفهی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایهی Presentation به پایان رسیده است. از مزایای وجود لایهی Presentation این است که تست نویسی مربوط به نمایش دادهها و تعامل بین کاربر و سیستم به سهولت انجام میشود بدون آنکه نگران دشواری Unit Test نویسی Web Formها باشید. حال میتوانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.
ممنون از زحمات شما.
چند سؤال و نظر:
- با تعریف الگوی مخزن به چه مزیتی دست پیدا کردید؟ برای مثال آیا هدف این است که کدهای پیاده سازی آن، با توجه به وجود اینترفیس تعریف شده، شاید روزی با مثلا NHibernate تعویض شود؟ در عمل متاسفانه حتی پیاده سازی LINQ اینها هم متفاوت است و من تابحال در عمل ندیدم که ORM یک پروژه بزرگ رو عوض کنند. یعنی تا آخر و تا روزی که پروژه زنده است با همان انتخاب اول سر میکنند. یعنی شاید بهتر باشه قسمت مخزن و همچنین سرویس یکی بشن.
- چرا لایه سرویس تعریف شده از یک یا چند اینترفیس مشتق نمیشود؟ اینطوری تهیه تست برای اون سادهتر میشه. همچنین پیاده سازیها هم وابسته به یک کلاس خاص نمیشن چون از اینترفیس دارن استفاده میکنند.
- این اشیاء Request و Response هم در عمل به نظر نوعی ViewModel هستند. درسته؟ اگر اینطوره بهتر یک مفهوم کلی دنبال بشه تا سردرگمیها رو کمتر کنه.
یک سری نکته جانبی هم هست که میتونه برای تکمیل بحث جالب باشه:
- مثلا الگوی Context per request بجای نوشتن new ShopDataContext بهتر استفاده بشه تا برنامه در طی یک درخواست در یک تراکنش و اتصال کار کنه.
- در مورد try/catch و استفاده از اون بحث زیاد هست. خیلیها توصیه میکنن که یا اصلا استفاده نکنید یا استفاده از اونها رو به بالاترین لایه برنامه موکول کنید تا این وسط کرش یک قسمت و بروز استثناء در اون، از ادامه انتشار صدمه به قسمتهای بعدی جلوگیری کنه.
- من در عمل تفاوتی بین لایه مخزن و سرویس شما مشاهده نمیکنم. یعنی لایه مخزن داره GetAll میکنه، بعد لایه سرویس هم داره همون رو به یک شکل دیگری بر میگردونه. این تکرار کد نیست؟ این دو یکی نیستند؟
عموما در منابع لایه مخزن رو به صورت روکشی برای دستورات مثلا EF یا LINQ to SQL معرفی میکنند. فرضشون هم این است که این روش ما رو از تماس مستقیم با ORM برحذر میداره (شاید فکر میکنند ایدز میگیرند اگر مستقیم کار کنند!). ولی عرض کردم این روکش در واقعیت فقط شاید با EF یا L2S قابل تعویض باشه نه با ORMهای دیگر با روشهای مختلف و بیشتر یک تصور واهی هست که جنبه عملی نداره. بیشتر تئوری هست بدون پایه تجربه دنیای واقعی. ضمن اینکه این روکش باعث میشه نتونید از خیلی از امکانات ORM مورد استفاده درست استفاده کنید. مثلا ترکیب کوئریها یا روشهای به تاخیر افتاده و امثال این.
- پس در عمل شما Request ViewModel و Response ViewModel تعریف کردید.
- پیاده سازی الگوی مخزن در عمل (بر اساس بحث فعلی که در مورد کار با ORMها است) به صورت کپسوله سازی ORM در همه جا مطرح میشه و اینکار اساسا اشتباه هست. چون هم شما رو محروم میکنه از قابلیتهای پیشرفته ORM و هم ارزش افزودهای رو به همراه نداره. دست آخر میبینید در لایه مخزن GetAll دارید در لایه سرویس هم GetAll دارید. این مساله هیچ مزیتی نداره. یک زمانی در ADO.NET برای GetAll کردن باید کلی کد شبیه به کدهای یک ORM نوشته میشد. خود ORM الان اومده اینها رو کپسوله کرده و لایهای هست روی اون. اینکه ما مجددا یک پوسته روی این بکشیم حاصلی نداره بجز تکرار کد. عدهای عنوان میکنند که حاصل اینکار امکان تعویض ORM رو ممکن میکنه ولی اینها هم بعد از یک مدت تجربه با ORMهای مختلف به این نتیجه میرسند که ای بابا! حتی پیاده سازی LINQ این ORMها یکی نیست چه برسه به قابلیتهای پیشرفتهای که در یکی هست در دوتای دیگر نیست (واقع بینی، بجای بحث تئوری محض).
- اینکه این تبدیلات (پر کردن ViewModel از روی مدل) هم میتونه و بهتره که (نه الزاما) در لایه سرویس انجام بشه، نتیجه مناسبی هست.
public class PersonFormViewModel
{
public long Id { get; set; }
public long RequestId { get; set; }
[DisplayName("نام کاربری"), Required(ErrorMessage = "نام کاربری الزامی میباشد.")]
public string Username { get; set; }
public bool Accepted { get; set; }
[DisplayName("مدل")]
public string DeviceModel { get; set; }
public DateTime? ExpireDate { get; set; }
public RequestViewModel RequestViewModel { get; set; }
} public class RequestViewModel
{
public long Id { get; set; }
public string Username { get; set; }
[DisplayName("توضیحات")]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
public DateTime CreateDate { get; set; }
public Nullable<long> DeviceId { get; set; }
public Nullable<long> ParentId { get; set; }
public long RequestTypeId { get; set; }
public bool IsFinalized { get; set; }
public virtual PersonFormViewModel PersonFormViewModel { get; set; }
} _requestService.Add(requestViewModel); _uow.SaveChanges();
var addedRequest = _requestService.Add(requestViewModel ); var personViewModel = requestViewModel .PersonFormViewModel; _personFormService.Add(personViewModel); _uow.SaveChanges();