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.