Plugin design pattern

A pattern for extensible code. Interfaces are used to decouple and importlib to dynamically load modules.

Example description

The simple example runs as a farm that loads “animal” objects with method do. A JSON file contains a plug-in list, used for loading modules, and a list of objects to be created with name_of_type attribute relating to a class in the loaded plug-in.

This allows the code to be extended for new “animals” without any change. The protocol uncouples the original implementations for “animal” from newer ones, as long as the protocol is kept the same.


Files in this example:



Running test

The list of modules only includes traditionalAnimals, which contains cow, chicken, and sheep. They only differ in default values and printed message.

The list of objects to create is: >1. A cow without passing parameters; >2. A duck with parameters; >3. A sheep with parameters; >4. A inexisting class with parameter {"name":"bleh"}; >5. A inexisting class without parameters.


import json
import sys
utilPath = './plugin_utils'
sys.path.append(utilPath)

import plugingIn
import factory


with open(utilPath+'/config.json') as f:
    data = json.load(f)

    plugingIn.load_register(data['modules'])

    farm = [factory.makeFromJson(**animal_stats) for animal_stats in data['animals']]

    for animal in farm:
        animal.do()
Brumhilda the cow goes moo with 10 liters of milk per day
Gertrude the cow goes moo with 8 liters of milk per day
Ms.Clucks the chicken goes cluck with 50 eggs per day
Cheap the sheep goes beh with 999 grams of whool per day
I am bleh the platypus!! Kneel befor me!
I am Plato the platypus!! Kneel befor me!

Files

factory.py

Contains some “factory” behaviour functions. > - Maintains a list of classes; > - Inserts new items on the list; > - Instantiates from list with arguments.

Also contains a default class platypus.


Code("./plugin_utils/factory.py")
from dataclasses import dataclass
from farmAnimal import farmAnimal



farmAnimal_type_list: 'dict[str, callable[..., farmAnimal]]' = {}



def makeFromJson(**args: 'dict[str,any]'):
    args_ = args.copy()
    name_of_type = args_.pop('name_of_type','platypus')
    return makeAnimal(name_of_type=name_of_type, args=args_)



def makeAnimal(args: 'dict[str,any]', name_of_type: str = "platypus"):
    '''instantiate animal from registered class with arguments'''

    builder = farmAnimal_type_list.get(name_of_type, platypus)
    return builder(**args)



def registerAnimal(type_name: str, builder: 'callable[...,farmAnimal]'):
    '''register class by name'''

    farmAnimal_type_list[type_name] = builder



@dataclass
class platypus():
    '''Default random animal that follows the farmAnimal Protocol'''

    name: str = 'Plato'

    def do(self) -> None:
        print(f"I am {self.name} the platypus!! Kneel befor me!")

back to top

plugingIn.py

Implements plug-in loading. > - Defines an interface for modules with the register method; > - Loads module from name: str; > - Calls the register method on the module.


Code("./plugin_utils/plugingIn.py")
import importlib
import factory
from typing import Protocol


class moduleInterface(Protocol):
    def register() -> None:
        '''Register classes in module'''

def load(name: str) -> moduleInterface:
    '''loads the plugins'''

    return importlib.import_module(name)

def load_register(plugin_names: 'list[str]') -> None:
    '''for each plug-in name, load and register in factory'''

    for name in plugin_names:
        plugin = load(name)
        plugin.register()

back to top

farmAnimal.py

Defines a protocol for classes to be used in the program. > - Defines class protocol with do method.


Code("./plugin_utils/farmAnimal.py")
from typing import Protocol

class farmAnimal(Protocol):
    '''A farm animal'''

    def do(self) -> None:
        '''A farm animal does ???'''

back to top

traditionalAnimals.py

Implements a plug-in. > - Contains classes that follow the farmAnimal protocol; > - Contains register method.


Code("./plugin_utils/traditionalAnimals.py")
import factory
from dataclasses import dataclass


@dataclass
class chicken():

    name: str = "Suzy"
    eggs_per_day: str = '10'

    def do(self) -> None:
        print(f"{self.name} the chicken goes cluck with {self.eggs_per_day} eggs per day")



@dataclass
class cow():

    name: str = "Brumhilda"
    milk_per_day: str = '10'

    def do(self) -> None:
        print(f"{self.name} the cow goes moo with {self.milk_per_day} liters of milk per day")



@dataclass
class sheep():

    name: str = "Poly"
    whool_per_day: str = '100'

    def do(self) -> None:
        print(f"{self.name} the sheep goes beh with {self.whool_per_day} grams of whool per day")



def register() -> None:
    factory.registerAnimal('cow', cow)
    factory.registerAnimal('chicken', chicken)
    factory.registerAnimal('sheep', sheep)

back to top

config.json

Data and modules to be loaded. > - Contains list of modules do be dynamically loaded; > - Contains list of objects to be created.


Code("./plugin_utils/config.json")
{
    "modules": ["traditionalAnimals"],

    "animals": [
        {
            "name_of_type":"cow"
        },
        {
            "name_of_type":"cow",
            "name":"Gertrude",
            "milk_per_day":"8"
        },
        {
            "name_of_type":"chicken",
            "name":"Ms.Clucks",
            "eggs_per_day":"50"
        },
        {
            "name_of_type":"sheep",
            "name":"Cheap",
            "whool_per_day":"999"
        },
        {
            "name_of_type":"bleh",
            "name":"bleh"
        },
        {
            "name_of_type":"blah"
        }
    ]
}

back to top

Conclusion:

The interface decoupling is the main feature behind most of the design patterns. Some can be summarized as:

  • Adapter -> Interface intermidiary to connect UI to object with different interface

  • Composite -> Use a dsitributor object with same interface as real worker to divide tasks

  • Bridge -> An interface between two connected parts of a system

  • Flyweight -> Delegation + dependency injection or dependency inversion through interfaces

  • Proxy -> A gateway with same interface to control access.

  • Facade -> A gateway with simplified interface.

  • Decorator -> A gateway with improved interface.

  • Chain of responsability -> With same interface to handle tasks, objects pass requests in chain.

  • Command -> Disconnect client-server with a command object (one-sided).

  • Mediator -> Disconnect objects with a mediator object.

  • Observer -> Disconnect client-server with a command object (one-to-many).

  • Strategy -> One interface, multiple implementations, an adapter in the middle.

  • Visitor -> A mess of entanglements of classes. Not really sure whats the point.

  • State -> Dependency inversion. The states objects are responsible for the variable behaviour.

It is much easier to maintain documentation with nbdev.