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
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.