DEV Community

loading...
Cover image for Design Patterns: Factory Method

Design Patterns: Factory Method

Tamerlan Gudabayev
Learning is fun
Originally published at softwareadventuring.com Updated on ・5 min read

Today we will discuss about one of the most useful design patterns.

The Factory Method

It's a creational design pattern meaning that it helps us create objects efficiently.

Efficient object creation is a regular problem, that's why Factory Method is one of the most widely used design patterns.

By the end of this article, you will:

  • Understand the core concepts of Factory Method
  • Recognize opportunities to use Factory Method
  • Learn to create a suitable implementation of Factory Method to suit your application

Definition

“One advantage of static factory methods is that, unlike constructors, they have names.” ― Joshua Bloch, Effective Java

Factory Method is a creational design pattern that provides an interface to create objects to a parent-class but allows child classes to modify the type of object that will be created.

Problem

Alt Text

Let's imagine that you have a shipping company. When it first started you only supported trucks.

In terms of code, it's gonna look like this:

  • Shipping class — manages the shipping mechanism.
  • Truck class — class that represents a truck.
class Truck:
    def __init__(self, truck_id):
        self.truck_id = truck_id
        pass

    def ship(self):
        # Logic for shipping
        pass;

class Shipping:
    def __init__(self, info):
        self.truck = Truck(info["truck_id"])
        self.status = "Packaged"
        self.package = info["package"]
        pass

    def send(self):
        self.status = "Pending"
        self.truck.ship()

# Initializing a shipping mission
data = {
    "truck_id": 1,
    "package": "some_package"
}

mission = Shipping(info=data)
mission.send()
Enter fullscreen mode Exit fullscreen mode

As you can see, the shipping class directly create a truck object, making them very coupled to one another. The shipping class would not work without a truck object.

Now, imagine that your shipping company became popular and clients are wanting to ship their products overseas. So naturally, you introduce a new type of transportation, the plane.

But, how will we be able to add that to our code?

A naive solution will be to check if the shipping class uses a truck or a plane.

class Shipping:
    def __init__(self, info, type):
        self.type = type
        if self.type == "truck":
            self.truck = Truck(info["truck_id"])
            self.status = "Packaged"
            self.package = info["package"]
        else:
            self.plane = Plane(info["plane_id"])
            self.status = "Packaged"
            self.package = info["package"]

    def send(self):
        self.status = "Pending"

        if self.type == "truck":
            self.truck.ship()
        else:
            self.plane.ship()
Enter fullscreen mode Exit fullscreen mode

This can get messy very quickly:

  • The more the types of transportation, the more conditional code we have to write.
  • Due to more code, we have added complexity.
  • This breaks the single responsibility principle, the shipping class handles shipping of many different types of transportation.
  • This breaks the open-closed principle because every time we have to add a new type of transportation we have to edit existing code.
  • Changing in any of the code in the truck or plane class will require changes to the shipping class.

Our problem here is that we don't exactly know what types of objects we will have, sometimes it's a truck, plane, or even a new type of transportation.

This is what the factory method pattern fixes.

Solution

The factory method pattern suggests that it's better to instantiate objects outside the constructor to a separate method and let subclasses control which objects get created.

So what we can do to our shipping class is:

  • Remove the object instantiation from the constructor
  • Make the Shipping class an abstract class
  • Add an abstract method setTransport that will set the appropriate transport
  • Create two new classes TruckShipping and PlaneShipping that inherit from Shipping class.
  • Implement the setTransport function in the new classes.
import abc # Python Library that allows us to create abstract classes

class Shipping(metaclass=abc.ABCMeta):

    def __init__(self, package):
        self.status = "Packaged"
        self.package = package
                self.transport = None

    @abc.abstractmethod
    def setTransport(self, data):
        pass

class TruckShipping(Shipping):

    def setTransport(self, id):
        self.transport = Truck(id=id)

class PlaneShipping(Shipping):

    def setTransport(self, id):
        self.transport = Plane(id=id)
Enter fullscreen mode Exit fullscreen mode

But what about the send method we had in the first version of the Shipping class. The first version of the send method directly accessed the truck object and ran the method ship inside it.

Factory method pattern encourages us to create an interface or abstract class to our object types, to make them consistent and interchangeable.

Let's do another refactor:

  • Create an abstract class called Transport with an abstract method ship.
  • Create two new classes that inherit from Transport called Truck and Plane.
class Transport(metaclass=abc.ABCMeta):
    def __init__(self, id):
        self.id = id

    @abc.abstractmethod
    def ship(self):
        pass

class Truck(Transport):

    def ship(self):
        # Shipping instructions for trucks
        pass

class Plane(Transport):

    def ship(self):
        # Shipping instructions for planes
        pass
Enter fullscreen mode Exit fullscreen mode

Finally in our Shipping abstract class, we can create a method called send that calls the ship method of a transport.

def send(self):
        self.transport.ship()
Enter fullscreen mode Exit fullscreen mode

When to use the pattern?

Design patterns should always be used within reason, as software engineers we need to be able to detect these opportunities.

For factory method pattern there are a couple of reasons why you might want to use it:

  • When you don't know the exact types of objects your class will be interacting with. In our example, we could've had one transport system, or a hundred. You just don't know.
  • Use factory method pattern when you want your users to extend the functionality of your library or framework.
  • The factory method can also be used to save memory usage. Instead of creating a new object every time, you can save it in some sort of caching system.

Pros & Cons

Alt Text

Pros

  • Removes tight coupling between classes.
  • Increases readability.
  • Allows us to construct subclasses with different properties than the parent class without using the constructor.
  • Follows the single responsibility principle, making the code easier to support.
  • Follows the open-closed principle, if you want to add a new type of transport, you can simply create a new subclass, without modifying existing code.
  • Easier to test.

Cons

  • The code becomes more complicated because you introduced many new subclasses.

Conclusion

Factory method is one of the most widely used design pattern, and one that you just might use next time at work!

In this article, you learned:

  • What the factory method is, and it's components.
  • How to refactor code, into the factory method pattern.
  • Situations where factory method would be useful.
  • Pros and cons of factory method

Further Reading

If you want to learn more about the design patterns, I would recommend Diving into Design Patterns. It explains all 23 design patterns found in the GoF book, in a fun and engaging manner.

Another book that I recommend is Heads First Design Patterns: A Brain-Friendly Guide, which has fun and easy-to-read explanations.

Discussion (1)

Collapse
sxddhxrthx profile image
Siddhartha Chowdhury

I have already been using the factory method but it is with your article that I can you know about its nunaces. Also, just a suggestion for cleaner code, in your naive solution example, you may want to take package and status initialisation out of if-else block.