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.
Top comments (3)
You can improve it a little!
Add
*args, **kwargs
to it and will allow pass vars to
make
.Like
ServiceFactory().make('foo_bar', foo, bar)
Awesome and simple tutorial! I've listened about Factory little time ago, but now I understood it completely and just see that, we have one similar solution, very, very, very hard to same solution lol... lets refactor our code to use more Factory :) thanks!
Wow, Thanks for writing this as I wanted to know more about cause I have used Masonite but didn't know that this is called factory pattern.