Exploring  Builder Design Pattern in iOS

Exploring Builder Design Pattern in iOS

ยท

4 min read

๐Ÿ“ Introduction

The Builder Design Pattern is a creational design pattern that allows you to construct complex objects step by step.

The idea behind the builder pattern is that the process of setting up an object is handled by a dedicated Builder type, rather than by the object itself.

This way it separates the construction of an object from its representation, giving you more control and flexibility during the building process.

The pattern is especially useful when working with objects that have many configurable parameters.

๐ŸŽจ Diagram

๐Ÿ”ท Key Components

  1. Product: is the complex object that will be created.

  2. Builder: is the object that takes inputs and creates the Product.

  3. Director: provides the builder with inputs and a step by step guide and requests the builder to create the product once everything has been provided.

๐Ÿ‘จ๐Ÿผโ€๐Ÿ’ป Implementation

Let's consider an example where we have a Car object that needs to be constructed and in this case this will be our Product.

The Car object has various properties such as brand, model, color, engineType, and numberOfDoors.

We can showcase this through the Car object:

struct Car {
  let brand: String
  let model: String
  let color: String
  let engineType: String
  let numberOfDoors: Int
}

Then we create the Builder that will be responsible for creating the Product following the provided instructions.

class CarBuilder {

  enum BuilderError: Error {
    case missingPart
  }

  private var brand: String?
  private var model: String?
  private var color: String?
  private var engineType: String?
  private var numberOfDoors: Int?

  func setBrand(_ brand: String) -> CarBuilder {
    self.brand = brand
    return self
  }

  func setModel(_ model: String) -> CarBuilder {
    self.model = model
    return self
  }

  func setColor(_ color: String) -> CarBuilder {
    self.color = color
    return self
  }

  func setEngineType(_ engineType: String) -> CarBuilder {
    self.engineType = engineType
    return self
  }

  func setNumberOfDoors(_ numberOfDoors: Int) -> CarBuilder {
    self.numberOfDoors = numberOfDoors
    return self
  }

  func build() throws -> Car {

    guard let brand = brand,
          let model = model,
          let color = color,
          let engineType = engineType,
          let numberOfDoors = numberOfDoors else {

      throw BuilderError.missingPart

    }

    return Car(brand: brand, model: model,
               color: color, engineType: engineType,
               numberOfDoors: numberOfDoors)
  }
}

In the example above, the CarBuilder class is responsible for constructing the Car object.

It provides methods to set each property of the Car object and returns a reference to the builder itself, enabling method chaining.

The build() method throws an error if any required data is missing, this will ensure that the Builder is provided with the necessary data to create the Product.

After we have the first two components than we can create the Director that will be responsible for using the Builder to create the Product that we need.

class CarFactory {

  func createSportsCar() throws -> Car {

    let builder = CarBuilder()

    return try builder
      .setBrand("BMW")
      .setModel("M4")
      .setColor("Black")
      .setEngineType("V10")
      .setNumberOfDoors(2)
      .build()

  }

  func createFamilyCar() throws -> Car {

    let builder = CarBuilder()

    return try builder
          .setBrand("Volvo")
          .setModel("XC60")
          .setColor("White")
//          .setEngineType("")
          .setNumberOfDoors(4)
          .build()
  }

}

In this example we create a CarFactory that will create different types of Car products using the CarBuilder.

So we can create an instance of the CarFactory and see that in action:

let carFactory = CarFactory()

if let spotCar = try? carFactory.createSportsCar() {
  print("The Sport-Car is created successfully!")
}
// The Sport-Car is created successfully!

if let familyCar = try? carFactory.createFamilyCar() {
  print("The Family-Car is created successfully!")
} else {
  print("Sorry, there was a missing part in the process of creating this car. Please check again.")
}
// Sorry, there was a missing part in the process of creating this car. Please check again.

โœ… Positive aspects

1. Encourages a clear separation between object construction and representation, promoting code maintainability.

2. Provides a flexible and expressive way to construct objects, allowing for different configurations.

3. Supports testability by enabling the creation and testing of various object variations.

โŒ Negative aspects

1. Introduces additional code for the builder, potentially increasing the overall complexity, especially for simpler objects.

2. Requires careful handling of optional properties and error management during object construction.

๐ŸŽ‰ Conclusions

The Builder Design Pattern, when implemented with attention to best practices, offers a powerful and flexible approach to constructing complex objects.

It separates the construction process from the object's representation, facilitating code maintenance and testability.

However, it should be applied judiciously, considering the complexity and requirements of the project.

Remember, the implementation of design patterns should align with the specific needs and constraints of your application, and the Builder pattern is particularly useful when dealing with complex object creation that requires multiple steps and variations.

If you want to be notified of the upcoming articles you can subscribe to the Newsletter and support The iOS Mentor blog using Buy Me a Coffee.

You also have the option to support this blog as a sponsor, contributing to the growth of our iOS Development community.

I am also available on LinkedIn and GitHub so let's connect!

Thanks for reading everyone and enjoy the rest of your day! ๐Ÿ™

Did you find this article valuable?

Support The iOS Mentor by becoming a sponsor. Any amount is appreciated!

ย