Three Design Patterns (Factory, Abstract Factory, Builder) and Their Usage with Ruby

Eyüp Sercan Uygur
7 min readMar 7, 2022

--

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.

--

--