Three Design Patterns (Factory, Abstract Factory, Builder) and Their Usage with Ruby
If you all have is a hammer, everything becomes a nail…
Probably you’ve heard the term design pattern. The name is self-explanatory however understanding them and especially knowing when to use them is a real coding ability. In this article, I’ll make a brief introduction to what is a design pattern, giving 22 of them and 3 of them with code examples with Ruby. Before starting I have to tell you that, they are not the all and I’m pretty sure that with evolving technology and the tools, there will be more of them, with higher needs for efficiency, abstraction, reusability, and so on…
What is Design-Pattern?
A design pattern is a way you structure your code to achieve a certain task. Of course, a real-world example can be solved in different ways. However, while solving the problem, it’s also important how you get there! So for this, when you are thinking about a problem, the first thing you should need to ask yourself is what is going to be the result? Then sketch a map to go there. Then during this sketch, you’ll find yourself refactoring the code, making it re-usable, error-prone, fast. The solution is your algorithm, the way you structure your algorithm is your pattern. Nowadays in the industry, there are some accepted ways of solving some problems and we call them to design patterns. All developers should know them, at least get a sense of it so when they see some different ‘approach’ to a problem, they can understand the code, and follow the ‘path’ of the algorithm. Without structural thinking, it’s unbelievably easy to miss the right solution, especially when you are reading some other language that you don’t use regularly. Opposite, if you are aware of the patterns, from the first look some ideas will come to your mind about the approach, maybe you can improve the solution as well. As a quick note, I’m an OOP (Object Oriented Programming) guy. In this article, I’ll try to investigate the OOP design patterns, nothing more than that! Also, I’m not going to dive deep into the history, owner, or that kind of information about the patterns. So this will be a highly technical articles series, take your seat, grab your coffee and follow me!
First of all, let’s give the sub-trees;
We can divide patterns into 3 subgroups:
1Creational patterns: Mainly focused on object creation strategies, increase flexibility, and reuse.
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Singleton
2Structural patterns: Mainly focused on how to assemble the objects and classes to a higher hierarchy.
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
3Behavioral patterns: Mainly focused on communication and role sharing between objects.
- Chain of Responsibility
- Command
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Factory
The factory model is a creational design pattern, that gives us an interface for creating objects in a super class but allows subclasses to alter the objects.
class Factory# This is our constructor and we are changing its implementation in the subclasses def factory_method raise NotImplementedError, “#{self.class} does not have the ‘#{__method__}’” end def wanted_operation processed = factory_method result = “Factory returns the result with #{processed.operation}” endend
require(‘./Factory’)require(‘./CarProduct’)class Car < Factory def factory_method CarProduct.new endend
require(‘./Product’)class CarProduct < Product def operation result = { car: ‘BMW’, model: ‘i-5’, price: ‘20000’} endend
# This is our intermediary between creators and the products.class Product def operation raise NotImplementedError, “#{self.class} does not have the ‘#{__method__}’” endend
require(‘./Factory’);require(‘./ShipProduct’)class Ship < Factory def factory_method ShipProduct.new endend
require(‘./Product’)class ShipProduct < Product def operation result = { ship: ‘DYR’, model: ‘op-1’, price: ‘70000’} endend
This model is used to separate object creation from the class that it belongs to. If you can create an object without calling the constructor of its class, then you have the flexibility you need. For example, we have a complicated code base and we don’t know how to add new classes to this codebase. If we change this codebase to a factory model, here all we need to do is add a new product and creator as in the below so we can have our constructed object while we are calling the client object with its creator class.
require(‘./Factory’)require(‘./TruckProduct’)class Truck < Factory def factory_method TruckProduct.new endend
require(‘./Product’)class TruckProduct < Product def operation result = { truck: ‘Ford’, model: ‘F-max’, price: ‘42500’} endend
Now, all we need to do is call the client method.
require(‘./Car’)require(‘./Ship’)require(‘./Truck’)def client_code(creator) creator.wanted_operationendp client_code(Car.new)p client_code(Ship.new)p client_code(Truck.new)
“Factory returns the result with {:car=>\”BMW\”, :model=>\”i-5\”, :price=>\”20000\”}”“Factory returns the result with {:ship=>\”DYR\”, :model=>\”op-1\”, :price=>\”70000\”}”“Factory returns the result with {:truck=>\”Ford\”, :model=>\”F-max\”, :price=>\”42500\”}”
If you inspect the code we are calling the creator classes inside the client_code method and on top of this object, we are calling the wanted operation method. This method calls the factory_method and creates an object. On top of this object, we are calling the operation method. We defined the operation method inside the Product and modify them inside the product subclasses. Finally, the result we want was returned.
Abstract Factory
Let’s change our example. Right now we have 2 different cars and two different vessels. Here we want to combine them into our results. We are creating our vehicles and combining them with the help of our abstract classes. From abstract classes, we are inheriting our methods and manipulating these methods inside subclasses. We aim to return one car with one vessel that have a lesser amount price, and one car and one vessel that have higher prices. If we dig into more we see that we created objects as cars and vessels and for vessels objects, we add combination methods in the end. Intermediary functions just return the object values, and collaborator functions are responsible for interchangeable interactions between cars and vessels.
class AbstractFactory# This is our constructor and we are changing it’s implementation in the subclasses def factory_method_a raise NotImplementedError, “#{self.class} does not have the ‘#{__method__}’” end# This is our constructor and we are changing it’s implementation in the subclasses def factory_method_b raise NotImplementedError, “#{self.class} does not have the ‘#{__method__}’” endend
require(‘./Factory’)require(‘./CarProduct1’)require(‘./ShipProduct’)class VehicleA < AbstractFactory def factory_method_a CarProduct1.new end def factory_method_b ShipProduct.new endend
require(‘./Factory’);require(‘./CarProduct2’)require(‘./SailBoatProduct’)class VehicleB < AbstractFactory def factory_method_a CarProduct2.new end def factory_method_b SailBoatProduct.new endend
class AbstractGroundProduct def intermediary_a raise NotImplementedError, “#{self.class} does not have ‘#{__method__}’” endend
class AbstractSeaProduct def intermediary_b raise NotImplementedError, “#{self.class} does not have ‘#{__method__}’” end def collaborator_b(_collaborator) raise NotImplementedError, “#{self.class} does not have ‘#{__method__}’” endend
require(‘./AbstractGroundProduct’)class CarProduct1 < AbstractGroundProduct def intermediary_a result = { car: ‘BMW’, model: ‘i-5’, price: ‘20000’} endend
require(‘./AbstractGroundProduct’)class CarProduct2 < AbstractGroundProduct def intermediary_a result = { car: ‘Renault’, model: ‘Megane’, price: ‘12000’} endend
require(‘./AbstractSeaProduct’)class SailBoatProduct < AbstractSeaProduct def intermediary_b result = { sailboat: ‘NZYC’, model: ‘XH-3’, price: ‘8250’}end def collaborator_b(collaborator) “#{collaborator.intermediary_a} cooperate with #{intermediary_b}” endend
require(‘./AbstractSeaProduct’)require(‘pry’)class ShipProduct < AbstractSeaProduct def intermediary_b result = { ship: ‘DYR’, model: ‘op-1’, price: ‘70000’}end def collaborator_b(collaborator) “#{collaborator.intermediary_a} cooperate with #{intermediary_b}” endend
require(‘./VehicleA’)require(‘./VehicleB’)def client_code(creator) car_product = creator.factory_method_a vessel_product = creator.factory_method_b vessel_product.intermediary_b vessel_product.collaborator_b(car_product)endp client_code(VehicleA.new)p client_code(VehicleB.new)
“{:car=>\”BMW\”, :model=>\”i-5\”, :price=>\”20000\”} cooperate with {:ship=>\”DYR\”, :model=>\”op-1\”, :price=>\”70000\”}”“{:car=>\”Renault\”, :model=>\”Megane\”, :price=>\”12000\”} cooperate with {:sailboat=>\”NZYC\”, :model=>\”XH-3\”, :price=>\”8250\”}”
Here, inheritance is the key. All abstract classes don’t have the expected implementation and subclasses have it. For every created object, we just need to create a class that inherits from those abstract classes with the methods that we would like to change. Of course, the return values could be different. I prefer this to show the interaction between the objects and their methods. Please follow the created object and the parent class one by one, so you’ll grasp the idea behind it.
Builder
Before we were combining the results of objects directly. In this approach, we’ll split them into pieces and there will be another logic to combine them. Here is the aim, having a step-by-step approach to create our object. Some object needs more features some do not! So a car needs extra fog lights and the other car does not need them at all. In this case, instead of creating the car, we’ll create the features objects.
class Builder def chasis raise NotImplementedError, “#{self.class} does not have the ‘#{__method__}’” end def engine raise NotImplementedError, “#{self.class} does not have the ‘#{__method__}’” end def fog_lights raise NotImplementedError, “#{self.class} does not have the ‘#{__method__}’” endend
require(‘./Builder’)require(‘./Product’)class Production < Builder def initialize @product = Product.new end def chasis @product.add({chasis: true}) end def engine @product.add({engine: true}) end def fog_lights @product.add({fog_lights: true}) endend
class Product def initialize @parts = {} end def add(part) @parts.merge!(part) end def result @parts endend
require(‘./Production’)def client_code minimum_car = Production.new minimum_car.chasis minimum_car.engineendp client_code{:chasis=>true, :engine=>true}
If we call minimum_car.fog_lights method the result would be;
{:chasis=>true, :engine=>true, :fog_lights=>true}
So with the help of this method, we can create different types of objects as we want.
You can continue the patterns from the list above and keep learning.