DEV Community

Cover image for Design Patterns in Ruby #4: Simple Factory vs Factory Method
Shrouk Abozeid
Shrouk Abozeid

Posted on

Design Patterns in Ruby #4: Simple Factory vs Factory Method

When building applications, creating an object is rarely as simple as calling User.new.

Different objects may require different initialization logic, validation, external API calls, or configuration. If every class is responsible for deciding what object to create, this logic quickly becomes scattered throughout the codebase.

Factory patterns solve this problem by separating object creation from object usage, making the code easier to maintain and extend.

Let's look at the two most common approaches: Simple Factory and Factory Method.

Simple Factory

it is not actually a design pattern by the book but it is commonly used.

it has a very basic idea of encapsulating the objects creation from it’s usage

it has a class ‘Simple Factory’ that contains the logic of creating objects , that factory is used in a “client” to create the objects then use it

Instead of allowing clients to instantiate objects directly, a single factory class becomes responsible for deciding which object should be created.

Structure

A Simple Factory consists of:

  • A factory class that contains all the object creation logic.
  • A single factory method that returns different objects based on some condition.
  • A client that uses the factory instead of creating objects directly.

a naive implementation of simple factory in ruby

class UserSimpleFactory
  def self.create_user(type, user_args)
    case type
    when 'admin'
      Admin.create(name: user_args[:name], email: user_args[:email], password: user_args[:password])
    when 'customer'
      Customer.create(name: user_args[:name], email: user_args[:email], password: user_args[:password])
    when 'guest'
      Guest.create(name: user_args[:name], country: user_args[:country])
   else
     raise 'INVALID_TYPE'
   end
  end
end

class RegisterHandler
  def create_user(params)
    user = UserSimpleFactory.create_user(params[:type], params[:user])
  end

  def register(params)
    user = create_user(params)
    "Hello #{user.name}"
  end
end
Enter fullscreen mode Exit fullscreen mode

Advantages

  • Centralizes object creation.
  • Keeps clients from depending on concrete classes.
  • Easy to understand and implement.

The Problem

This works well while you only have a few product types.

However, every time a new user type is introduced, the factory needs another when branch.

Eventually the factory becomes a large conditional statement responsible for creating every possible object.

This violates the Open/Closed Principle because adding a new product requires modifying an existing class.

at that time you might think to extract parts of this method into different classes and voilà you get a factory method pattern

iron man viola

Factory Method

The Gang of Four defines Factory Method as:

Define an interface for creating an object, but let subclasses decide which class to instantiate.

this pattern lets a class defer instantiation to subclass. and encapsulates the instantation of concrete types

The parent class defines the workflow, while each subclass decides which concrete object should be created.

This removes the need for long case statements and replaces them with polymorphism.

Structure

  1. Abstract creator : it gives you an interface with method(THE factory method) to create object(product), any other method on this class operates on the object returned by the factory method
  2. Concrete creator : is a subclass that implement the factory method
  3. Abstract product; interface(parent) of all products that different concrete creators return
  4. Concrete product must implement the same interface so that the factories can refere to the interface not the concrete classes

this pattern decouples the implementation of the product from its use

class RegisterHandler
  def create_user(user_args)
    raise NotImplementedError
  end

  def register(params)
    user = create_user(params)
    "Hello #{user.name}"
  end
end

class AdminRegister < RegisterHandler
  def create_user(user_args)
    Admin.create(name: user_args[:name], email: user_args[:email], password: user_args[:password])
  end
end

class CustomerRegister < RegisterHandler
  def create_user(user_args)
    Customer.create(name: user_args[:name], email: user_args[:email], password: user_args[:password])
  end
end

class GuestRegister < RegisterHandler
  def create_user(user_args)
    Guest.create(name: user_args[:name], country: user_args[:country])
  end
end
Enter fullscreen mode Exit fullscreen mode

Notice that the register method is implemented only once.

The only thing subclasses customize is how the user is created.

The registration workflow remains unchanged regardless of the user type.

Advantages

  • Eliminates large conditional statements.
  • Follows the Open/Closed Principle.
  • Makes it easy to introduce new product types.
  • Uses polymorphism instead of branching logic.
  • Keeps creation logic isolated inside dedicated creator classes.

The trade-off is that it introduces more classes, which can feel unnecessary for small applications.


Did you know that Rails framework is full of Factory patterns? 👀

Example: Action Mailer

Every mailer inherits from ApplicationMailer

When you define

class UserMailer < ApplicationMailer
  def welcome(user)
    mail(to:user.email)
  end
end
Enter fullscreen mode Exit fullscreen mode

The framework handles the workflow of building and delivering an email.

Your subclass supplies the message-specific details.

Example: Active Job adapters

You write:

class SendEmailJob < ApplicationJob
end
Enter fullscreen mode Exit fullscreen mode

Then configure:

config.active_job.queue_adapter = :solid_queue
Enter fullscreen mode Exit fullscreen mode

or

config.active_job.queue_adapter = :sidekiq
Enter fullscreen mode Exit fullscreen mode

Each adapter creates its own queue implementation.

Rails defines the workflow.

The adapter subclasses provide the implementation

Key Takeaways

A Simple Factory centralizes object creation using conditional logic. It's a pragmatic solution for smaller applications but becomes harder to maintain as the number of products grows.

Factory Method takes a different approach by replacing those conditionals with polymorphism. Instead of modifying one factory every time a new product is added, you create a new creator class that knows how to build that product.

A good rule of thumb is this:

If your factory keeps getting longer every time you introduce a new product, it's probably time to replace it with Factory Method.

for a full example in ruby check this https://github.com/ShroukAbozeid/design-pattern/tree/main/factory

Top comments (0)