The Factory Method Pattern in Ruby

Feb 10, 2022 - 4 min read
The Factory Method Pattern in Ruby
Share

Here's a video version of this article if you prefer to watch instead:

What the Factory Method pattern allows you to do is to isolate conditional instantiations so that when you do need to change them, you'll make the change in just one method.

But to illustrate this, let's look at an example.

Imagine you have this code.

class Endpoint
  def home(params)
    if params[:user_type] == "admin"
      Admin.new
    elsif params[:user_type] == "member"
      Member.new
    else
      Guest.new
    end
  end
end

So in this example you can see we have some params there that you might get from a user submitted form, and the params hash includes a user type.

Now if the user type is admin then we want to create an admin object but if the user type is member we want to instantiate a member object.

Otherwise if the user type is none of those two options, then we will create a guest object.

So what is the problem with this code? By looking at it in isolation, the problem is not obvious.

But let's take it one step further.

Let's imagine we have the same code block used twice inside this class.

class Endpoint
  def home(params)
    if params[:user_type] == "admin"
      Admin.new
    elsif params[:user_type] == "member"
      Member.new
    else
      Guest.new
    end
  end

  def contact(params)
    if params[:user_type] == "admin"
      Admin.new
    elsif params[:user_type] == "member"
      Member.new
    else
      Guest.new
    end
  end
end

So we have two routes namely home and contact and they both take a use type in the params, and based on it they create a user object.

To determine the kind of user they are working with they both need to go through the same process.

But this logic could be reused in multiple places throughout the application.

So when the time comes to add a new type of user to your application, you will have to go through each one of these code blocks and add the new type to all of them. Hopefully you don't forget to update one of them.

So that's one problem. But there is one more.

Once you create a user object you need to make sure that all of the user objects behave the same way. Because you will use them all in the same way.

Namely you will call the same methods on the resulting object, no matter what class it was created from.

And one way to make sure that all these objects behave the same way, i.e. they respond to the same methods, is to create an abstract class from which all the user classes inherit from.

Here's how that looks like.

class UserBase
  def first_name = raise("not implemented")
  def last_name = raise("not implemented")
end

class Admin < UserBase; end
class Member < UserBase; end
class Guest < UserBase; end

The UserBase class doesn't do much in terms of behavior. But what it does do is force all subclasses to overwrite its methods.

So if your endpoint is using those methods they need to be available across all the objects. Otherwise you'll get an exception.

class Endpoint
  def home(params)
    user = if params[:user_type] == "admin"
      Admin.new
    elsif params[:user_type] == "member"
      Member.new
    else
      Guest.new
    end

    full_name = [user.first_name, user.last_name].join(" ")
    { name: full_name }.to_json
  end
end

Ok, so let's got back to the Factory Method pattern.

What the Factory Method pattern does is it helps you with scenarios like these where you have some logic for creating different objects of the same class.

It does it by encapsulating the logic in a method.

So let's create a new class, and move the conditional in there.

class UserFactory
  def call(user_type)
     if user_type == "admin"
      Admin.new
    elsif user_type == "member"
      Member.new
    else
      Guest.new
    end
  end
end

class Endpoint
  def home(params)
    user = UserFactory.call(params[:user_type])
    full_name = [user.first_name, user.last_name].join(" ")
    { name: full_name }.to_json
  end

  def home(params)
    user = UserFactory.call(params[:user_type])
    { first_name: user.first_name }.to_json
  end
end

By doing that, we've isolated the instantiation of the user object into a different class that we can now reuse across our entire codebase.

And when the time comes to add a new use type to our application, we only need to change that one method. Everything else stays the same.

And because the change is so small, it's a lot harder to introduce bugs in your application.

So that's all there is to the Factory Method pattern.

12 Project Ideas
Cezar Halmagean
Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.