Implementing the Factory Pattern via Dynamic Registry and Python Decorators

Geoffrey Koh
4 min readJan 27, 2020

--

This is the first of a series of articles I will set out to write in 2020 highlighting different design patterns and principles being applied in my own projects.

The Factory method is one of the Gang of Four design patterns that is commonly used for creating objects. In this pattern, the method of the Factory class that is involved in the creation of objects typically has switch statements (or if/else blocks in the case of Python) in which, depending on the value of a string (denoting the type of object to be created) being passed in, will create the required object instance. Since this is one of the most basic design patterns, this article does not attempt to re-iterate the advantages of using Factories. Rather, we aim to demonstrate a particular way to implement the Factory method in a more elegant way, and at the same time, espouse the Open-Closed Principle (one of the SOLID principles in Software Design).

What You Will Achieve

By the end of this article, you should be able to create a Factory class in which you can register new types of classes to be created without amending the Factory class itself.

To illustrate the various examples, we will not use “toy” code samples that do not resemble anything in real world. Instead, we will use simplified snippets from existing project codes so that you can have a better understanding of how these concepts can be used in actual systems.

Command Line Executor

One of the many uses of a command line executor in a production system is to be able to run a console command in different environments. According to the Dependency Inversion Principle, high level modules (the business logic) should not depend on low level modules (codes representing the actual environment to run the command). Both should depend on abstractions. To build this abstraction, we have the concept of Executors.

Base class for a command line executor

Suppose we want to run the following python script in a local environment, we simply create a LocalExecutor instance and run it.

A concrete class definition for a command executor that runs the command on the local machine

One can, of course, create an instance of the LocalExecutor simply by creating a class instance as if we would do so in any Python code.

Creating a LocalExecutor directly

Such an approach is simple and straightforward. Now suppose at some point of time, we’d want to add another type of executor, i.e. one that runs on a remote host for example. Or if we’d like to give the option to allow a user to select which type of environment he’d want to run his command at runtime. To do so, a typical design pattern is the Factory pattern, in which one has an ExecutorFactory class object that will determine the appropriate concrete executor class to create.

Executor Factory

As mentioned at the beginning of this article, one easy way to implement the factory pattern is to have a series of if-else blocks that selects and creates the concrete executor instances depending on a string or value that is being passed in. However, we strive to allow users to add in other types of executors without amending the body of the ExecutorFactory. To do so, we keep an internal registry within the ExecutorFactory that maps name of the executor to the class itself.

Basic structure of an ExecutorFactory class

Registering New Executors

So how do we register new executors? To do so, we turn to Python decorators, which is a nifty Python feature that allows one to modify the behavior of a function or a class.

In this example, we don’t modify the executor classes per se. Rather, we decorate the class itself, such that, during class declaration, it is automatically registered into the ExecutorFactory’s internal registry, and we do it like so.

Wrapper function within ExecutorFactory to aid with the registration of the class

Thereafter, we simply modify the code of the LocalExecutor by adding a decorator just before the class definition itself.

Decorating the LocalExecutor and registering it with the Factory

Instantiating and using the LocalExecutor is then as simple as creating and running it.

Putting it All Together

Here we show the entire source code. In addition to the LocalExecutor, we also create a RemoteExecutor class and demonstrate how it can be added to the registry.

To allow for remote execution for this example, we create an Amazon AWS EC2 Linux instance and generate a key pair that allows for remote connection. For more details, follow the instructions here. (If you have a computer cluster that allows for passwordless ssh, you may replace the hostname with the appropriate hostname or IP address without the need for any key pair)

Full code for the examples used in this article (you may have to replace the remote url with the actual hostname that you set up)

Why This Approach is Useful

In distributed software systems, the underlying computational infrastructure is not something that is constant. As a firm adapts to new technologies, there will be a need to run existing operations on a multitude of platforms. In this article we only showed localhost and remote access via ssh. It is not unimaginable that one day, one might need to create other types of executors that run on, say, Google Cloud Platform, or on a cluster managed by Apache Mesos or Kubernetes. In order to minimize the amount of code refactoring, abstracting the executor and encapsulating its creation details through a Factory seems the natural thing to do. To go one step further, by using dynamic registration through Python decorators, we allow one to create new executors into this framework by simply adding a new class and decorating it using @ExecutorFactory.register() to register it, thereby adhering to the principle of Open to extension, Closed for modification.

Final Words

There are several ways of implementing the different Design Patterns and their variations, and no one can claim monopoly over the “right” way to do so. Here, we present an approach that, to us, seemed eloquent and at the same time, sufficiently extensible enough to cater for future requirements that is not yet known.

Acknowledgement

I would like to acknowledge https://www.linkedin.com/in/nguyenthinguyet1711/ for authoring the original source code, using which I’ve adapted and simplified bits and pieces of it for this article.

--

--