Published on

Learning Design Patterns: Builder Pattern - A Surefire Guide

Authors

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!