This page looks best with JavaScript enabled

Design Patterns - Factory

 ·  ☕ 3 min read  ·  🤖 Kaung

Context

Factory pattern is a creational design pattern. It allows users to instantiate specifics classes via Factory class. It also provides flexibility and extensibility. Developers can add new classes (features, products, etc) to the class, without distrupting existing classes.

From my experience, it has been particularly useful in working with analytical libraries. Below is a practical example.

Example

Suppose we have implemented a root finding algorithm - say Newton-Raphson method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class NewtonRaphson:
    '''
    Using scipy lib here - not going to implement the algorithm because this is an example
    Ref: https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.optimize.newton.html#scipy.optimize.newton
    '''
    def __init__(self, tolerance=1.48e-8, maxiter=50):
        import scipy.optimize as optimize # lazy import
        self.tolerance = tolerance
        self.maxiter = maxiter
        self.solver = optimize.newton
    
    def calculate(self, func, x0):
        return self.solver(func, x0, tol=self.tolerance, maxiter=self.maxiter)

We also got another root finding algorithm developed by another team - let’s say bisection method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class BisectRootFinder:

    # Ref: https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.optimize.bisect.html#scipy.optimize.bisect
    def __init__(self, xtolerance=1e-12, rtolerance=4.44e-16, maxiter=100, disp=True):
        from scipy.optimize import bisect # lazy import
        self.xtolerance = xtolerance
        self.rtolerance = rtolerance
        self.maxiter = maxiter
        self.disp = disp
        self.solver = bisect
    
    def calculate(self, func, a, b):
        return self.solver(func, a, b, xtol=self.xtolerance, rtol=self.rtolerance, maxiter=self.maxiter, disp=self.disp)

If both algorithms are developed by the same team, we can just write an interface - something like GenericRootFinder and expose them to users.

However, we have 2 different methods from 2 different teams. In order to expose these in an organized way, we can throw in Factory pattern. This would allow both teams to focus on their methods, ship the analytical library in an unified manner, do maintenance with ease and introduce minimal breaks among users and their applications in the future.

So the Factory would look like this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class RootFinderFactory:

    def __init__(self, instname='DefaultFactory'):
        from functools import partial # lazy import
        self.instname = instname # we may want more than one factories
        self.mapper = {
            'newton': partial(NewtonRaphson),
            'bisect': partial(BisectRootFinder)
        }

    def create(self, method='newton', **kwargs):
        return self.mapper.get(method)(**kwargs)

Summary

Above Factory class can be extended in many ways without affecting existing implementations. It can bring in new algorithms of root finding. From client side, it could be as easy as changing a config to generate a different root-finding method instance.

In this article, I have shared what factory pattern is and its practical use case in organizing an analytical library.

If you have comments on this, you can reach me at hello@kaung.dev.

You can see the full implementation and tests at my github.

References

Share on
Support Me

kaung
WRITTEN BY
Kaung
Software Engineer | Physicist