다음을 통해 공유


VB.NET Fluent Builder Design Pattern

Introduction

The Builder design pattern is a creational design pattern and can be used to create complex objects step by step.

Supposing we have an object with many dependencies and need to acquire each one of these dependencies, certain actions must be issued. In such cases, we can use the Builder pattern in order to:

  • Encapsulate, create, and assemble the parts of a complex object in a separate Builder object.
  • Delegate the object creation to a Builder object instead of creating the objects directly.

To summarize, by using the Builder design pattern, we were able to create a complex object and its complex parts.

Another thing that is great about builder design pattern is they promote breaking about methods that may have more than three arguments as a best practice for methods for working with business operations is less than three arguments. Still, some objects might have more than 3 attributes or properties and you usually need some way to initialize them via the constructor. Some attribute might not be mandatory, therefore on some occasions, you can get by with a few overloads adding more parameters as needed.

Description

Before looking at writing a builder pattern here are advantages and disadvantages - as mentioned by [Rohid Ayd]:

Advantages of Builder Design Pattern

  • The parameters to the constructor are reduced and are provided in highly readable method calls.
  • Builder design pattern also helps in minimizing the number of parameters in the constructor and thus there is no need to pass in null for optional parameters to the constructor.
  • The object is always instantiated in a complete state
  • Immutable objects can be built without much complex logic in the object building process.

Moving through this article, all examples are shown in Windows desktop code but can also be used in web solutions also. 

Disadvantages of Builder Design Pattern

  • The number of lines of code increases at least to double in builder pattern, but the effort pays off in terms of design flexibility and much more readable code.
  • Requires creating a separate ConcreteBuilder for each different type of class item.

Examples

Simple fast food

Easy to understand example, ordering a burger at a fast food place. Imagine you have a choice to add or not have cheese, Pepperoni, Lettuce or Tomato.

A conventional method to create the burger is with a new constructor e.g.

public Burger(size, cheese = True, pepperoni = True, tomato = False, lettuce = True)

This is what's called a telescoping constructor anti-pattern, as stated above a method should when humanly possible not have many arguments as it makes things difficult to understand.

A cleaner solution is to use a builder e.g.

Imports BaseLibrary.BaseClasses 
  
Namespace Builders 
    Public Class  BurgerBuilder 
        Public ReadOnly  Property Size() As Integer
  
        Private mCheese As Boolean
        Public Property  Cheese() As  Boolean
            Get
                Return mCheese 
            End Get
            Private Set(ByVal value As Boolean) 
                mCheese = value 
            End Set
        End Property
        Private mPepperoni As Boolean
        Public Property  Pepperoni() As  Boolean
            Get
                Return mPepperoni 
            End Get
            Private Set(ByVal value As Boolean) 
                mPepperoni = value 
            End Set
        End Property
        Private mLettuce As Boolean
        Public Property  Lettuce() As  Boolean
            Get
                Return mLettuce 
            End Get
            Private Set(ByVal value As Boolean) 
                mLettuce = value 
            End Set
        End Property
        Private mTomato As Boolean
        Public Property  Tomato() As  Boolean
            Get
                Return mTomato 
            End Get
            Private Set(ByVal value As Boolean) 
                mTomato = value 
            End Set
        End Property
  
        Public Sub  New(ByVal size As Integer) 
            Me.Size = size 
        End Sub
  
        Public Function  AddPepperoni() As  BurgerBuilder 
            Pepperoni = True
            Return Me
        End Function
  
        Public Function  AddLettuce() As  BurgerBuilder 
            Lettuce = True
            Return Me
        End Function
  
        Public Function  AddCheese() As  BurgerBuilder 
            Cheese = True
            Return Me
        End Function
  
        Public Function  AddTomato() As  BurgerBuilder 
            Tomato = True
            Return Me
        End Function
  
        Public Function  Build() As  Burger 
            Return New  Burger(Me) 
        End Function
    End Class
End Namespace

Then we have the burger class which uses the builder class above.

Namespace BaseClasses 
    Public Class  Burger 
        Public ReadOnly  Property Size() As Integer
        Public ReadOnly  Property Cheese() As Boolean
        Public ReadOnly  Property Pepperoni() As Boolean
        Public ReadOnly  Property Lettuce() As Boolean
        Public ReadOnly  Property Tomato() As Boolean
  
        Public Sub  New(builder As BurgerBuilder) 
            Size = builder.Size 
            Cheese = builder.Cheese 
            Pepperoni = builder.Pepperoni 
            Lettuce = builder.Lettuce 
            Tomato = builder.Tomato 
        End Sub
    End Class
End Namespace

To construct the burger using the builder class. 

Module Module1 
  
    Sub Main() 
  
        Dim burger = (New BurgerBuilder(14)). 
                AddPepperoni(). 
                AddLettuce(). 
                AddTomato(). 
                Build() 
    End Sub
  
End Module

This is easier to understand and worth those extra lines of code than using a multi-argument constructor. 

Email example

A good example for working with a builder pattern is sending email messages where there are many parts to properly construct an email message and can become very complex and hard to maintain later on plus in today’s world more time than not a tester have eye’s on the code who may not fully understand code that is shown that follows.

Namespace Classes 
    Module SimpleMailOperations 
        Public Sub  Demo1() 
            Dim mail As New  MailMessage() 
            mail.From = New  MailAddress("jane@comcast.net") 
            mail.To.Add("bill@comcast.net") 
            mail.Subject = "This is an email"
  
            Dim plainMessage As AlternateView = 
                    AlternateView.CreateAlternateViewFromString( 
                        "Hello, plain text", Nothing, "text/plain")  
  
            Dim htmlMessage As AlternateView = 
                    AlternateView.CreateAlternateViewFromString( 
                        "This is an automated email, please do not respond<br><br>An exception " & 
                        "ocurred in <br><span style=""font-weight: bold; padding-left: 20px;" & 
                        "padding-right:5px"">Application name</span>MyApp<br>" & 
                        "<span style=""font-weight: bold; " & 
                        " padding-left: 5px;padding-right:5px"">Application Version</span>" & 
                        "1.00<br><span style=""font-weight: bold; padding-left: " & 
                        "70px;padding-right:5px"">", Nothing, "text/html")  
  
            mail.AlternateViews.Add(plainMessage) 
            mail.AlternateViews.Add(htmlMessage) 
            Dim smtp As New  SmtpClient("smtp.comcast.net") 
            smtp.Send(mail) 
        End Sub
  
    End Module
End Namespace

Considering that the code sample just presented there are various parts that can be improved upon even without a builder pattern which leads into an example of a builder pattern for sending email messages.

Dim mailer As New  MailBuilder() 
  
mailer.CreateMail(GmailConfiguration1). 
    WithRecipient("karen@comcast.net"). 
    WithCarbonCopy("mary@gmail.com"). 
    WithSubject("Test"). 
    AsRichContent(). 
    WithHtmlView("<p>Hello <strong>Bob</strong></p>"). 
    WithPickupFolder(). 
    WithTimeout(2000). 
    SendMessage()

The MailBuilder class provides spoken work methods easy to read and understand rather than, for some difficult to read the code. Each method in the MailBuilder class returns an instance of itself which is known as chaining. 


Going back to hiding complexity, the first part of the chain handles configuring the client which is the transport for sending an email message.

Public Function  CreateMail(pConfiguration As String) As  MailBuilder 
  
    Configuration = New  MailConfiguration(pConfiguration) 
  
    ConfigurationSection = pConfiguration 
  
    Client = New  SmtpClient(Configuration.Host, Configuration.Port) With { 
        .Credentials = New  NetworkCredential(Configuration.UserName, Configuration.Password), 
        .EnableSsl = True, 
        .Timeout = Configuration.TimeOut 
    } 
  
    Message = New  MailMessage() With
        { 
            .From = New  MailAddress(Configuration.FromAddress), 
            .IsBodyHtml = False
        } 
  
    Return Me
  
End Function

Which in turn creates an instance of MailConfiguration responsible for reading setting from an application's configuration file (see example). 
.
Back to the example above, WithRecipient and WithCarbonCopy, both add to MailMessage where both methods below permit multiple entries no different when sending email through an email application such as Microsoft Outlook.

Public Function  WithRecipient(pSender As String) As  MailBuilder 
  
    Message.To.Add(pSender) 
  
    Return Me
  
End Function
Public Function  WithCarbonCopy(pSender As String) As  MailBuilder 
  
    Message.CC.Add(pSender) 
  
    Return Me
  
End Function

Once all desired parts are set the method SendMessage is called which uses values presented to compose and send an email message. 

To run test see the following code sample. Note in this code sample email message are sent to a folder below the application folder which is done via a post-build event setup in project properties.

Working with databases

Reading

Another use for the builder pattern is any operation typically performed with database operations. In this section reading, data and updating will be explored using the builder pattern. What is not shown is adding and removal of records yet they can also use the builder pattern from first working with the code examples below. 

What the builder pattern will provide, the ability to read all data from joined tables returning all records, an example for filtering by country. Since when there is a chance for editing primary keys are needed but not to be displayed so there are chain methods to indicate to show or hide primary keys. Also is an ORDER BY is needed, which column and ASC DESC order. These can be expanded upon, the entire idea here is to open a developer's mind to possibilities.

To read all customer data, order by the last name in descending order not showing any primary keys and first setting the return data to a BindingSource component which becomes the data source of a DataGridView the following pattern handles this, the same chaining of methods as done with the prior section for sending email messages.

Public Class  Form1 
  
    Private Sub  Form1_Shown(sender As Object, e As  EventArgs) Handles  Me.Shown 
        Dim initialOrderByFieldName = "LastName"
  
        Dim customerReader = New CustomersBuilder 
  
        bindingSourceCustomers.DataSource = customerReader. 
            Begin(). 
            OrderBy("LastName"). 
            DescendingOrder. 
            WithoutIdentifiers(). 
            ReadAllCustomersRecords() 
  
        DataGridView1.DataSource = bindingSourceCustomers

Suppose customers want to control sorting and order of a sort, present them with controls such as ComboBox and CheckBox controls with a button to perform the sort.

Sample builder pattern, in this case done in sections rather than in a continuous chain.

Private Sub  readCustomersButton_Click(sender As Object, e As  EventArgs)  
  
    bindingSourceCustomers.DataSource = Nothing
  
    Dim customerReader = New CustomersBuilder 
  
    customerReader.Begin() 
    customerReader.OrderBy(columnNamesComboBox.Text) 
  
    If decendingOrderCheckBox.Checked Then
        customerReader.DescendingOrder() 
    Else
        customerReader.AscendingOrder() 
    End If
  
    bindingSourceCustomers.DataSource = customerReader.ReadAllCustomersRecords() 
    DataGridView1.DataSource = bindingSourceCustomers 
  
    DataGridView1.ExpandAllColumns 
    DataGridView1.NormalizeColumnHeaders() 
  
End Sub

Another possibility is to only show records meeting a condition, in this case by country using a ComboBox to select from.

Private Sub readCustomersByCountryButton_Click(sender As Object, e As EventArgs) Handles readCustomersByCountryButton.Click 
    bindingSourceCustomers.DataSource = Nothing 
 
    Dim customerReader = New CustomersBuilder 
 
    bindingSourceCustomers.DataSource = customerReader. 
        Begin(). 
        NoOrderBy. 
        WhereCountryIdentifierIs(CType(countriesComboBox.SelectedItem, Country).Identifier). 
        ReadCustomerRecordsByCountry() 
 
 
    DataGridView1.DataSource = bindingSourceCustomers 
 
    DataGridView1.ExpandAllColumns 
    DataGridView1.NormalizeColumnHeaders() 
 
End Sub

Updating

Using the same builder class other operations can be performed. In the following section, the builder class will be used to update a contact phone information where there are several tables which are required to first percent data then to edit data. The importance of knowing back end data structure is that this adds to the complexity and by using a builder fluent pattern although there is a little more code then without chaining via the builder pattern later down the road the code is easier to read and maintain.  

First, the CustomersBuilder is created followed by specifying which contact to update, the phone type (Office, Cell, Home), the status and finally to perform the update operation.

Dim customerReader = New CustomersBuilder 
  
Dim result = customerReader. 
        Begin(). 
        UpdateContactPhone(contact.Id). 
        SetContactPhoneTypeAs(contact.PhoneType). 
        WithPhoneNumber(contact.PhoneNumber). 
        PhoneStatusIsActive(contact.Active). 
        UpdateContactPhoneDetails() 
  
If Not  result Then
    MessageBox.Show($"Failed to update contact for {companyName}") 
End If

Once UpdateContactPhoneDetails is called the code path moves to a data class which takes one argument of type Contact. The method to update is shown below, a method which can also be called without using the CustomersBuilder class.

Public Function  UpdatePhone(contact As Contact, Optional showCommand As Boolean  = False) As  Boolean
    Dim updateStatement As String  = 
            "UPDATE dbo.ContactContactDevices " & 
            "SET " & 
            "PhoneTypeIdenitfier = @PhoneTypeIdenitfier, " & 
            "PhoneNumber = @PhoneNumber ," & 
            "Active = @Active " & 
            "WHERE ContactIdentifier = @ContactIdentifier"
  
    Using cn As  New SqlConnection With {.ConnectionString = ConnectionString} 
        Using cmd As  New SqlCommand With {.Connection = cn, .CommandText = updateStatement}  
  
            cmd.Parameters.AddWithValue("@PhoneTypeIdenitfier", contact.PhoneTypeIdenitfier) 
            cmd.Parameters.AddWithValue("@PhoneNumber", contact.PhoneNumber) 
            cmd.Parameters.AddWithValue("@Active", contact.Active) 
            cmd.Parameters.AddWithValue("@ContactIdentifier", contact.Id) 
  
            If showCommand Then
                Console.WriteLine(cmd.ActualCommandText()) 
            End If
  
            Try
                cn.Open() 
                cmd.ExecuteNonQuery() 
            Catch ex As Exception 
                mHasException = True
                mLastException = ex 
            End Try
        End Using 
    End Using 
  
    Return IsSuccessFul 
  
End Function

By using a builder pattern coupled with a data class everything is segmented, allows a developer to use or forgo the builder class. If the update method existed prior to the builder class this means the developer did a good job of writing the update method, one argument and using a base class for exception handling. 

Misc/Reports

Another use for a builder is for creating reports that for printing or simply displaying in the user interface. To keep it simple the report will be on products.
The product class

Namespace ProductBuilderClasses 
    Public Class  Product 
        Public Property  Name() As  String
        Public Property  Price() As  Double
    End Class
End Namespace

The first step is to define a class for the report. This is the object, we are going to build with the Builder design pattern.

Namespace ProductBuilderClasses 
  
    Public Class  ProductStockReport 
        Public Property  HeaderPart() As  String
        Public Property  BodyPart() As  String
        Public Property  FooterPart() As  String
  
        Public Overrides  Function ToString() As String
  
            Return New  StringBuilder(). 
                AppendLine(HeaderPart). 
                AppendLine(BodyPart). 
                AppendLine(FooterPart). 
                ToString() 
  
        End Function
    End Class
End Namespace

Now we need a builder interface to organize the building process:

Namespace Interfaces 
    Public Interface  IProductStockReportBuilder 
        Function BuildHeader() As IProductStockReportBuilder 
        Function BuildBody() As IProductStockReportBuilder 
        Function BuildFooter() As IProductStockReportBuilder 
        Function GetReport() As ProductStockReport 
    End Interface
End Namespace

Here is the concrete builder class which is going to implement this interface, needs to create all the parts for our stock report object and return that object as well. So, let’s implement our concrete builder class: 

Namespace ProductBuilderClasses 
    Public Class  ProductStockReportBuilder 
        Implements IProductStockReportBuilder 
  
        Private productStockReport As ProductStockReport 
        Private products As IEnumerable(Of Product) 
  
        Public Sub  New(products As IEnumerable(Of Product)) 
            Me.products = products 
            productStockReport = New  ProductStockReport() 
        End Sub
        Private Function  BuildHeader() As  IProductStockReportBuilder _ 
            Implements IProductStockReportBuilder.BuildHeader 
  
            productStockReport. 
                HeaderPart = $"REPORT FOR PRODUCTS ON DATE: {Date.Now}{Environment.NewLine}"
  
            Return Me
        End Function
  
        Private Function  BuildBody() As  IProductStockReportBuilder _ 
            Implements IProductStockReportBuilder.BuildBody 
  
            productStockReport.BodyPart = String.Join( 
                Environment.NewLine, 
                products.Select(Function(p) $"Product name: {p.Name,8}, product price: {p.Price}")) 
  
            Return Me
  
        End Function
  
        Private Function  BuildFooter() As  IProductStockReportBuilder _ 
            Implements IProductStockReportBuilder.BuildFooter 
  
            productStockReport.FooterPart = vbLf & "Report provided by the ABC company."
  
            Return Me
  
        End Function
  
        Public Function  GetReport() As  ProductStockReport _ 
            Implements IProductStockReportBuilder.GetReport 
  
            Dim productStockReport = Me.productStockReport 
  
            Clear() 
  
            Return productStockReport 
  
        End Function
  
        Private Sub  Clear() 
            productStockReport = New  ProductStockReport() 
        End Sub
    End Class
End Namespace

Now it's time to build the object.

Namespace ProductBuilderClasses 
  
    Public Class  ProductBuilderDemo 
        Public Sub  New() 
            Dim products = New List(Of Product) From 
                    { 
                        New Product With {.Name = "Monitor", .Price = 200.5}, 
                        New Product With {.Name = "Mouse", .Price = 20.41}, 
                        New Product With {.Name = "Keyboard", .Price = 30.15} 
                    } 
  
            Dim builder = New ProductStockReportBuilder(products) 
            Dim director = New ProductStockReportDirector(builder) 
            director.BuildStockReport() 
  
            Dim report = builder.GetReport() 
            ' 
            ' Display to the console 
            ' 
            Console.WriteLine(report) 
        End Sub
    End Class
End Namespace

Test, in this case, is done in a console application. 

Module Module1 
  
    Sub Main() 
        Dim demo As New  ProductBuilderDemo 
        Console.ReadLine() 
    End Sub
  
End Module

Results

Summary 

In this article/code sample the Builder pattern has been shown with real-world examples on how the Builder pattern might be applied coupled with advantages and disadvantages of the pattern.  Having options such as this pattern can make code easier to read and maintain.

See also

SQL-Server- C# Find duplicate record with the identity  
Builder Pattern C#  
Builder Pattern  
Builder Pattern  

Source code

Although there is an attached Visual Studio 2017 solution there is also the same code on a GitHub repository which over time may get updated while not always in this code sample.