The Multibuilder Pattern


In this post I would like to present a couple of solutions for builder pattern in python, and finally I show how I implemented a builder that I call multibuilder pattern.

Builder pattern comes in handy when we need to set a lot of fields of an object and we do not want to use many methods with multiple parameters.

All implementations are found in this repo: https://github.com/torokmark/builders_in_python

The Simple One

Task: Let us implement a Person with the following fields like name, age, phone.
Approach: Make a PersonBuilder with methods according to the fields in Person, plus a build which returns an object of Person.

class Person:
    def __init__(self, name, age, phone):
        self._name = name
        self._age = age
        self._phone = phone

    def name(self):
        return self._name

    def age(self):
        return self._age
    
    def phone(self):
        return self._phone

    def __str__(self):
        return '[name={}; age={}; phone={}]'.format(self._name, self._age, self._phone)
class PersonBuilder:
    def __init__(self):
        self._name = 'John Doe'
        self._age = 99
        self._phone = '000'

    def name(self, name):
        self._name = name
        return self

    def age(self, age):
        self._age = age
        return self

    def phone(self, phone):
        self._phone = phone
        return self

    def build(self):
        return Person(self._name, self._age, self._phone)
if __name__ == '__main__':
    print(PersonBuilder()
        .name('jancsi')
        .age(12)
        .phone('11223344')
        .age(18)
        .build())            
    # => [name=jancsi; age=18; phone=11223344]
    print(PersonBuilder()
        .age(12)
        .phone('11223344')
        .age(18)
        .build())            
    # => [name=John Doe; age=18; phone=11223344]
    print(PersonBuilder()
        .build())            
    # => [name=John Doe; age=99; phone=000]

The One with Named Parameters

Though builder is very flexible, it is inevitable to have some default values for those fields that are not set during the building.

Task: How to do this without builder?
Approach: Let us use default values in constructor parameter list.

class Person:
    def __init__(self, name='John Doe', age=99, phone='000'):
        self._name = name
        self._age = age
        self._phone = phone

    def name(self):
        return self._name

    def age(self):
        return self._age
    
    def phone(self):
        return self._phone

    def __str__(self):
        return '[name={}; age={}; phone={}]'.format(
          self._name, 
          self._age, 
          self._phone)
if __name__ == '__main__':
    print(Person())                        
    # => [name=John Doe; age=99; phone=000]
    print(Person(phone='123456', age=45))  
    # => [name=John Doe; age=45; phone=123456]
    print(Person('jancsi', 54, '312333'))  
    # => [name=jancsi; age=54; phone=312333]

The Classic

Task: Let us implement a classic car builder.
Solution: Take the example from Gang of Four!

class Car:
    def __init__(self, wheel=4, seat=4, color='red'):
        self.wheel = wheel
        self.seat = seat
        self.color = color

    def __str__(self):
        return '[wheels: {}, seats: {}, color: {}]'.format(
          self.wheel, 
          self.seat, 
          self.color)
class Builder:
    def set_wheels(self, wheel): pass
    def set_seats(self, seat): pass
    def set_color(self, color): pass
class CarBuilder(Builder):

    def __init__(self):
        self.car = Car()

    def set_wheels(self, wheel):
        self.car.wheel = wheel

    def set_seats(self, seat):
        self.car.seat = seat

    def set_color(self, color):
        self.car.color = color

    def get_result(self):
        return self.car
class CarBuilderDirector:
    def build(self):
        builder = CarBuilder()
        builder.set_wheels(8)
        builder.set_seats(4)
        builder.set_color("Red")
        return builder.get_result()
if __name__ == '__main__':
    car = CarBuilderDirector().build()
    print(car)      
    # => [wheels: 8, seats: 4, color: Red]

The Builder with Builders

Task: Implement a Response object builder. It has a status code, a header and a body part. Create a builder that can build multiple part of an object.
Approach: Let us implement so called subbuilders and pass the created objects to an outer builder, which combines them together.

Using just HeaderBuilder#add(key, value) instead of using header specific setters.

class Builder:
    def build(self): pass
class HeaderBuilder(Builder):
    def __init__(self):
        self._header = {}

    def add(self, key, value):
        self._header[key] = value
        return self

    def build(self):
        return self._header
class BodyBuilder(Builder):
    def __init__(self):
        self._body = []

    def add(self, content):
        self._body.append(content)
        return self

    def build(self):
        return self._body
class ResponseBuilder(Builder):
    def __init__(self):
        self._header = None
        self._body = None
        self._status = None

    def header(self, header):
        self._header = header
        return self

    def body(self, body):
        self._body = body
        return self

    def status(self, status):
        self._status = status
        return self

    def build(self):
        ret = {}
        ret['statusCode'] = self._status
        ret['header'] = self._header
        ret['body'] = self._body
        return ret
if __name__ == '__main__':
    print(ResponseBuilder()
            .header(
                HeaderBuilder()
                    .add('Accept', '*')
                    .add('Age', 12)
                    .build()
            )
            .body(
                BodyBuilder()
                    .add('some message comes here')
                    .add('another message here...')
                    .build()
            )
            .status(200)
            .build()
    )                   
    # => {'statusCode': 200, 'header': {'Accept': '*', 'Age': 12}, 'body': ['some message comes here', 'another message here...']}

The Multibuilder

Task: Create a builder that can build multiple part of an object at one time whitout using so called subbuilders.
Approach: Let us chain everything up and memorize the already created objects.

class HeaderBuilder:
    def __init__(self):
        self._header = {}

    def add(self, key, value):
        self._header[key] = value
        return self

    def body(self):
        return self._body

    def build(self):
        return self.status(200)

    def status(self, code):
        ret = {}
        ret['status'] = code
        ret['header'] = self._header
        ret['body'] = self._body.__dict__.get('_body')
        return ret

    def set_body(self, body):
        self._body = body
class BodyBuilder:
    def __init__(self):
        self._body = [] 

    def add(self, value):
        self._body.append(value)
        return self

    def header(self):
        return self._header

    def build(self):
        return self.status(200)

    def status(self, code):
        ret = {}
        ret['status'] = code
        ret['header'] = self._header.__dict__.get('_header')
        ret['body'] = self._body
        return ret

    def set_header(self, header):
        self._header = header
class ResponseBuilder:

    def __init__(self):
        self.__header = HeaderBuilder()
        self.__body = BodyBuilder()
        self.__body.set_header(self.__header)
        self.__header.set_body(self.__body)

    def header(self):
        return self.__header

    def body(self):
        return self.__body
if __name__ == "__main__":
    print(ResponseBuilder()
            .header()
                .add('Access-Control-Allow-Origin', '*')
                .add('Accept', '*')
            .body()
                .add(json.dumps('hello', default=str))
            .status(204))

    print(ResponseBuilder()
            .header()
                .add('Age', '12')
                .add('Accept', '*')
            .body()
                .add('message comes here!')
            .header()
                .add('Access-Control-Allow-Origin', '*')
            .build()) # status is default to 200

All these approaches are found in https://github.com/torokmark/builders_in_python