The Factory Method Design Pattern in Python

Introduction

In this article, we'll be diving into the Factory Method Design Pattern, implemented in Python.

Design Patterns define tried and tested solutions to various recurring problems in software development. They do not represent actual code, but rather ways in which we can organize our code for the optimum results.

In a world of limited resources, Design Patterns help us achieve the most results with the least amount of used resources. It is also important to note that Design Patterns do not apply to all situations and it is crucial to assess the problem at hand in order to choose the best approach for that particular scenario.

Design Patterns are divided into a few broad categories, though mainly into Creational Patterns, Structural Patterns, and Behavioral Patterns.

The Factory Method pattern is a Creational Design Pattern.

The Factory Method Design Pattern

Definition

The Factory Method is used in object-oriented programming as a means to provide factory interfaces for creating objects. These interfaces define the generic structure, but don't initialize objects. The initialization is left to more specific subclasses.

The parent class/interface houses all the standard and generic behaviour that can be shared across subclasses of different types. The subclass is in turn responsible for the definition and instantiation of the object based on the superclass.

Motivation

The main motivation behind the Factory Method Design Pattern is to enhance loose coupling in code through the creation of an abstract class that will be used to create different types of objects that share some common attributes and functionality.

This results in increased flexibility and reuse of code because the shared functionality will not be rewritten having been inherited from the same class. This design pattern is also known as a Virtual Constructor.

The Factory Method design pattern is commonly used in libraries by allowing clients to choose what subclass or type of object to create through an abstract class.

A Factory Method will receive information about a required object, instantiate it and return the object of the specified type. This gives our application or library a single point of interaction with other programs or pieces of code, thereby encapsulating our object creation functionality.

Factory Method Implementation

Our program is going to be a library used for handling shape objects in terms of creation and other operations such as adding color and calculating the area of the shape.

Users should be able to use our library to create new objects. We can start by creating single individual shapes and availing them as is but that would mean that a lot of shared logic will have to be rewritten for each and every shape we have available.

The first step to solving this repetition would be to create a parent shape class that has methods such as calculate_area() and calculate_perimeter(), and properties such as dimensions.

The specific shape objects will then inherit from our base class. To create a shape, we will need to identify what kind of shape is required and create the subclass for it.

We will start by creating an abstract class to represent a generic shape:

import abc
class Shape(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def calculate_area(self):
        pass

    @abc.abstractmethod
    def calculate_perimeter(self):
        pass

This is the base class for all of our shapes. Let's go ahead and create several concrete, more specific shapes:

class Rectangle(Shape):
    def __init__(self, height, width):
        self.height = height
        self.width = width

    def calculate_area(self):
        return self.height * self.width 

    def calculate_perimeter(self):
        return 2 * (self.height + self.width) 

class Square(Shape):
    def __init__(self, width):
        self.width = width

    def calculate_area(self):
        return self.width ** 2

    def calculate_perimeter(self):
        return 4 * self.width

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def calculate_area(self):
        return 3.14 * self.radius * self.radius

    def calculate_perimeter(self):
        return 2 * 3.14 * self.radius

So far, we have created an abstract class and extended it to suit different shapes that will be available in our library. In order to create the different shape objects, clients will have to know the names and details of our shapes and separately perform the creation.

This is where the Factory Method comes into play.

The Factory Method design pattern will help us abstract the available shapes from the client, i.e. the client does not have to know all the shapes available, but rather only create what they need during runtime. It will also allow us to centralize and encapsulate the object creation.

Let us achieve this by creating a ShapeFactory that will be used to create the specific shape classes based on the client's input:

class ShapeFactory:
    def create_shape(self, name):
        if name == 'circle':
            radius = input("Enter the radius of the circle: ")
            return Circle(float(radius))

        elif name == 'rectangle':
            height = input("Enter the height of the rectangle: ")
            width = input("Enter the width of the rectangle: ")
            return Rectangle(int(height), int(width))

        elif name == 'square':
            width = input("Enter the width of the square: ")
            return Square(int(width))

This is our interface for creation. We don't call the constructors of concrete classes, we call the Factory and ask it to create a shape.

Our ShapeFactory works by receiving information about a shape such as a name and the required dimensions. Our factory method create_shape() will then be used to create and return ready objects of the desired shapes.

The client doesn't have to know anything about the object creation or specifics. Using the factory object, they can create objects with minimal knowledge of how they work:

def shapes_client():
    shape_factory = ShapeFactory()
    shape_name = input("Enter the name of the shape: ")

    shape = shape_factory.create_shape(shape_name)

    print(f"The type of object created: {type(shape)}")
    print(f"The area of the {shape_name} is: {shape.calculate_area()}")
    print(f"The perimeter of the {shape_name} is: {shape.calculate_perimeter()}")

Running this code will result in:

Enter the name of the shape: circle
Enter the radius of the circle: 7

The type of object created: <class '__main__.Circle'>
The area of the circle is: 153.86
The perimeter of the circle is: 43.96

Or, we could build another shape:

Enter the name of the shape: square
Enter the width of the square: 5

The type of object created: <class '__main__.Square'>
The area of the square is: 25
The perimeter of the square is: 20

What's worth noting is that besides the client not having to know much about the creation process - when we'd like to instantiate an object, we don't call the constructor of the class. We ask the factory to do this for us based on the info we pass to the create_shape() function.

Pros and Cons

Pros

One of the major advantages of using the Factory Method design pattern is that our code becomes loosely coupled in that the majority of the components of our code are unaware of other components of the same codebase.

This results in code that is easy to understand and test and add more functionality to specific components without affecting or breaking the entire program.

The Factory Method design pattern also helps uphold the Single Responsibility Principle where classes and objects that handle specific functionality resulting in better code.

Cons

Creation of more classes eventually leads to less readability. If combined with an Abstract Factory (factory of factories), the code will soon become verbose, though, maintainable.

Conclusion

In conclusion, the Factory Method Design Pattern allows us to create objects without specifying the exact class required to create the particular object. This allows us to decouple our code and enhances its reusability.

It is important to note that, just like any other design pattern, it is only suitable for specific situations and not every development scenario. An assessment of the situation at hand is crucial before deciding to implement the Factory Method Design Pattern to reap the benefits of the pattern.

Author image
About Robley Gori
Nairobi, Kenya Twitter Website