الگوی طراحی builder، برای ساختن اشیاء بسیار مفید است؛ اما پروسه ساختن اشیاء آن بسیار پیچده هست و به صورت معمول، این پروسه شامل چندین قسمت میشود.
در این مثال ما مشکلات ساختن شیء Person را مورد بررسی قرار میدهیم و این شیء از اشیایی کوچکتر مانند Name ، Surname و یا Primary Contact و غیره نیز تشکیل شده است.
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); } }
همان طور که مشاهده میکنید، مقدار دهی شیء IContact پیچیدهتر از Name و Surname هست و روش اضافه کردن Contactها نیز بسیار پیچیده است؛ زیرا آنها به دو گروه PrimaryContact و Contacts تقسیم شدهاند.
شی Person شامل تعدای Contact مانند تلفن، ایمیل و یا هر چیزی دیگری میتواند باشد.
در این مثال ما دو نوع 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; } }
به صورت کلی سه راه برای ساختن اشیاء وجود دارد:
1) استفاده از سازنده کلاس Person و سپس استفاده از متدهای AddContact و SetPrimaryContact برای ساختن شیء، به صورت کامل.
2) استفاده از Abstract Factory برای ساختن Person و سپس استفاده از متدهای AddContact و SetPrimaryContact برای ساختن شیء به صورت کامل.
3) استفاده از Builder برای ساختن شیء به صورت کامل و یکجا همراه با contactهای آن.
طراحی 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; } }
خوب، اولین مشکلی که در این پیاده سازی مشهود است، مربوط به متد Build هست. اگر مقدارهای سازنده کلاس Person را به صورت null ارسال کنیم، باعث خطا میشود و این خطا به این خاطر نیست که ما مقدار Null را به کلاس PersonBuilder ارسال کردهایم؛ زیرا ما تمام متدهای Set را با استفاده NullGurd مورد حفاظت قرار دادهایم. مشکل اصلی از وضعیت داخلی شیء PersonBuilder هست. اگر متدهای Set را فراخوانی نکنیم، تمام فیلدهای خصوصی، مقدار null میگیرند و یکی از راههای رفع این مشکل این است که پارامترها را از طریق سازنده PersonBuilder مقدار دهی کنیم. ولی کمی بعدتر متوجه خواهیم شد که این پیاده سازی مانند کلاس person هست و در نتیجه این روش بی استفاده است.
راه حل: استفاده از Interface Segregation principle در PersonBuilder :
اصل ISP میگوید: "کلاینتها نباید وابسته به متدهایی باشند که آنها را پیاده سازی نمیکنند." برای رسیدن به این امر در مثال بالا، باید آن واسط را به واسطهای کوچکتری تقسیم کرد. این تقسیم بندی باید بر اساس استفاده کنندگان از واسطها صورت گیرد.
برای اینکه شیء Person را بسازیم، متوجه خواهید شد بعضی از دادهها الزامی و بعضی دیگر اختیاری هستند؛ مانند PrimaryContact که از دادههای ضروری شیء Person است. ولی AllContacts میتواند به صورت اختیاری تعریف شود و در پیاده سازی PersonBuilder بالا، کلاینت متوجه نخواهد شد کدام متد اختیاری یا اجباری هست و در نتیجه ممکن است فراموش کند متد SetPrimaryContact را فراخوانی کند و همین مساله باعث میشود تا نرم افزار با خطا مواجه شود.
راه حل: به کد زیر توجه فرمایید:
class PersonBuilder { private PersonBuilder() { } public static IExpectSurnamePersonBuilder WithName(string name) { ... } }
همانطور که مشاهده میفرمایید، سازنده کلاس به صورت خصوصی تعریف شدهاست. درنتیجه بیرون از کلاس نمیتوان از آن وهله ساخت و آنرا مورد استفاده قرار داد و تنها راه وهله سازی از کلاس PersonBuilder از طریق متد WithName خواهد بود. ثانیا این متد PersonBuilder را برنمیگرداند؛ بلکه شیءایی را برمیگرداند که منتظر فراهم کردن مقدار Surname است و با استفاده از این روش میتوانیم پروسه فراخوانی متدها را مشخص کنیم.
درنتیجه پروسه ساختن شیء، به چندین قسمت تقسیم شده که به صورت زیر میباشد:
1) فراهم کردن مقدار Surname
2) فراهم کردن مقدار Name
3) فراهم کردن مقدار PrimaryContact
4) فراهم کردن مقدار سایر Contactهای شخص
5) ساختن شیء Person
پس به ازای هر کدام از عملیاتها، یک اینترفیس خواهیم داشت:
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; } }
و اگر کلاینت بخواهد وهلهای را از کلاس PersonBuilder بسازد، به صورت زیر خواهد بود:
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();
اصول طراحی ISP باعث میشوند، کد خواناتر شود و همین خوانایی سبب میگردد نگهداری و توسعه نرم افزار راحتتر شود.
چکیده:
ساختن اشیا در زبانهای object oriented کار بسیار سادهای است و همین سادگی، خطاهای جبران ناپذیری را به نرم افزار تحمیل میکنند و باعث ایجاد اشیایی ناپایدار در سیستم میشود. در اولین گام، الگوی طراحی Builder را به صورت ساده مورد بررسی قرار دادیم و در نهایت این طراحی را تا جای پیش بردیم که بتوانیم اشیایی پایدار را بسازیم. ولی این طراحی هنوز با مشکلاتی رو به رو هست؛ مانند نقض کردن قانون command query separation که این مشکل را در مقالهی بعدی برطرف خواهیم کرد.