Factories
From Simple to Ridiculous
Recently I helped organize some Sandi Metz training at the University of Minnesota. It was great and I highly recommend you bring Sandi to your place of employment or that you attend one of her public courses.
During class we discussed factory methods and the different ways of implementing them. But we didn’t have enough time time to dive in to the options. I told my classmates that I would write something up, so here it is – a guided tour of factories from the simple to the ridiculous!
We have some code that returns a campus name based upon its abbreviation.
class CampusDetails
def campus_name(abbreviation)
case abbreviation
when "UMNTC"
"University of Minnesota Twin Cities"
when "UMNMO"
"University of Minnesota Morris"
else
"Unknown Campus"
end
end
end
That code works but it has a couple of smells. First, it’s got a giant case statement. Second, it’s obsessed with the string primitive of the campus abbreviation. But if there’s no reason to change this code you might be happy leaving it as is.
Let’s introduce a reason to change this code.
In addition to the campus’ abbreviation we want to know the campus’ mascot name. We might end up with code like this:
class CampusDetails
def campus_name(abbreviation)
case abbreviation
when "UMNTC"
"University of Minnesota Twin Cities"
when "UMNMO"
"University of Minnesota Morris"
else
"Unknown Campus"
end
end
def campus_mascot(abbreviation)
case abbreviation
when "UMNTC"
"Gopher"
when "UMNMO"
"Cougar"
else
"Unknown Mascot"
end
end
end
And now we have two duplicate case statements and two methods that obsess about the abbreviation string. What we want here is a factory; a way to create an object from the abbreviation and to have that logic live in only one location.
A simple first pass at a factory is to create a little private method inside the class you’re working with:
class CampusDetails
def campus_name(abbreviation)
build_campus(abbreviation).name
end
def campus_mascot(abbreviation)
build_campus(abbreviation).mascot
end
private
def build_campus(abbreviation)
case abbreviation
when "UMNTC"
UMNTC.new
when "UMNMO"
UMNMO.new
else
UnknownCampus.new
end
end
end
class UMNTC
def name
"University of Minnesota Twin Cities"
end
def mascot
"Gopher"
end
end
class UMNMO
def name
"University of Minnesota Morris"
end
def mascot
"Cougar"
end
end
class UnknownCampus
def name
"Unknown Campus"
end
def mascot
"Unknown Mascot"
end
end
We’ve isolated our case statement to one method and we’ve created a trio of simple objects that provide the behavior we want. Are we done? Well, maybe. What problems are there in this code? There’s still a case statement, which I’m not a fan of. But since it’s isolated in a private method I’m not that concerned.
Then along comes a new campus – University of Minnesota Crookston (mascot: Golden Eagle). We add it to our collection of classes:
# previous code sample is unchanged
class UMNCR
def name
"University of Minnesota Crookston"
end
def mascot
"Golden Eagle"
end
end
Easy! But wait. When we use the “UMNCR” abbreviation we get back an UnknownCampus. What happened? We forgot to tell our factory method about the new class.
Our factory method is nice but it requires that we change it every time we add a new campus. What should be a simple change – add a new campus – requires changes in two different places. This is Shotgun Surgery. Let’s refactor our factory to solve this problem before we introduce Crookston.
We’re pretty sure that all University of Minnesota campus classes will start with UMN. Maybe we can take advantage of that:
class CampusDetails
# this stuff is unchanged
private
def build_campus(abbreviation)
begin
# abbreviation[-2,2] gets the last two characters of the abbreviation string
Object.const_get("UMN#{abbreviation[-2,2]}")
rescue
UnknownCampus
end.new
end
end
That works. If we pass it an abbreviation that matches with a known class it will return a new instance of that class. Otherwise we’ll get a new instance of UnknownCampus. And when we add the UMNCR class the factory just works without any changes.
Then someone decides to add a class for University of Minnesota Rochester. Rochester’s an odd part of the UMN system; it’s technically part of the Twin Cities campus, except when it’s not. To highlight that oddness the programmer calls the class UMNTCRO. And when our factory tries to build a campus for “UMNTCRO” it fails and returns a UnknownCampus instead. Our factory relied on a naming convention and Rochester breaks that convention.
What if we had a way to show that all these classes were related? Then our factory could use that relationship to figure out which one to build. Since Ruby is so thoroughly object-oriented we do have this tool – inheritance!
All of our classes are specialized versions of a Campus concept and if we reflect that with inheritance we can take advantage of some Ruby niceties to help our factory be more flexible.
class Campus
def name
raise NotImplementedError
end
def mascot
raise NotImplementedError
end
end
class UMNTC < Campus
# the internals are unchanged from the last example
end
class UMNMO < Campus
# the internals are unchanged from the last example
end
class UMNCR < Campus
# the internals are unchanged from the last example
end
class UnknownCampus < Campus
# the internals are unchanged from the last example
end
After we refactor to inheritance, we can add this monkey patch
class Class
def subclasses
ObjectSpace.each_object(Class).select { |klass| klass < self }
end
end
This uses Ruby’s built in ObjectSpace
module to discover every subclass of a class. Now every class can tell us its subclasses.
puts Campus.subclasses
#=> [UMNTC, UMNMO, UMNCR, UnknownCampus]
This is nice but it doesn’t help us yet. Our factory has a way to find all of the Campus classes but no way of knowing which one it should build. We can fix this by adding a method to each of our Campus classes.
class Campus
def self.handles?(_)
false
end
end
class UMNTC < Campus
def self.handles?(abbreviation)
abbreviation == "UMNTC"
end
end
class UMNMO < Campus
def self.handles?(abbreviation)
abbreviation == "UMNMO"
end
end
class UMNCR < Campus
def self.handles?(abbreviation)
abbreviation == "UMNCR"
end
end
class UnknownCampus < Campus
def self.handles?(_)
true
end
end
UMNCR.handles?("UMNCR")
#=> true
UMNCR.handles?("UMNTC")
#=> false
Now our Campus classes can tell our factory which one it should build, we just have to ask if it handles our abbreviation.
class CampusDetails
# this stuff is unchanged
private
def build_campus(abbreviation)
Campus.subclasses.detect { |campus_class| campus_class.handles?(abbreviation)}.new
end
end
The detect
method iterates through the subclasses and returns the first class that says it can handle our abbreviation. With this change we can easily add UMNTCRO without having to further change our factory:
class UMNTCRO < Campus
def self.handles?(abbreviation)
abbreviation == "UMNTCRO"
end
def name
"University of Minnesota Rochester"
end
def mascot
"Raptor"
end
end
This probably works. I say probably because it relies on classes being loaded in the right order. Remember that UnknownCampus
says it handles everything. If it’s not the last class in your array of subclasses your factory is going to fail some of the time.
For example, if UnknownCampus
gets loaded before UMNTCRO
then Campus.subclasses
will look like this:
puts Campus.subclasses
#=> [UMNTC, UMNMO, UMNCR, UnknownCampus, UMNTCRO]
And detect
will return the first class that says it handles the abbreviation. So if you ask your factory to build a campus with the abbreviation “UMNTCRO”, it will return an UnknownCampus. Because it asked UnknownCampus first and UnknownCampus said “Sure! I handle that!”
We can get around this a few different ways. One is to take UnknownCampus out of the hierarchy and delete its handles?
method.
class UnknownCampus
def name
"Unknown Campus"
def
def mascot
"Unknown Mascot"
end
end
This means that UnknownCampus
no longer appears in the collection of Campus
subclasses. We then use the almost-never-seen behavior of using a default with detect
class CampusDetails
# this stuff is unchanged
private
def build_campus(abbreviation)
Campus.subclasses.detect(->{UnknownCampus}) { |campus_class| campus_class.handles?(abbreviation)}.new
end
end
It’s possible that behavior is never used because it looks goofy as hell. If you provide a Proc or lambda to detect
it will be called if your block returns nil. So, if no Campus
subclass says it can handle your abbreviation you’ll get an UnknownCampus instance.
Syntactic oddness aside this solves our load order problem. So let’s leave it be.
Our factory is pretty good at this point, right? As long as programmers remember to have new campuses inherit from Campus
and define the handles?
method everything should just work.
And then someone decides to implement “College in the Schools” which teaches college level classes in high schools. It’s kind of like a Campus
…kind of.
class CollegeInTheSchools < Campus
def self.handles?(abbreviation)
abbreviation == "CITS"
end
def name
"College in the Schools"
end
def mascot
# No mascot.
""
end
end
There are a bunch of other ways in which this class is not Campus-like, but I’ll leave those to your imagination. The relevant point here is that our inheritance approach can break down. CollegeInTheSchools
is campus-like enough to satisfy our factory (since it responds to both name
and mascot
), but it’s not campus-like enough that it should inherit from our Campus
class. However our factory demands that all campus-like classes inherit from Campus
.
This is a pickle.
Instead of inheritance what if classes that were ‘campus-like’ could register themselves as ‘campus-like’? A simple approach would be to store a list of campus-like classes in a coniguration file:
CAMPUS_LIKE = [UMNTC, UMNRO, UMNMO, UMNTCRO, CampusInTheSchools]
and then our factory could find the right ‘campus-like’ class:
class CampusDetails
# this stuff is unchanged
private
def build_campus(abbreviation)
CAMPUS_LIKE.detect(->{UnknownCampus}) { |campus_class| campus_class.handles?(abbreviation)}.new
end
end
This works but it re-introduces a problem we had back at the beginning. If we add a new class we have to remember to add it to the list of ‘campus-like’ things. We’re back at Shotgun Surgery.
What if each class were responsible for reporting itself as ‘campus-like’? It’d look something like this:
class UMNTC
# everything else is unchanged
def self.campus_like?
true
end
end
#The other classes have the same method, so there's no reason to include them.
Then we could change CAMPUS_LIKE
to be a bit more dynamic:
CAMPUS_LIKE = ObjectSpace.each_object(Class).select do |klass|
klass.respond_to?(:campus_like?) && klass.campus_like?
end
With this change our CAMPUS_LIKE
constant is populated with every class that claims to be campus-like. The name of these classes is irrelavant, the ancesntors of these classes is irrelevant. The only thing that matters is that the class says it’s campus-like.
This approach is not foolproof, obviously. If we add a new class is added that should be campus-like but forget to implement the method, our factory will never build our new class. Alternatively, we could claim that a class is campus-like when it is not and our factory will return invalid objects. It doesn’t matter which factory you pick, there is always a way to screw it up.
Possibly worse, this factory also has the downside of being utterly baffling to newcomers to this code. Whatever its faults, the case statement we started off with was easy to understand. And compare that to the code we have now:
class UMNTC
def self.campus_like?
true
end
def self.handles?(abbreviation)
abbreviation == "UMNTC"
end
def name
"University of Minnesota Twin Cities"
end
def mascot
"Gopher"
end
end
class UMNMO
def self.campus_like?
true
end
def self.handles?(abbreviation)
abbreviation == "UMNMO"
end
def name
"University of Minnesota Morris"
end
def mascot
"Cougar"
end
end
class UMNCR
def self.campus_like?
true
end
def self.handles?(abbreviation)
abbreviation == "UMNCR"
end
def name
"University of Minnesota Crookston"
end
def mascot
"Golden Eagle"
end
end
class UMNTCRO
def self.campus_like?
true
end
def self.handles?(abbreviation)
abbreviation == "UMNTCRO"
end
def name
"University of Minnesota Rochester"
end
def mascot
"Raptor"
end
end
class CollegeInTheSchools
def self.campus_like?
true
end
def self.handles?(abbreviation)
abbreviation == "CITS"
end
def name
"College in the Schools"
end
def mascot
""
end
end
class UnknownCampus
def name
"Unknown Campus"
end
def mascot
"Unknown Mascot"
end
end
CAMPUS_LIKE = ObjectSpace.each_object(Class).select do |klass|
klass.respond_to?(:campus_like?) && klass.campus_like?
end
class CampusDetails
def campus_name(abbreviation)
build_campus(abbreviation).name
end
def campus_mascot(abbreviation)
build_campus(abbreviation).mascot
end
private
def build_campus(abbreviation)
CAMPUS_LIKE.detect(->{UnknownCampus}) { |campus_class| campus_class.handles?(abbreviation)}.new
end
end
I think someone new to this code would be justified in cursing. But there are cases where a factory like this (or one even more abstract) are exactly what you need. Are you working with a huge code base with tons of classes and factories? Do you have lots of classes that satisfy lots of different roles? Then this sort of abstraction might be perfect for you. Are you dealing with classes that almost never change and a factory that never needs to know about new collaborators? Then this abstraction is overkill.
That’s the lesson of abstraction, really. There’s not a level of abstraction that’s universally wrong, it’s always context dependent. You have to find the level that’s right for your code and your programmers.
If you want to read more about Rust, Ruby, Refactoring and other things that start with R (and maybe other letters), maybe check out my totally free newsletter. You can read previous newsletters, or sign up for free. Comments/feedback/&c. welcome on twitter, at ian@ianwhitney.com, or leave comments on the pull request for this post.