tsorn
tsorn

Reputation: 3625

Programming pattern for combining factory functions

Say you have a bunch of factory functions, each of which does two things:

  1. Modify or add arguments to the initialization of a class
  2. Does something with the class instance afterwards

E.g.

class Dog:
  def __init__(self, **very_many_kwargs):
    pass

def create_police_dog(department, **dog_kwargs):
  dog_kwargs['race'] = 'pitbull_terrier'
  dog = Dog(**dog_kwargs)
  police_academy = PoliceAcademy()
  police_academy.train(dog)
  return dog

def create_scary_dog(**dog_kwargs):
  dog_kwargs['teeth_size'] = 'MAX'
  dog_kwargs['eye_color'] = fetch_angry_eye_colors('https://dogs.com')
  dog = Dog(**dog_kwargs)
  dog.experience_unhappy_childhood()
  return dog

How to combine multiple of such functions in series?

Upvotes: 1

Views: 487

Answers (3)

chepner
chepner

Reputation: 530990

Each function should accept an existing instance of Dog and modify it, then return the modified instance. That way, you can simply compose the functions. (This is sort of a functional equivalent of the Builder pattern, though the example below is somewhat awkward and clunky.)

class Dog:
    def __init__(self, **very_many_kwargs):
        pass

def create_police_dog(dog, department):
    dog.race = 'pitbull_terrier'
    police_academy = PoliceAcademy()
    police_academy.train(dog)
    return dog

def create_scary_dog(dog):
    dog.teeth_size = 'MAX'
    dog.eye_color = fetch_angry_eye_colors('https://dogs.com')
    dog.experience_unhappy_childhood()
    return dog

scary_police_dog = create_scary_dog(
                     create_police_dog(
                       Dog(),
                       'vice'
                     )
                   )
    

Here is, I think, a more typical implementation of the Builder pattern.

class Dog:
    ...


class DogBuilder:
    def __init__(self):
        self.kwargs = {}
        self._train = False
        self._experience_unhappy_childhood = False

    def build(self):
        d = Dog(**self.kwargs)
        if self._train:
            PoliceAcademy().train(d)
        if self._experience_unhappy_childhood:
            d.experience_unhappy_childhood()
        return d

    def train(self):
        self._train = True

    def set_race(self, r):
        self.kwargs['race'] = r

    def experience_unhappy_childhood(self):
        self._experience_unhappy_childhood = True

    def make_police_dog(self, department):
        self.kwargs['department'] = department
        return self.set_race('pitbull_terrier').train()

    def make_scary_dog(self):
        return self.set_eye_color(fetch_angry_eye_colors('https://dogs.com')).
                    set_teeth_size('MAX').
                    experience_unhappy_childhood()


scary_police_dog = (DogBuilder()
                     .make_police_dog('vice')
                     .make_scary_dog()
                     .build()

Here's an approach using multiple inheritance (more specifically, cooperative multiple inheritance).

class Dog:
    def __init__(self, *, race, teeth_size='Min', eye_color=None, **kwargs):
        super().__init__(**kwargs)
        self.race = race
        self.eye_color = eye_color
        self.teeth_size = teeth_size


class PoliceDog(Dog):
    def __init__(self, *, department, **kwargs):
        kwargs['race'] = 'pitbull_terrier'
        super().__init__(**kwargs)
        PoliceAcademy().train(self)


class ScaryDog(Dog):
    def __init__(self, **kwargs):
        kwargs['teeth_size'] = 'MAX'
        kwargs['eye_color'] = fetch_angry_eye_colors('https://dogs.com')
        super().__init__(**kwargs)
        self.experience_unhappy_childhood()


class ScaryPoliceDog(PoliceDog, ScaryDog):
    pass


d = ScaryPoliceDog(department="vice")

Unless there is something specific about a scary police dog that doesn't apply to an ordinary police dog or scary dog, nothing special needs to be done in ScaryPoliceDog. It's all handled via delegation using super.

In particular:

  1. The MRO for ScaryPoliceDog is PoliceDog, ScaryDog, Dog, object. The __init__ method for each will be called in turn.
  2. super() in PoliceDog does not refer to Dog; it refers to ScaryDog, because that's the class following PoliceDog in the MRO.
  3. ScaryPoliceDog does not need to specify a race. Although that field is required by Dog, it will be supplied by PoliceDog, as PoliceDog.__init__ will be called first.

Upvotes: 0

Woodford
Woodford

Reputation: 4449

Decorators almost work but since you want your modifications to occur both before and after instantiation, they won't chain properly. Instead define a custom system of generic modifiers that can be chained together at creation time:

from abc import ABC, abstractmethod                                                                  
                                                                                                     
class DogModifier(ABC):                                                                              
    @abstractmethod                                                                                  
    def mod_kwargs(self, **kwargs):                                                                  
        pass                                                                                         
                                                                                                     
    @abstractmethod                                                                                  
    def post_init(self, dog):                                                                        
        pass                                                                                         
                                                                                                     
class PoliceDog(DogModifier):                                                                        
    def __init__(self, department):                                                                  
        self._dept = department                                                                      
                                                                                                     
    def mod_kwargs(self, **kwargs):                                                                 
        kwargs['race'] = 'pitbull_terrier'                                                          
                                                                                                    
    def post_init(self, dog):                                                                       
        PoliceAcademy(self._dept).train(dog)                                                        
                                                                                                        
class ScaryDog(DogModifier):                                                                        
    def mod_kwargs(self, **kwargs):                                                                 
        kwargs['teeth_size'] = 'MAX'                                                                
        kwargs['eye_color'] = fetch_angry_eye_color('https://dogs.com')                             
                                                                                                    
    def post_init(self, dog):                                                                       
        dog.experience_unhappy_childhood()                                                          
                                                                                                    
def create_dog(*modifiers, **dog_kwargs):                                                               
    for m in modifiers:                                                                             
        m.mod_kwargs(**dog_kwargs)                                                                      
                                                                                                    
    dog = Dog(**dog_kwargs)                                                                         
                                                                                                    
    for m in modifiers:                                                                             
        m.post_init(dog)                                                                            
                                                                                                    
    return dog                                                                                      
                                                                                                    
# ...                                                                                               
                                                                                                    
police_dog = create_dog(PoliceDog('bomb squad'), kw1='a', kw2='b')                                  
scary_dog = create_dog(ScaryDog(), kw1='x', kw2='y')                                                
scary_police_dog = create_dog(PoliceDog('bomb squad'), ScaryDog(), kw1='z')

*code shown as example only - bugfixes left as an exercise for the reader

Upvotes: 1

Yevhen Kuzmovych
Yevhen Kuzmovych

Reputation: 12130

I don't think my solution is the best and would be very interested in any other solutions. But here is my idea:

class DogFactory:
    def __init__(self):
        self.dog_kwargs = {}
        self.dog_functions = []

    def scary(self):
        self.dog_kwargs['teeth_size'] = 'MAX'
        self.dog_kwargs['eye_color'] = fetch_angry_eye_colors('https://dogs.com')
        self.dog_functions.append(Dog.experience_unhappy_childhood)
        return self

    def police(self):
        self.dog_kwargs['race'] = 'pitbull_terrier'
        police_academy = PoliceAcademy()
        self.dog_functions.append(police_academy.train)
        return self


    def create(self):
        dog = Dog(**self.dog_kwargs)

        for f in self.dog_functions:
            f(dog)

        return dog


dog = DogFactory() \
        .scary() \
        .police() \
        .create()

Upvotes: 0

Related Questions