About
How much abstraction is too much?
On one hand IoC is probably a too heavy canon for a dynamic language like Ruby, instantiating external dependencies in #initalize
doesn't sound like a good idea either.
If I have to replace a dependency, I want to know what interface it has to provide.
That is what methods is my project actually using, so not the same as class MyCollection implements ListInterface
.
Example
require 'import'
Interfacer = import('interfacer').Interfacer
class Post
extend Interfacer
attribute(:time_class, '.now', '#to_s') { Time }
def publish!
"~ Post #{self.inspect} has been published at #{self.time_class.now} (using #{self.time_class})."
end
end
puts "~ With the default time_class."
post = Post.new
puts post.publish!
puts "\n~ With overriden time_class."
require 'date'
post = Post.new
post.time_class = DateTime
puts post.publish!
To me, this is what I consider to be the golden middle way. There's nearly no extra code, no factory method etc, but I can replace the time class any time I want.
Why?
class TimeMock
class << self
alias_method :now, :new
end
def to_s
'Monday evening'
end
end
describe Post do
before(:each) do
subject.time_class = TimeMock
end
it "prints out when a post was published" do
expect(post.publish!).to match(/has been published at Monday evening/)
end
end
You can say that you could just stub Time.now
and you're right, but I'm not a huge fan of that approach. I like clear dependencies, actual objects and (on a slightly different subject, but still vaguely related) I think tests should test public APIs and not order in which things are executed (when used mocks), because everything breaks when you do internal refactoring.
But whatever, let's have an another example.
settings = import('settings')
class Post
attribute :json_encoder, '.generate' { settings.json_encoder }
end
Now when you decide to switch to say oj
, all you have to do is this:
# settings.rb
require 'oj'
export json_encoder: Oj