نگاهی به تست جوئل نگارش 2
اصول طراحی شیءگرا
PM> Install-Package DNTFrameworkCore.Web
- Refactor کردن فرمهای ثبت و ویرایش مرتبط با یک Aggregate، به یک PartialView که با یک ViewModel کار میکند. برای موجودیتهای ساده و پایه، همان Model/DTO، به عنوان Model متناظر با یک ویو یا به اصطلاح ViewModel استفاده میشود؛ ولی برای سایر موارد، از مدلی که نام آن با نام موجودیت + کلمه ModalViewModel یا FormViewModel تشکیل میشود، استفاده خواهیم کرد.
- یک فرم، در قالب یک پارشالویو، به صورت Ajaxای با استفاده از افزونه jquery-unobtrusive-ajax بارگذاری شده و به سرور ارسال خواهد شد.
- یک فرم براساس طراحی خود میتواند در قالب یک مودال باز شود، یا به منظور inline-editing آن را بارگذاری و به قسمتی از صفحه که مدنظرتان میباشد اضافه شود.
- وجود ویو Index به همراه پارشالویو _List برای نمایش لیستی و یک پارشالویو برای عملیات ثبت و ویرایش الزامی میباشد. البته اگر از مکانیزمی که در مطلب « طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD» مطرح شد، استفاده نمیکنید و نیاز دارید تا اطلاعات صفحهبندی شده، مرتب شده و فیلتر شدهای را در قالب JSON دریافت کنید، از اکشنمتد ReadPagedList کنترلر پایه استفاده کنید.
public class BlogsController : CrudController<IBlogService, int, BlogModel> { public BlogsController(IBlogService service) : base(service) { } protected override string CreatePermissionName => PermissionNames.Blogs_Create; protected override string EditPermissionName => PermissionNames.Blogs_Edit; protected override string ViewPermissionName => PermissionNames.Blogs_View; protected override string DeletePermissionName => PermissionNames.Blogs_Delete; protected override string ViewName => "_BlogModal"; }
<form asp-action="@(Model.IsNew() ? "Create" : "Edit")" asp-controller="Blogs" asp-modal-form="BlogForm"> <div> <input type="hidden" name="save-continue" value="true"/> <input asp-for="RowVersion" type="hidden"/> <input asp-for="Id" type="hidden"/> <div> <div> <label asp-for="Title"></label> <input asp-for="Title" autocomplete="off"/> <span asp-validation-for="Title"></span> </div> </div> <div> <div> <label asp-for="Url"></label> <input asp-for="Url" type="url"/> <span asp-validation-for="Url"></span> </div> </div> </div> ... </form>
<div> <a asp-modal-delete-link asp-model-id="@Model.Id" asp-modal-toggle="false" asp-controller="Blogs" asp-action="Delete" asp-if="!Model.IsNew()" asp-permission="@PermissionNames.Blogs_Delete" title="Delete Blog"> <i></i> </a> <a title="Refresh Blog" asp-if="!Model.IsNew()" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Edit" asp-route-id="@Model.Id"> <i></i> </a> <a title="New Blog" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Create"> <i></i> </a> <button type="button" data-dismiss="modal"> <i></i> Cancel </button> <button type="submit"> <i></i> Save Changes </button> </div>
public class RoleModalViewModel : RoleModel { public IReadOnlyList<LookupItem> PermissionList { get; set; } }
protected override IActionResult RenderView(RoleModel role) { var model = _mapper.Map<RoleModalViewModel>(role); model.PermissionList = ReadPermissionList(); return PartialView(ViewName, model); }
برای مدیریت سناریوهای Master-Detail به مانند قسمت مدیریت دسترسیها در تب Permissions فرم بالا، امکاناتی در زیرساخت تعبیه شده است ولی پیادهسازی آن را به عنوان یک تمرین و با توجه به سری مطالب «Editing Variable Length Reorderable Collections in ASP.NET MVC» به شما واگذار میکنم.
نکته تکمیلی: برای ارسال اطلاعات اضافی به ویو Index متناظر با یک موجودیت میتوانید متد RenderIndex را به شکل زیر بازنویسی کنید:
protected override IActionResult RenderIndex(IPagedQueryResult<RoleReadModel> model) { var indexModel = new RoleIndexViewModel { Items = model.Items, TotalCount = model.TotalCount, Permissions = ReadPermissionList() }; return Request.IsAjaxRequest() ? (IActionResult) PartialView(indexModel) : View(indexModel); }
مدل RoleIndexViewModel استفاده شده در تکه کد بالا نیز به شکل زیر خواهد بود:
public class RoleIndexViewModel : PagedQueryResult<RoleReadModel> { public IReadOnlyList<LookupItem> Permissions { get; set; } }
فرآیند بارگذاری یک پارشالویو در مودال
به عنوان مثال برای استفاده از مودالهای بوت استرپ، ایده کار به این شکل است که یک مودال را به شکل زیر در فایل Layout قرار دهید:
<div class="modal fade" @*tabindex="-1"*@ id="main-modal" data-keyboard="true" data-backdrop="static" role="dialog" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <div class="modal-body"> Loading... </div> </div> </div> </div>
سپس در زمان کلیک بروی یک دکمه Ajaxای، ابتدا main-modal را نمایش داده و بعد از دریافت پارشالویو از سرور، آن را با محتوای modal-content جایگزین میکنیم. به همین دلیل Tag Halperهای مطرح شده در مطلب جاری، callbackهای failure/complete/success متناظر با unobtrusive-ajax را نیز مقداردهی میکنند. برای این منظور نیاز است تا متدهای جاوااسکریپتی زیر نیز در سطح شیء window تعریف شده باشند:
/*---------------------------------- asp-modal-link ---------------------------*/ window.handleModalLinkLoaded = function (data, status, xhr) { prepareForm('#main-modal.modal form'); }; window.handleModalLinkFailed = function (xhr, status, error) { //.... }; /*---------------------------------- asp-modal-form ---------------------------*/ window.handleModalFormBegin = function (xhr) { $('#main-modal a').addClass('disabled'); $('#main-modal button').attr('disabled', 'disabled'); }; window.handleModalFormComplete = function (xhr, status) { $('#main-modal a').removeClass('disabled'); $('#main-modal button').removeAttr('disabled'); }; window.handleModalFormSucceeded = function (data, status, xhr) { if (xhr.getResponseHeader('Content-Type') === 'text/html; charset=utf-8') { prepareForm('#main-modal.modal form'); } else { hideMainModal(); } }; window.handleModalFormFailed = function (xhr, status, error, formId) { if (xhr.status === 400) { handleBadRequest(xhr, formId); } };
برای بررسی بیشتر، پیشنهاد میکنم پروژه DNTFrameworkCore.TestWebApp موجود در مخزن این زیرساخت را بازبینی کنید.
هدف از این مطلب، ارائه راه حلی برای تولید خودکار کد یا شماره یکتا و ترتیبی در زمان ثبت رکورد جدید به صورت یکپارچه با EF Core، میباشد. به عنوان مثال فرض کنید در زمان ثبت سفارش، نیاز است بر اساس یکسری تنظیمات، یک شماره منحصر به فرد برای آن سفارش، تولید شده و در فیلدی تحت عنوان Number قرار گیرد؛ یا به صورت کلی برای موجودیتهایی که نیاز به یک نوع شماره گذاری منحصر به فرد دارند، مانند: سفارش، طرف حساب و ...
یک مثال واقعی
در زمان ثبت یک Task، کاربر میتواند به صورت دستی یک شماره منحصر به فرد را نیز وارد کند؛ در غیر این صورت سیستم به طور خودکار شمارهای را به رکورد در حال ثبت اختصاص خواهد داد. بررسی یکتایی این کد در صورت وارد کردن به صورت دستی، توسط اعتبارسنج مرتبط باید انجام گیرد؛ ولی در غیر این صورت، زیرساخت مورد نظر تضمین میکند که شماره یکتایی را ایجاد کند.
public interface INumberedEntity { string Number { get; set; } }
foreach (var entityType in builder.Model.GetEntityTypes() .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType))) { builder.Entity(entityType.ClrType) .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50); if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType)) { builder.Entity(entityType.ClrType) .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId)) .HasName( $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}") .IsUnique(); } else { builder.Entity(entityType.ClrType) .HasIndex(nameof(INumberedEntity.Number)) .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}") .IsUnique(); } }
public class NumberedEntity : Entity, IMultiTenantEntity { public string EntityName { get; set; } public long NextNumber { get; set; } public long TenantId { get; set; } }
public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity> { public void Configure(EntityTypeBuilder<NumberedEntity> builder) { builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false); builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique(); builder.ToTable(nameof(NumberedEntity)); } }
شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت میتوان آخرین شماره ثبت شدهی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:
- ایجاد Gap مابین شمارههای تولید شده، که مدنظر ما نمیباشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شمارهای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)
پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number
internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity> { private readonly IUnitOfWork _uow; private readonly IOptions<NumberingConfiguration> _configuration; public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration) { _uow = uow ?? throw new ArgumentNullException(nameof(uow)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata) { if (!entity.Number.IsNullOrEmpty()) return; bool retry; string nextNumber; do { nextNumber = GenerateNumber(entity); var exists = CheckDuplicateNumber(entity, nextNumber); retry = exists; } while (retry); entity.Number = nextNumber; } private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber) { //... } private string GenerateNumber(INumberedEntity entity) { //... } }
ابتدا بررسی میشود اگر پراپرتی Number مقداردهی شدهاست، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شمارهای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت میگیرد.
بررسی متد GenerateNumber
private string GenerateNumber(INumberedEntity entity) { var option = _configuration.Value.NumberedEntityOptions[entity.GetType()]; var entityName = $"{entity.GetType().FullName}"; var lockKey = $"Tenant_{_uow.TenantId}_" + entityName; _uow.ObtainApplicationLevelDatabaseLock(lockKey); var nextNumber = option.Start.ToString(); var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName); if (numberedEntity == null) { _uow.ExecuteSqlCommand( "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName, option.Start + option.IncrementBy, _uow.TenantId); } else { nextNumber = numberedEntity.NextNumber.ToString(); _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ", numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id); } if (!string.IsNullOrEmpty(option.Prefix)) nextNumber = option.Prefix + nextNumber; return nextNumber; }
ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد میکنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنشهای همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید میباشد، با توجه به اینکه INumberedEntity میباشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.
پیاده سازی متد مذکور به شکل زیر میباشد:
public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource) { uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000); }
با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیتهای INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونهها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی میشود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود.
گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری میباشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام میگیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.
ساختار NumberingConfiguration
public class NumberingConfiguration { public bool Enabled { get; set; } public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } = new Dictionary<Type, NumberedEntityOption>(); }
public class NumberedEntityOption { public string Prefix { get; set; } public int Start { get; set; } = 1; public int IncrementBy { get; set; } = 1; }
با استفاده از دوکلاس بالا، امکان تنظیم الگوی تولید برای موجودیتها را خواهیم داشت.
گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیتها
public static void AddNumbering(this IServiceCollection services, IDictionary<Type, NumberedEntityOption> options) { services.Configure<NumberingConfiguration>(configuration => { configuration.Enabled = true; configuration.NumberedEntityOptions.AddRange(options); }); services.AddTransient<IPreActionHook, NumberingPreInsertHook>(); }
و استفاده از این متد الحاقی در Startup پروژه
services.AddNumbering(new Dictionary<Type, NumberedEntityOption> { [typeof(Task)] = new NumberedEntityOption { Prefix = "T_", Start = 1000, IncrementBy = 5 } });
و موجودیت Task
public class Task : TrackableEntity, IAggregateRoot, INumberedEntity { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 1024; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Description { get; set; } public TaskState State { get; set; } = TaskState.Todo; public byte[] RowVersion { get; set; } public string Number { get; set; } }
با خروجیهای زیر
سناریویی را در نظر بگیرید که یک برنامه وب نوشته شده، قرار است به چندین مستاجر (مشتری یا tenant) خدماتی را ارائه کند. در این حالت اطلاعات هر مشتری به صورت کاملا جدا شده از دیگر مشتریان در سیستم قرار دارد و فقط به همان قسمتها دسترسی دارد.
مثلا یک برنامه مدیریت رستوران را در نظر بگیرید که برای هر مشتری، در دامین
مخصوص به خود قرار دارد و همه آنها به یک سیستم متمرکز متصل شده و اطلاعات
خود را از آنجا دریافت میکنند.
در معماری Multi-Tenancy، چندین کاربر میتوانند از یک نمونه (Single
Instance) از اپلیکیشن نرمافزاری استفاده کنند. یعنی این نمونه روی سرور
اجرا میشود و به چندین کاربر سرویس میدهد. هر کاربر را یک Tenant
مینامیم. میتوان به Tenantها امکان تغییر و شخصیسازی بخشی از اپلیکیشن
را داد؛ مثلا امکان تغییر رنگ رابط کاربری و یا قوانین کسبوکار، اما آنها نمیتوانند
کدهای اپلیکیشن را شخصیسازی کنند.
خوشبختانه اوضاع با وجود OWIN بهتر شده و ما در این مطلب قصد استفاده از یک تولکیت را به نام SaasKit، برای پیاده سازی این معماری در ASP.NET Core داریم. هدف از این toolkit، سادهتر کردن هر چه بیشتر ساخت برنامههای SaaS (Software as a Service) هست. با استفاده از OWIN ما قادریم که بدون در نظر گرفتن فریم ورک مورد استفاده، رفتار مورد نظر خودمان را مستقیما در یک چرخه درخواست HTTP پیاده سازی کنیم و البته به لطف طراحی خاص ASP.NET Core 1.0 و استفاده از میان افزارهایی مشابه OWIN در برنامه، کار ما با SaasKit باز هم راحتتر خواهد بود.
شروع کار
یک پروژه ASP.NET Core جدید را ایجاد کنید و سپس ارجاعی را به فضای نام SaasKit.Multitenancy (موجود در Nuget) بدهید.PM> Install-Package SaasKit.Multitenancy
شناسایی مستاجر (tenant)
اولین جنبه در معماری multi-tenant، شناسایی مستاجر بر اساس اطلاعات درخواست جاری میباشد که میتواند از hostname ، کاربر جاری یا یک HTTP header باشد.ابتدا به تعریف کلاس مستاجر میپردازیم:
public class AppTenant { public string Name { get; set; } public string[] Hostnames { get; set; } }
public class AppTenantResolver : ITenantResolver<AppTenant> { IEnumerable<AppTenant> tenants = new List<AppTenant>(new[] { new AppTenant { Name = "Tenant 1", Hostnames = new[] { "localhost:6000", "localhost:6001" } }, new AppTenant { Name = "Tenant 2", Hostnames = new[] { "localhost:6002" } } }); public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return tenantContext; } }
سیم کشی کردن
بعد از پیاده سازی این اینترفیس نوبت به سیم کشیهای SaasKit میرسد. من در اینجا سعی کردم که مثل الگوی برنامههای ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگیهای SaasKit را ثبت کنیم. فایل startups.cs را باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید:public void ConfigureServices(IServiceCollection services) { services.AddMultitenancy<AppTenant, AppTenantResolver>(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // after .UseStaticFiles() app.UseMultitenancy<AppTenant>(); // before .UseMvc() }
دریافت مستاجر جاری
حالا هر جا که نیاز به وهلهای از شیء مستاجر جاری داشتید، میتوانید به روش زیر عمل کنید:public class HomeController : Controller { private AppTenant tenant; public HomeController(AppTenant tenant) { this.tenant = tenant; } }
@inject AppTenant Tenant;
<a asp-controller="Home" asp-action="Index">@Tenant.Name</a>
اجرای نمونه مثال
فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود 3 آدرس را نگاشت کردیم)"commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002", },
dotnet run
و اگر آدرس http://localhost:6002 را وارد کنیم، مستاجر 2 را مشاهده میکنیم:
قابل پیکربندی کردن مستاجر ها
از آنجائیکه نوشتن مشخصات مستاجرها در کد زیاد جالب نیست، برای همین
تصمیم داریم که این مشخصات را با استفاده از قابلیتهای ASP.NET Core از
فایل appsettings.json دریافت کنیم. تنظیمات مستاجرها را مطابق اطلاعات زیر به این فایل اضافه کنید:
"Multitenancy": { "Tenants": [ { "Name": "Tenant 1", "Hostnames": [ "localhost:6000", "localhost:6001" ] }, { "Name": "Tenant 2", "Hostnames": [ "localhost:6002" ] } ] }
public class MultitenancyOptions { public Collection<AppTenant> Tenants { get; set; } }
services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
public class AppTenantResolver : ITenantResolver<AppTenant> { private readonly IEnumerable<AppTenant> tenants; public AppTenantResolver(IOptions<MultitenancyOptions> options) { this.tenants = options.Value.Tenants; } public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return Task.FromResult(tenantContext); } }
در آخر
اولین قدم در پیاده سازی یک معماری multi-tenant، تصمیم گیری درباره این
موضوع است که شما چطور مستاجر خود را شناسایی کنید. به محض این شناسایی شما میتوانید عملیاتهای بعدی خود را مثل تفکیک بخشی از برنامه، فیلتر کردن دادهای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمتهای مختلف
برنامه بر اساس هر مستاجر، انجام دهید.
_ سورس مثال بالا در گیت هاب قابل دریافت میباشد.
_ منبع: اینجا
class Person : IPerson { private string Name { get; } private string Surname { get; } private IContact PrimaryContact { get; set; } private IList<IContact> AllContacts { get; } public Person(string name, string surname, IContact primaryContact) { if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name)); if (string.IsNullOrEmpty(surname)) throw new ArgumentException(nameof(surname)); this.Name = name; this.Surname = surname; this.AllContacts = new List<IContact>(); this.SetPrimaryContact(primaryContact); } public void SetPrimaryContact(IContact contact) { this.AddContact(contact); this.PrimaryContact = contact; } public void AddContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.AllContacts.Add(contact); } }
interface IContact { } class PhoneNumber : IContact { private string AreaCode { get; } private string Number { get; } public PhoneNumber(string areaCode, string number) { if (string.IsNullOrEmpty(areaCode)) throw new ArgumentException(nameof(areaCode)); if (string.IsNullOrEmpty(number)) throw new ArgumentException(nameof(number)); this.AreaCode = areaCode; this.Number = number; } } class EmailAddress : IContact { private string Address { get; } public EmailAddress(string address) { if (string.IsNullOrEmpty(address)) throw new ArgumentException(nameof(address)); this.Address = address; } }
طراحی PersonBuilder :
interface IPerson { void SetPrimaryContact(IContact primaryContact); void AddContact(IContact contact); } interface IPersonBuilder { void SetName(string name); void SetSurname(string surname); void SetPrimaryContact(IContact primaryContact); void AddContact(IContact contact); IPerson Build(); }
class PersonBuilder: IPersonBuilder { private string Name { get; set; } private string Surname { get; set; } private IContact PrimaryContact { get; set; } private IList<IContact> OtherContacts { get; } = new List<IContact>(); public void SetName(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name)); this.Name = name; } public void SetSurname(string surname) { if (string.IsNullOrEmpty(surname)) throw new ArgumentException(nameof(surname)); this.Surname = surname; } public void SetPrimaryContact(IContact primaryContact) { if (primaryContact == null) throw new ArgumentNullException(nameof(primaryContact)); this.PrimaryContact = primaryContact; } public void AddContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.OtherContacts.Add(contact); } public IPerson Build() { IPerson person = new Person(this.Name, this.Surname, this.PrimaryContact); foreach (IContact contact in this.OtherContacts) person.AddContact(contact); return person; } }
راه حل: استفاده از Interface Segregation principle در PersonBuilder :
class PersonBuilder { private PersonBuilder() { } public static IExpectSurnamePersonBuilder WithName(string name) { ... } }
پس به ازای هر کدام از عملیاتها، یک اینترفیس خواهیم داشت:
interface IExpectSurnamePersonBuilder { IExpectPrimaryContactPersonBuilder WithSurname(string surname); } interface IExpectPrimaryContactPersonBuilder { IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact); } interface IExpectOtherContactsPersonBuilder { IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact); IPersonBuilder WithNoMoreContacts(); } interface IPersonBuilder { IPerson Build(); }
class PersonBuilder : IExpectSurnamePersonBuilder, IExpectPrimaryContactPersonBuilder, IExpectOtherContactsPersonBuilder, IPersonBuilder { private string Name { get; } private string Surname { get; set; } private IContact PrimaryContact { get; set; } private Person Person { get; set; } private PersonBuilder(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name)); this.Name = name; } public static IExpectSurnamePersonBuilder WithName(string name) { return new PersonBuilder(name); } public IExpectPrimaryContactPersonBuilder WithSurname(string surname) { if (string.IsNullOrEmpty(surname)) throw new ArgumentException(nameof(surname)); this.Surname = surname; return this; } public IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.Person = new Person(this.Name, this.Surname, contact); return this; } public IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact) { if (contact == null) throw new ArgumentNullException(nameof(contact)); this.Person.AddContact(contact); return this; } public IPersonBuilder WithNoMoreContacts() { return this; } public IPerson Build() { return this.Person; } }
IPerson person = PersonBuilder .WithName("Ali") .WithSurname("Karimi") .WithPrimaryContact(new EmailAddress("admin@gmail.com")) .WithOtherContact(new EmailAddress("Test1@work.com")) .WithOtherContact(new EmailAddress("Test2@home.com")) .WithNoMoreContacts() .Build();
چکیده: