How many times have you disconsidered an external Ruby library only because it didn’t adhere to Object Oriented Programming principles?
I’ve been there too, it sucks. You want your code to be as stable as possible and easy to change. You don’t want to polute it by introducing fragile dependencies. But while that’s a respectable goal to strive for, in practice, you don’t always have that luxury.
Let’s look at some example code.
class MyClass def initialize @person = ThirdParty::Person.new("John", "Doe", 23) end end
Can you tell what is wrong with that code?
MyClass knows too much. It knows the name of the
Person class, the fact that it’s initializer requires three arguments, and the order of those arguments.
That means that whenever
Person changes, your class will have to change also. You don’t want that. That’s not how you build code that’s easy to extend.
There are two different dependencies here. The first one is on the name of the dependency (the
Person class), and the second one is about the order of its arguments. The later makes the subject of this article.
So how do you fix it? Obviously, you cannot change
Person‘s initializer method since you don’t own that dependency (its not your code). So, how do you fix it?
Well… you can use a factory object.
What is a factory object?
A factory object is an object that exists for the sole purpose of creating other objects.
I bet you didn’t get anything useful out of that sentence. I know, so let’s see what it means.
We will use a factory object to wrap the
module Wrapper def self.person(**args) ThirdParty::Person.new(args[:first_name], args[:last_name], args[:age]) end end class MyClass def initialize @person = ::Wrapper.person(first_name: "John", last_name: "Doe", age: 23) end end
In the code above, the
Wrapper module is a factory object. It’s sole purpose is to initialize a new
Person object. But it does so in a special way, it doesn’t just mimic
Person‘s initializer. It creates a different way to initialize a new
By transforming the method signature into a hash, you’ve accomplished two things.
- You’ve provided a form of documentation so people know what those arguments mean only by looking at the signature of the initializer.
- You’ve made the position of each argument irrelevant. Bingo!
For this tiny example, the benefits might not be obvious, but it’s easy to imagine how a factory could help when the method being wrapped takes many more arguments.
I’m using a module here instead of a class because it communicates to your future self and other developers that you don’t intend to instantiate it. You just need it to handle a message.