الگوی Builder

الگوی Builder، امکان تفکیک ساخت یک آبجکت پیچیده از representation آن را فراهم می کند. با این هدف که یک فرآیند ساخت بتواند برای representationهای متفاوت استفاده شود.الگوی builderدر الگوی builder، بخش های متفاوتی وجود دارد. بخش Director که منطق construction process را می شناسد و با استفاده از اینترفیس builder می تواند یک object را بسازد.بخش Builder، یک abstraction می باشد که طرز ساختن اجزای یک object را توصیف می کند.بخش Concrete Builder، یکی از پیاده سازی های builder است که می تواند یک representation را بسازد.کلاس director در زمان ساخته شدن، یک instance از builder را توسط client دریافت می کند. director که منطق construction را می داند، با استفاده از آن builder، عملیات construction process را انجام می دهد. با توجه به اینکه builder توسط client به director پاس داده می شود، فرآیند ساخت به representation وابستگی ندارد. زمانی که نیاز داریم الگوریتم ساخت یک object، از چگونگی assemble شدن اجزای آن object جدا باشد، می توانیم از الگوی builder استفاده کنیم.همچنین زمانی که قصد داریم construction process با representationهای متفاوت را reuse کنیم نیز می توانیم از این الگو استفاده کنیم. از آنجایی که builder یک اینترفیس را برای director تعریف می کند، این اینترفیس باعث می شود که product representation متفاوت باشد. بنابراین می توانیم product را با هر نوع representation ارائه کنیم.فرض کنید یک class به نام Document داریم که دارای sub classهای متفاوتی است و آن را به عنوان product در نظر می گیریم.public class Document { } public class HtmlDocument : Document { } public class PdfDocument : Document { } public class XmlDocument : Document { }و یک اینترفیس به نام IReportBuilder داریم که از طریق متد Build یک Document را برمی گرداند.public interface IReportBuilder { void AddTitle(string title); void AddParagraph(string content); void Footer(string text); Document Build(); }می توانیم با ایجاد کردن یک کلاس با عنوان HtmlReportBuilder که IReportBuilder را implement کرده است، یک HtmlDocument را برگردانیم.public class HtmlReportBuilder : IReportBuilder { public void AddTitle(string title) { throw new System.NotImplementedException(); } public void AddParagraph(string content) { throw new System.NotImplementedException(); } public void Footer(string text) { throw new System.NotImplementedException(); } public Document Build() { return new HtmlDocument(); } }بنابراین builder این اجازه را به ما می دهد که product's representation را تغییر دهیم و یک product جدید اضافه کنیم. برای sub classهای PdfDocument و XmlPdfDocument نیز کلاس builder اضافه می کنیم و منطق تولید product را پیاده سازی می کنیم و construction process تغییر نمی کند. کلاس director، از IReportBuilder برای ساخت یک document استفاده می کند. نوع builder که به آن پاس داده می شود اهمیت ندارد زیرا construction process به representation وابستگی ندارد و در مورد فرآیند ساخت صحبت می کند. یک کلاس director برای ارائه گزارش سود در نظر می گیریم.public class ProfitReportGenerator { private readonly IReportBuilder _reportBuilder; public ProfitReportGenerator(IReportBuilder reportBuilder) { _reportBuilder = reportBuilder; } public void Parse(string filePath) { var content = LoadFile(filePath); _reportBuilder.AddTitle(&quotProfit Report&quot); // ...for... _reportBuilder.AddParagraph(&quot....&quot); _reportBuilder.AddParagraph(&quot....&quot); } }از بیرون ReportBuilder را دریافت کردیم و از طریق یک فایل، دیتا را خواندیم و با استفاده از منطق خود، آن را parse کردیم. به همین ترتیب می توانیم directorهای دیگری اضافه کنیم. client کلاس director را می شناسد و با پاس دادن builder، نتیجه را از آن دریافت می کند.var builder = new HtmlReportBuilder(); var reportGenerator = new ProfitReportGenerator(builder); reportGenerator.Parse(&quot...&quot); var report = builder.Build();به این ترتیب، client در مورد representation مربوط به product، اطلاعاتی ندارد. به این دلیل که آن ها در builder interface تعریف نشده اند. از آنجایی که builder به ما apiهایی می دهد که قدم به قدم یک object را بسازیم، افرادی هستند که صرفا از این قابلیت آن استفاده می کنند و construction process را جدا نمی کنند. var builder = new HtmlReportBuilder(); builder.AddTitle(&quottitle&quot); builder.AddParagraph(&quotparagraph1&quot); builder.AddParagraph(&quotparagraph2&quot); var resport = builder.Build();برای مثال می توانید builder موجود در dotnet framework را در نظر بگیرید.var builder = new SqlConnectionStringBuilder(); builder.DataSource = &quot192.168.40.52&quot builder.ConnectTimeout = 20; builder.InitialCatalog = &quottest-db&quot var connectionString = builder.ConnectionString; // Buildدر اینجا آبجکتی که قرار است ساخته شود، آبجکتی است که حالت های مختلفی برای ساخته شدن دارد و client مراحل مختلفی را برای آن مشخص می کند. بنابراین بجای استفاده از overloadهای مختلف، یک builder در اختیار client قرار می گیرید.این مدل از builder، یک نوع ساده شده می باشد که در آن director وجود ندارد و client برای ساخت مرحله به مرحله یک object از آن استفاده می کند. اصطلاحا به آن Simple Builder گفته می شود. بنابراین این نوع را می توانیم بخشی از الگوی builder در نظر بگیریم و برای حل یک دسته از مسائل می تواند استفاده شود. در simple builder از تکنیک های پیاده سازی می توانیم استفاده کنیم. یک کلاس HtmlDocumentBuilder در نظر بگیرید که حالت های مختلفی می تواند داشته باشد. برای استفاده راحت تر از این builder، از تکنیک chaining method استفاده می کنیم. به این صورت که هر متد یک HtmlDocumentBuilder را به عنوان خروجی برمی گرداند. public class HtmlDocumentBuilder { public HtmlDocumentBuilder AddHeader(string header) { //... return this; } public HtmlDocumentBuilder AddParagraph(string content) { //... return this; } public HtmlDocumentBuilder AddTable(string content) { //... return this; } public string Build() { return null; } }در نتیجه می توانیم از این builder به این صورت استفاده کنیم. var output = new HtmlDocumentBuilder() .AddHeader(&quotX&quot) .AddParagraph(&quottest 1&quot) .AddParagraph(&quottest 2&quot) .AddParagraph(&quottest 3&quot) .Build();می توانیم از یک builder در builder دیگر استفاده کنیم.یک builder class با نام UrlBuilder در نظر بگیرید.public class UrlBuilder { private string _scheme = &quothttp&quot private string _host = &quotlocalhost&quot private string _port = string.Empty; private QueryStringBuilder _queryStringBuilder; public UrlBuilder WithScheme(string scheme) { _scheme = scheme; return this; } public UrlBuilder WithHost(string host) { _host = host; return this; } public UrlBuilder WithPort(int port) { _port = $&quot:{port}&quot return this; } public UrlBuilder WithQueryString(Action<QueryStringBuilder> builderConfigurator) { var builder = new QueryStringBuilder(); builderConfigurator.Invoke(builder); _queryStringBuilder = builder; return this; } public string Build() { var queryString = _queryStringBuilder?.Build(); return $&quot{_scheme}://{_host}{_port}/{queryString}&quot } }این builder، با وارد کردن مقادیر پیش فرض و متدهای مختلفی که ایجاد کرده است می تواند به client برای ایجاد حالت های مختلف، کمک کند.var url = new UrlBuilder() .WithScheme(&quothttps&quot) .WithHost(&quotgoogle.com&quot) .Build();در این مثال، QueryStringBuilder به عنوان یک nested builder استفاده می شود و برای اینکه client درگیر تایپ آن نشود، در متد WithQueryString یک delegate را به عنوان ورودی دریافت کردیم.public class QueryStringBuilder { private Dictionary<string, string> _parameters = new Dictionary<string, string>(); public QueryStringBuilder WithParam(string key, string value) { _parameters.Add(key, value); return this; } public string Build() { // build implement return string.Empty; } }بنابراین client می تواند به این شکل از آن استفاده کند.var url = new UrlBuilder() .WithHost(&quotmySite.com/index&quot) .WithQueryString(b => b.WithParam(&quotpage&quot, &quot1&quot).WithParam(&quottake&quot, &quot20&quot)) .WithPort(2020) .Build();مساله دیگری که ممکن است در پیاده سازی builder مشاهده شود، موضوع step builder است که در نتیجه استفاده از آن، client به استفاده از یکسری مراحل مشخص force می شود. یک کلاس builder با عنوان HttpRequestBuilder در نظر بگیرید که یک HttpRequestMessage را در حالت های مختلف ایجاد می کند. public class HttpRequestBuilder { private HttpMethod _method; public HttpRequestBuilder Get() { _method = HttpMethod.Get; return this; } public HttpRequestBuilder Post() { _method = HttpMethod.Post; return this; } public HttpRequestBuilder Put() { _method = HttpMethod.Put; return this; } public HttpRequestBuilder WithBody(string body) { return this; } public HttpRequestMessage Build() { return null; } }موضوعی که در استفاده از این builder وجود دارد، صدا زدن متد WithBody بعد از متد Get کار منطقی به نظر نمی رسد. همچنین صدا زدن متدهای Get, Post, Put در کنار هم نیز منطقی نیست.هدف ما این است که client را به یک حرکت مشخص force کنیم. در step builder با استفاده از شکوندن interfaceها به interfaceهای کوچک تر، می توانیم مراحل دلخواه خود را ست کنیم. بنابراین ما نیز این کار را انجام می دهیم.public interface IHttpRequestBuilder { IHttpRequestBuilder Url(string url); HttpRequestMessage Build(); }public interface IHttpRequestMethodBuilder { IHttpRequestBuilder Get(); IHttpRequestContentBuilder Post(); IHttpRequestContentBuilder Put(); }public interface IHttpRequestContentBuilder { IHttpRequestBuilder WithBody(string body); }در IHttpRequestMethodBuilder، اگر Get صدا زده شود IHttpRequestBuilder برمی گردد که شامل متدهای Url و Build است. و اگر Post و Put صدا زده شود، IHttpRequestContentBuilder بر می گردد که شامل متد WithBody می باشد. بعد از صدا زدن WithBody می توان از IHttpRequestBuilder استفاده کرد.بنابراین از طریق segregate کردن اینترفیس های builder، سعی می کنیم که ترتیب را برای client مشخص کنیم.کلاس HttpRequestBuilder را به شکل زیر تغییر می دهیم.internal class HttpRequestBuilder : IHttpRequestBuilder, IHttpRequestMethodBuilder, IHttpRequestContentBuilder { private string _url = string.Empty; private string _content = string.Empty; private HttpMethod _method = HttpMethod.Get; internal HttpRequestBuilder() { } public IHttpRequestBuilder Url(string url) { _url = url; return this; } public IHttpRequestBuilder Get() { return this; } public IHttpRequestContentBuilder Post() { return this; } public IHttpRequestContentBuilder Put() { return this; } public IHttpRequestBuilder WithBody(string body) { return this; } public HttpRequestMessage Build() { throw new NotImplementedException(); } }در لحظه استفاده، اگر موفق شویم که از خود کلاس builder مستقیما instantiate کنیم، تمام متدهای آن را خواهیم دید. در صورتی که ما نمی خواهیم چنین اتفاقی بیافتد. بنابراین کلاس خود را internal کردیم که از بیرون دیده نشود و از طریق یک static factory، شرایط استفاده از آن را فراهم می کنیم.public static class HttpRequestFactory { public static IHttpRequestMethodBuilder NewRequest() { return new HttpRequestBuilder(); } }در این کلاس نیز از طریق متد NewRequest، کار را با اینترفیس IHttpRequestMethodBuilder شروع می کنیم. همچنین می توانستیم در کلاس HttpRequestBuilder، از طریق private کردن constructor و اضافه کردن متد NewRequest در این کلاس نیز به این هدف برسیم.روش استفاده به این ترتیب می شود. var builder = HttpRequestFactory .NewRequest() .Put() .WithBody(&quot&quot) .Url(&quothttp://localhost&quot) .Build();به طور خلاصه، الگوی builder با تفکیک construction process از representation، این امکان را به ما می دهد که بتوانیم از یک construction process، برای رسیدن به representationهای متفاوت استفاده کنیم. در این الگو representation را به عنوان variability در نظر می گیریم و construction process را به عنوان commonality در نظر می گیریم.