About

Gem version Build status Coverage status CodeClimate status YARD documentation

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.

JsonEncoder = import('registry').json_encoder

class Post
  attribute :json_encoder, '.generate' { JsonEncoder }
end

Now when you decide to switch to say oj, all you have to do is this:

# registry.rb

# Lazy loading, the result is cached.
export(:json_encoder) do
  import('adapters/oj')
end

So instead of having bunch of require 'json' calls, JSON module referenced in many places and #to_json calls used on monkey-patched core classes (ay!), we have one require statement, one statement of registering the encoder and then every class specifying what does it need from the encoder.

The last bit is very powerful. Instead of declaring an interface (from the top down) and implementing all the methods interface requires, we basically define something like interface for our project (from the bottom up).

OK, JSON encoders all looks the same. But what about say HTTP requests?

# http_adapter.rb

require 'net/http'

def exports.get(url)
  # TODO: Implement using net/http.
end
# registry.rb

export(:http_adapter) do
  import('net_http_adapter')
end

export log_path: '/var/logs/post.log'

export(:logger) do
  require 'logger'
  Logger.new(exports.log_path)
end
http_adapter, logger = import('registry').grap(:http_adapter, :logger)

class Post
  attribute :http_adapter, '.get' { http_adapter }
  attribute :logger, :debug, :info, :error { logger }
end

HTTP libraries have bunch of methods. But in this case, all we care about is to have a #get method accepting URL and returning a stream.

But ... what about need-based coding (TODO: find how it's really called).

Specifying the dependencies and putting in a trivial adapter will take you few seconds at the beginning on the project. And fair enough, maybe your startup goes bancrupt in 5 months and you won't ever have to deal with switching libraries.

But if not, and the project grows with implicit dependencies, good luck switching anything. The 2 minutes you saved on the beginning will cost you hours, if not days plus a lot of pulled out hair all over.

And this happens. During my early years Hpricot was the library one would use to parse HTML.So say I'd write a project using the library. These days the new kids has never even heard of Hpricot and after _why went missing, there's no chance of new version or even anyone knowing much about it anymore.

The solution? Switching to Nokogiri of course.

Writing adapters will force you to be simple. That's good.

And yes, there are libraries that you won't be able to mock out this way. If they'd be gone, you'd have to make some changes. For instance scheduled-format relies on parslet. Would parslet be gone, I'd have to change or replace the Parser and Transformer class and the .parse method.

It's not about doing the theoretically right thing, it's about doing what's best for productivity and maintanability. And yes, there is a line. But at least you'll make a conscious decision, what's a pluggable dependency and what's the one or few libraries that the project just can't do without.

Replacing JSON everywhere except of file n say because of a bug (or RAILS_ENV n? better debuggin etc?)

And let's say you use some libraries that all use JSON. Wouldn't it be nice if you could switch them to use Oj instead?

import('my_lib')

my_lib.registry.json_adapter = Oj
# task.rb
export Task: (Task = Class.new)

# task_list.rb
Task = import('registry').Task

export TaskList: (TaskList = Class.new {
  attribute :task_class, '#name' { Task }
})

# registry.rb
export(:Task) do
  import('mylib/task')
end

TODO