در برنامه نویسی راه های مختلفی برای انجام یک کار وجود دارد. builderها نیز از این قاعده مستثنی نیستند.
قصد دارم چهار روش برای پیاده سازی الگوی Builder در سی شارپ را به شما ارائه کنم.
کلاسی است که شامل مجموعه APIهای user-friendly می باشد و clientها می توانند برای ساخت شئ از آن استفاده کنند.
یک builder باید جزئیات غیر ضروری ساخت شئ را تا حد امکان از دید clientها پنهان کند. بدین ترتیب ساختن صحیح شئ آسان و ساختن نامناسب آن سخت خواهد شد.
public class HtmlDocumentBuilder { private readonly StringBuilder _markup = new StringBuilder(); public void OpenTag(string tag) => _markup.Append($"<{tag}>"); public void AddText(string text) => _markup.Append(text); public void CloseTag(string tag) => _markup.Append($"</{tag}>"); public HtmlDocument Build() => new HtmlDocument(_markup.ToString()); } //Usage var builder = new HtmlDocumentBuilder(); builder.OpenTag("p"); builder.AddText("Text"); builder.CloseTag("p"); HtmlDocument document = builder.Build(); Console.WriteLine(document.Markup); //<p>Text</p>
در این مثال، HtmlDocumentBuilder شامل APIهای ساده ای برای اضافه کردن tags و text می باشد. اما clientها همچنان می توانند متد CloseTag را قبل از OpenTag فراخوانی کنند که این خطا است. با این حال الحاق رشته ها مستعد خطای بیشتری است.
نوعی از الگوهای طراحی builder است که متدهای آن کلاس بجز متد Build، یک شئ از وضعیت جاری همان کلاس را بر می گردانند.
public class HtmlDocumentBuilder { private readonly StringBuilder _markup = new StringBuilder(); public HtmlDocumentBuilder OpenTag(string tag) { _markup.Append($"<{tag}>"); return this; } public HtmlDocumentBuilder AddText(string text) { _markup.Append(text); return this; } public HtmlDocumentBuilder CloseTag(string tag) { _markup.Append($"</{tag}>"); return this; } public HtmlDocument Build() => new HtmlDocument(_markup.ToString()); } //Usage HtmlDocument document = new HtmlDocumentBuilder() .OpenTag("p") .AddText("Text") .CloseTag("p") .Build(); Console.WriteLine(document.Markup); //<p>Text</p>
این روش به برنامه نویس ها اجازه می دهد تا متدها را به هم زنجیره کنند، که زیباتر به نظر میرسد و کمی تکرار کد را کاهش میدهد.
یکی از اشکالات مثال قبل این است که می توان متدها را به هر ترتیبی فراخوانی کرد. برای مثال، زمانی که هنوز چیزی برای ساخت وجود ندارد، می توان متد Build را فراخوانی کرد. البته خطای کامپایل وجود نخواهد داشت. تنها راه اطلاع دادن به برنامه نویس ها مبنی بر اینکه کار اشتباهی انجام میدهند، انجام بررسی زمان اجرا و ایجاد استثنائات است.
با این حال، راهی برای پیاده سازی builder وجود دارد به این صورت که کامپایلر ترتیب فراخوانی متدها را بررسی می کند. پیاده سازی زیر باعث می شود که نتوان ابتدا متد Build را فراخوانی کرد.
public class HtmlDocumentBuilder { public HtmlDocumentBuilder() => _markup = new StringBuilder(); public HtmlDocumentBuilder(StringBuilder markup) => _markup = markup; protected readonly StringBuilder _markup; public HtmlDocumentBuilderFinal OpenTag(string tag) { _markup.Append($"<{tag}>"); return new HtmlDocumentBuilderFinal(_markup); } public HtmlDocumentBuilderFinal AddText(string text) { _markup.Append(text); return new HtmlDocumentBuilderFinal(_markup); } public HtmlDocumentBuilderFinal CloseTag(string tag) { _markup.Append($"</{tag}>"); return new HtmlDocumentBuilderFinal(_markup); } } public class HtmlDocumentBuilderFinal : HtmlDocumentBuilder { public HtmlDocumentBuilderFinal(StringBuilder markup) : base(markup) { } public HtmlDocument Build() => new HtmlDocument(_markup.ToString()); }
ایده این است که متد Build را در یک کلاس جداگانه قرار دهیم. با این حال، فراخوانی متد Build پس از فراخوانی هر متد دیگری از نوع HtmlDocumentBuilder به سادگی انجام می شود.
HtmlDocument document = new HtmlDocumentBuilder() .AddText("Text") .Build();
متد AddText و سایر متدها، شئ HtmlDocumentBuilderFinal را که حاوی متد Build است برمی گردانند.
در جریان ایجاد شئ، builder معمولاً نیاز دارد به پروپرتی های آن شئ دسترسی داشته باشد. این بدان معنی است که پروپرتی ها باید دارای setterهای public باشند که باعث می شود encapsulation خراب شود.
public class MailMessage { public string From { get; set; } public List<string> To { get; set; } = new List<string>(); //... } public class MailMessageBuilder { private readonly MailMessage _mailMessage = new MailMessage(); public MailMessageBuilder From(string email) { _mailMessage.From = email; return this; } public MailMessageBuilder To(string email) { _mailMessage.To.Add(email); return this; } public MailMessage Build() => _mailMessage; }
در این پیاده سازی، شی MailMessage باید List<T> را در معرض نمایش قرار دهد و setterهای آن نیز public باشد.
با ایجاد یک builder تو در تو، می توانیم به کپسوله کردن کامل MailMessage و ادامه استفاده از عملکرد builder، دست پیدا کنیم.
public class MailMessage { private List<string> _to { get; set; } = new List<string>(); private MailMessage() { } public string From { get; private set; } public IReadOnlyList<string> To => _to; //Nested builder public class Builder { private readonly MailMessage _mailMessage = new MailMessage(); public Builder From(string email) { _mailMessage.From = email; return this; } public Builder To(string email) { _mailMessage._to.Add(email); return this; } public MailMessage Build() => _mailMessage; } }
تمام setterها در MailMessage اکنون private هستند. Builder یک کلاس تو در تو است بنابراین می تواند به پروپرتی های private در MailMessage دسترسی پیدا کند.
علاوه بر این، سازنده کلاس MailMessage عمدا خصوصی شد. بنابراین تنها راه نمونه سازی MailMessage برای clientها، استفاده از APIهای builder می باشد.
var mailMessage = new MailMessage.Builder() .From("from@from.com") .To("to@to.com") .To("to2@to.com") .Build();
همچنین قرار دادن builder در شئ می تواند cohesion کد را بالا ببرد.
پایان