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.
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 jsonimport sysutilPath ='./plugin_utils'sys.path.append(utilPath)import plugingInimport factorywithopen(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")
fromdataclassesimportdataclassfromfarmAnimalimportfarmAnimalfarmAnimal_type_list:'dict[str, callable[..., farmAnimal]]'={}defmakeFromJson(**args:'dict[str,any]'):args_=args.copy()name_of_type=args_.pop('name_of_type','platypus')returnmakeAnimal(name_of_type=name_of_type,args=args_)defmakeAnimal(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)returnbuilder(**args)defregisterAnimal(type_name:str,builder:'callable[...,farmAnimal]'):'''register class by name'''farmAnimal_type_list[type_name]=builder@dataclassclassplatypus():'''Default random animal that follows the farmAnimal Protocol'''name:str='Plato'defdo(self)->None:print(f"I am {self.name} the platypus!! Kneel befor me!")
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")
importimportlibimportfactoryfromtypingimportProtocolclassmoduleInterface(Protocol):defregister()->None:'''Register classes in module'''defload(name:str)->moduleInterface:'''loads the plugins'''returnimportlib.import_module(name)defload_register(plugin_names:'list[str]')->None:'''for each plug-in name, load and register in factory'''fornameinplugin_names:plugin=load(name)plugin.register()
Implements a plug-in. > - Contains classes that follow the farmAnimal protocol; > - Contains register method.
Code("./plugin_utils/traditionalAnimals.py")
importfactoryfromdataclassesimportdataclass@dataclassclasschicken():name:str="Suzy"eggs_per_day:str='10'defdo(self)->None:print(f"{self.name} the chicken goes cluck with {self.eggs_per_day} eggs per day")@dataclassclasscow():name:str="Brumhilda"milk_per_day:str='10'defdo(self)->None:print(f"{self.name} the cow goes moo with {self.milk_per_day} liters of milk per day")@dataclassclasssheep():name:str="Poly"whool_per_day:str='100'defdo(self)->None:print(f"{self.name} the sheep goes beh with {self.whool_per_day} grams of whool per day")defregister()->None:factory.registerAnimal('cow',cow)factory.registerAnimal('chicken',chicken)factory.registerAnimal('sheep',sheep)