- Published on
Learning Design Patterns: Builder Pattern - A Surefire Guide
- Authors
- Name
- Dan Tech
- @dan_0xff
The Builder Pattern is a very common design pattern. It makes our code safer and easier to write Unit Tests. Read on to understand how to implement it for your programming language!
The Problem With Builder Pattern
In the process of software development, we often face the task of building complex objects. These objects may include many different components, with varying creation steps.
Imagine you are building a feature to create different reports. A report may include a title, text content, tables, charts, and footers. Arranging and formatting these components can vary depending on the type of report (e.g., financial, performance, ...).
The challenge here is how to separate the process of creating the report's components from how they are arranged and presented. Otherwise, our source code will become cluttered, difficult to maintain, and hard to extend.
Code Without Builder Pattern
data class Report(
val title: String,
val textContent: String,
val table: String?,
val chart: String?,
val footer: String?
)
class ReportGenerator {
fun generateReport(
title: String,
textContent: String,
table: String? = null,
chart: String? = null,
footer: String? = null,
reportType: String
): Report {
return Report(title, textContent, table, chart, footer)
}
}
fun main() {
val generator = ReportGenerator()
val financialReport = generator.generateReport(
title = "Q3 Financial Report",
textContent = "Revenue grew strongly...",
table = "Table 1: Business Results",
footer = "© 2024"
)
val performanceReport = generator.generateReport(
title = "October Performance Report",
textContent = "Work performance increased by 15%...",
chart = "Chart 1: Employee Performance",
footer = "HR Department"
)
}
Here, the generateReport function has to handle creating all the different types of reports, leading to many optional parameters and complex logic.
Code With Builder Pattern
data class Report(
val title: String,
val textContent: String,
val table: String? = null,
val chart: String? = null,
val footer: String? = null
)
interface ReportBuilder {
fun setTitle(title: String): ReportBuilder
fun setTextContent(textContent: String): ReportBuilder
fun setTable(table: String): ReportBuilder
fun setChart(chart: String): ReportBuilder
fun setFooter(footer: String): ReportBuilder
fun build(): Report
}
class ReportBuilderImpl : ReportBuilder {
private var title: String = ""
private var textContent: String = ""
private var table: String? = null
private var chart: String? = null
private var footer: String? = null
override fun setTitle(title: String): ReportBuilder {
this.title = title
return this
}
override fun setTextContent(textContent: String): ReportBuilder {
this.textContent = textContent
return this
}
override fun setTable(table: String): ReportBuilder {
this.table = table
return this
}
override fun setChart(chart: String): ReportBuilder {
this.chart = chart
return this
}
override fun setFooter(footer: String): ReportBuilder {
this.footer = footer
return this
}
override fun build(): Report {
return Report(title, textContent, table, chart, footer)
}
}
class ReportDirector {
fun constructFinancialReport(builder: ReportBuilder) : Report {
return builder.build()
}
fun constructPerformanceReport(builder: ReportBuilder) : Report {
return builder.build()
}
}
fun main() {
val director = ReportDirector()
val financialReport = director.constructFinancialReport(ReportBuilderImpl()
.setTitle("Q3 Financial Report")
.setTextContent("Revenue grew strongly...")
.setTable("Table 1: Business Results")
.setFooter("© 2024"))
val performanceReport = director.constructPerformanceReport(ReportBuilderImpl()
.setTitle("October Performance Report")
.setTextContent("Work performance increased by 15%...")
.setChart("Chart 1: Employee Performance")
.setFooter("HR Department"))
println(financialReport)
println(performanceReport)
}
Here, we separate the report creation into specific builders FinancialReport, PerformanceReport and a director ReportDirector to coordinate the building process.
Lessons From the Builder Pattern
When designing a feature, pay attention to complex objects and how they are created. If the creation process involves many steps or can create many variations of the object, consider using the Builder Pattern.
Builder Pattern helps us:
- Increase the flexibility and extensibility of the constructors.
- The highest purpose is still: To make the source code easier to read and maintain.
@dantech wishes you success!