mercredi 12 octobre 2022

Builder Pattern python: 1 builder or 2 builders inheriting from base?

I've a need to use a builder for constructing an entity (let's call it MessageWrapper) which needs different processing before initialization, according the type of entity that was passed to the builder.

Currently I'm using one builder. Users of the Builder will be able to pass it (currently) two types of entities which conform to an "interface".

def set_entity(self, entity: IMyInterface):
   self._entity = entity
   return self

# IMyInterface is ABC with abstract process() method
def _process_entity_field(interface_instance: IMyInterface):
    ...
    return interface_instance.process()

def build(self):
    ...
    final_message_wrapper_field = self._process_entity_field()
    ...

Here's where my build() method is getting a bit messy though: MessageWrapper also requires another field processing_type: str, which will be either "A" or "B" (but in the future, may be "C").

I'm already taking care of the processing stage by using IMyInterface.process(), but I find myself doing this in the build() method:

if isinstance(self._entity, AThatConformsToMyInterface):
    processing_type = "A"
elif isinstance(self._entity, BThatConformsToMyInterface):
    processing_type = "B"
else:
    raise RuntimeError(
        f"Entity of type {self._entity.__class__.__name__} is not supported"
    )

Which kinda smells?

The alternative is to eschew the interface, and break out the builder to two builders that inherit from a common base:

a) MessageWrapperFromAObjBuilder - which will simply accept AThatConformsToMyInterface and simply set processing_type to "A".

b) MessageWrapperFromBObjBuilder - which will simply accept BThatConformsToMyInterface and simply set processing_type to "B".

which removes the need for the isinstance()... check, because we know which one of the XThatConformsToMyInterface we got.

The disadvantages of this alternative as I see them:

  1. In my mind the builder pattern is there exactly for cases where you need some kind of conditional processing in the build stage. So using two builders seems kinda meh.

  2. If type CThatConformsToMyInterface is introduced later, I'll probably have to have a designated builder for that.

So, one builder, or two builders? (it's worth emphasizing: the builder is building the same type of object at all times, it's how they get built/processed and their processing_type: str value that depends on XThatConformsToMyInterface.

To make things worse/more-challenging:

  • BThatConformsToMyInterface is an external library class.
  • I don't want to introduce another method to the IMyInterface "interface" which hints at what processing_type: str should be.

Aucun commentaire:

Enregistrer un commentaire