Masonite Lessons - Using The Factory Pattern

#python #framework #design patterns
Written By: Joe Mancuso

Preface

Masonite has a lot of very front facing and explicit design patterns. It has always been my goal that developers using Masonite will become better developers by being exposed to these patterns either by use or by reading about them in the documentation and understanding through real world examples.

I will be creating these articles that talk about various design patterns and how I use them in my applications on a daily basis.

Introduction

Design Patterns are pretty simple. We as developers encounter problems with our code. We as developers have 5 goals of software development is to deliver a product that is

  • on time
  • within budget
  • fault free (no bugs)
  • satisfies needs now
  • satisfies need in the future

We as developers need to be able to accomplish these tasks in a way that can satisfy needs now and in the future. These are problems developers face all the time. Throughout the years, developers have encountered the same issues over and over again and the same way a carpenter has “tricks of the trade,“ so do developers. These tricks-of-the-trade are called Software Design Patterns. They are simply solutions to problems we encounter all the time in our development life.

Once you start to get good as a developer, you will be able to map issues to certain design patterns and be able to layout a software architecture to fit the needs of the problem you are solving.

The Pattern

The factory pattern is used to instantiate an object at runtime. That’s it. It’s just a class that can return different types of objects dependent on whatever value is passed to it. For example you may pass in the value of chocolate into the IceCreamFactory and get back a ChocolateIceCream class.

This may need to be done at runtime which is where this pattern comes into play. The value of chocolate may be stored in a database column that is fetched for a certain user for example.

Code Example

class IceCreamFactory:

    flavors = {
        'chocolate': ChocolateIceCream,
        'vanilla': VanillaIceCream,
        'strawberry': StrawberryIceCream,
    }

    def make(self, flavor):
        return self.flavors[flavor]()

That's it. We just created a factory using the factory pattern. Now all we have to do is specify a flavor we need and we can get back the correct flavor:

flavor = IceCreamFactory().make('strawberry') #== <some.module.StrawberryIceCream>

The Problem - Real World Example

Here is a real world example of how I used this pattern. We are provisioning a server. For simplicity sake, we have 2 services we want to provision:

  • Postgres
  • Nginx

Now we need to provision both of these services on 2 different types of servers:

  • Ubuntu
  • Debian

Now notice here we have several different possibilities we can choose from:

  • Installing Postgres on Ubuntu
  • Starting Postgres on Ubuntu
  • Stopping Postgres on Ubuntu
  • Installing Nginx on Debian ... ... so on and so forth

There is an exponential number of possibilities, especially as we add new servers, new services or add layers in between them (for example installing via docker instead of bash commands).

How do we solve this issue? A bit of a combination of several patterns but we'll focus on the Factory for this article.

Solution

The solution here is to use 2 factories.

The first factory will be a ServerFactory which will be responsible for fetching any one of several servers (Ubuntu, Debian, Centos, ..).

The second factory will be a ServicesFactory which will be responsible for fetching any one of several services (Postgres, Nginx, RabbitMQ, ..).

The Server Factory

Using the boiler plate above let's go ahead and create our ServerFactory:

class ServerFactory:

    servers = {
        'ubuntu': UbuntuServer,
        'debian': DebianServer,
        'centos': CentosServer,
    }

    def make(self, server):
        return self.servers[server]()

Notice we just changed out the ice cream flavors for servers (UNIX flavors?).

The Service Factory

Now let's do the exact same thing for services:

class ServiceFactory:

    services = {
        'postgres': PostgresService,
        'nginx': NginxService,
        'rabbitmq': RabbitMQService,
    }

    def make(self, service):
        return self.services[service]()

notice all this code is the same boiler plate.

Combining The Factory

Now if we think of it from a database perspective:

A server has many services

Right? Since a server has many services, let's mimick that. Let's make the UbuntuServer have access to the ServiceFactory:

from app.factories import ServiceFactory

class UbuntuServer:
    
    services = ServiceFactory()

    def connect(self):
        self.establish_connection()
        return self

Boom. Done. Now our UbuntuServer has access to all of our services.

Using It All Together

Let's go ahead and use these 2 factories in our application:

from app.factories import ServerFactory

def show(self):
  user = User.find(1)
  user.server #== 'ubuntu'

  server = ServerFactory().make(user.server) #== <app.servers.UbuntuServer>
  server.connect()

  
  for service in ('postgres', 'nginx', 'rabbitmq'):
      server.services.make(service).install()

The Future

Design patterns are useful for solving problems now and in the future. This pattern is useful because:

  • supporting more servers is as simple as creating new servers.
  • supporting more services is as simple as creating new services.

These two things can exist separately of each other and therefore can be scaled up or down separately of each other.

Copyright Masonite 2019