ویرگول
ورودثبت نام
Mohsen Farokhi - محسن فرخی
Mohsen Farokhi - محسن فرخیSenior .NET Developer
Mohsen Farokhi - محسن فرخی
Mohsen Farokhi - محسن فرخی
خواندن ۸ دقیقه·۴ سال پیش

الگوی Builder

الگوی Builder، امکان تفکیک ساخت یک آبجکت پیچیده از representation آن را فراهم می کند. با این هدف که یک construction process بتواند برای representationهای متفاوت استفاده شود. در اینجا به بررسی جزئیات الگوی Builder، ساختار آن، مثال‌هایی از کاربرد و انواع پیاده‌سازی آن می‌پردازیم.

الگوی builder
الگوی builder

الگوی Builder شامل اجزای زیر است:

Director

  • مسئول مدیریت construction process است.
  • منطق کلی ساخت شیء را می‌شناسد و با استفاده از اینترفیس Builder اقدام به ساخت شیء می‌کند.

Builder

  • یک انتزاع (abstraction) برای تعریف چگونگی ساخت اجزای مختلف شیء ارائه می‌دهد.

Concrete Builder

  • یک پیاده‌سازی خاص از Builder است که می تواند یک representation را بسازد.

Product

  • شیء نهایی که توسط Concrete Builder ساخته می‌شود.

ویژگی‌های الگوی Builder

  • تفکیک construction process از نوع representation: این تفکیک امکان ایجاد انواع مختلفی از representation را با استفاده از یک فرآیند ساخت مشترک فراهم می‌کند.
  • انعطاف‌پذیری در استفاده از Builderها: Director نیازی به دانستن جزئیات پیاده‌سازی نوع خاصی از Builder ندارد؛ بنابراین می‌توان به راحتی نوع جدیدی از representation را اضافه کرد.

زمان استفاده از الگوی Builder

  • زمانی که الگوریتم ساخت یک شیء باید از چگونگی assemble شدن اجزای آن جدا باشد.
  • زمانی که نیاز داریم construction process برای representationهای متفاوت قابل استفاده مجدد باشد.

مثالی از الگوی Builder

فرض کنید یک کلاسDocumentداریم که انواع مختلفی از آن مانندHtmlDocument ،PdfDocument و XmlDocument وجود دارد. همچنین، یک اینترفیسIReportBuilder تعریف می‌کنیم که متدهای لازم برای ساخت یک گزارش را مشخص می‌کند.

public class Document { } public class HtmlDocument : Document { } public class PdfDocument : Document { } public class XmlDocument : Document { }
public interface IReportBuilder { void AddTitle(string title); void AddParagraph(string content); void AddFooter(string footer); Document Build(); }

یک پیاده‌سازی برای ساخت گزارش‌های HTML می‌تواند به صورت زیر باشد:

public class HtmlReportBuilder : IReportBuilder { public void AddTitle(string title) { /* پیاده‌سازی */ } public void AddParagraph(string content) { /* پیاده‌سازی */ } public void AddFooter(string footer) { /* پیاده‌سازی */ } public Document Build() => new HtmlDocument(); }

بنابراین builder این اجازه را به ما می دهد که product's representation را تغییر دهیم و یک product جدید اضافه کنیم. برای sub classهای PdfDocument و XmlPdfDocument نیز کلاس builder اضافه می کنیم و منطق تولید product را پیاده سازی می کنیم درحالی که construction process تغییر نمی کند.

کلاس Director منطق ساخت document را می‌شناسد و با استفاده از Builder، یک document تولید می‌کند.

مثال زیر یک کلاس برای تولید گزارش سود است:

public class ProfitReportGenerator { private readonly IReportBuilder _reportBuilder; public ProfitReportGenerator(IReportBuilder reportBuilder) { _reportBuilder = reportBuilder; } public void Parse(string filePath) { var content = LoadFile(filePath); _reportBuilder.AddTitle("Profit Report"); _reportBuilder.AddParagraph(content); _reportBuilder.AddFooter("End of Report"); } }

برای استفاده از Director:

var builder = new HtmlReportBuilder(); var reportGenerator = new ProfitReportGenerator(builder); reportGenerator.Parse("data.csv"); var report = builder.Build();

به این ترتیب، client در مورد representation مربوط به product، اطلاعاتی ندارد. به این دلیل که آن ها در builder interface تعریف نشده اند.


از آنجایی که builder به ما apiهایی می دهد که قدم به قدم یک object را بسازیم، افرادی هستند که صرفا از این قابلیت آن استفاده می کنند و construction process را جدا نمی کنند.

var builder = new HtmlReportBuilder(); builder.AddTitle("title"); builder.AddParagraph("paragraph1"); builder.AddParagraph("paragraph2"); var resport = builder.Build();

برای مثال می توانید builder موجود در dotnet framework را در نظر بگیرید.

var builder = new SqlConnectionStringBuilder(); builder.DataSource = "192.168.40.52" builder.ConnectTimeout = 20; builder.InitialCatalog = "test-db" var connectionString = builder.ConnectionString; // Build

در اینجا آبجکتی که قرار است ساخته شود، آبجکتی است که حالت های مختلفی برای ساخته شدن دارد و client مراحل مختلفی را برای آن مشخص می کند. بنابراین بجای استفاده از overloadهای مختلف، یک builder در اختیار client قرار می گیرید.

این مدل از builder، یک نوع ساده شده می باشد که در آن director وجود ندارد و client برای ساخت مرحله به مرحله یک object از آن استفاده می کند. اصطلاحا به آن Simple Builder گفته می شود. بنابراین این نوع را می توانیم بخشی از الگوی builder در نظر بگیریم و برای حل یک دسته از مسائل می تواند استفاده شود.

در simple builder از تکنیک های پیاده سازی می توانیم استفاده کنیم. یک کلاس HtmlDocumentBuilder در نظر بگیرید که حالت های مختلفی می تواند داشته باشد. برای استفاده راحت تر از این builder، از تکنیک Chaining Builder استفاده می کنیم. به این صورت که هر متد یک 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("X") .AddParagraph("test 1") .AddParagraph("test 2") .AddParagraph("test 3") .Build();

می توانیم از یک builder در builder دیگر استفاده کنیم.

یک builder class با نام UrlBuilder در نظر بگیرید.

public class UrlBuilder { private string _scheme = "http" private string _host = "localhost" 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 = $":{port}" 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 $"{_scheme}://{_host}{_port}/{queryString}" } }

این builder، با وارد کردن مقادیر پیش فرض و متدهای مختلفی که ایجاد کرده است می تواند به client برای ایجاد حالت های مختلف، کمک کند.

var url = new UrlBuilder() .WithScheme("https") .WithHost("google.com") .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("mySite.com/index") .WithQueryString(b => b.WithParam("page", "1").WithParam("take", "20")) .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("") .Url("http://localhost") .Build();


به طور خلاصه، الگوی builder با تفکیک construction process از representation، این امکان را به ما می دهد که بتوانیم از یک construction process، برای رسیدن به representationهای متفاوت استفاده کنیم. در این الگو representation را به عنوان variability در نظر می گیریم و construction process را به عنوان commonality در نظر می گیریم.

design pattern
۴
۰
Mohsen Farokhi - محسن فرخی
Mohsen Farokhi - محسن فرخی
Senior .NET Developer
شاید از این پست‌ها خوشتان بیاید