Skip to content

Instantly share code, notes, and snippets.

@richmolj
Last active December 18, 2015 14:08
Show Gist options
  • Save richmolj/5794539 to your computer and use it in GitHub Desktop.
Save richmolj/5794539 to your computer and use it in GitHub Desktop.
Dependency Injection
# Everyone should like some amount of Dependency Injection in Ruby.
# How much you like usually depends on how much importance you place on
# two competing values:
#
# 1. Build extensibly, loosely coupled components so you can properly
# adapt as your application scales. And things that start simple usually
# end up complex, so keep a solid foundation (read: solid OOP foundation
# in these concepts early on).
#
# and
#
# 2. Build the simplest thing possible, because YAGNI.
#
# I tend to agree with Jeff Atwood's perspective:
# http://www.codinghorror.com/blog/2004/10/kiss-and-yagni.html
# And with probably the seminal post on DI in ruby by Jamis Buck:
# http://weblog.jamisbuck.org/2008/11/9/legos-play-doh-and-programming
# And follow-up http://weblog.jamisbuck.org/2007/7/29/net-ssh-revisited
#
# DI isn't without benefits, but I feel proponents tend to turn a blind
# eye to the tradeoffs.
#
# DI is an obtrusive level of indirection
# Example from Objects on Rails:
class Post
def publish(clock = DateTime)
self.pubdate = clock.now
blog.add_entry(self)
end
end
# Set the published date on the post when adding to a blog.
# This is great because if we want to add an alternate strategy
# we just sub-out the injected class with a different one. For instance,
# publish after a review period:
class DelayClock
def now
DateTime.now + 24.hours
end
end
@post.publish(DelayClock.new)
# So that's the upside. The downside is our mental model goes from
# "A post becomes published in 24 hours after it is added" to
# "A blog entry could be a post, or something else. In the event it's
# a post, it will be published some period after saving. Look up the
# injected strategy to figure out when".
#
# This is only going to get more complex as we keep adding classes and
# treat everything as an abstraction. Abstraction == level of indirection
# == added complexity. I recommend looking at Net::SSH 1.1.4 (pointed
# out in the blog post above) for a larger, real-world example
#
# When this is a good thing
class Post
def initialize(backend)
@backend = backend
end
def all
@backend.all
end
end
Post.new(DB::Post).new
Post.new(Service::Post).new
# This is great because we have a subbable backend we can inject
# *and we actually need to use one or the other*. It's the difference
# between "we need to ways of finding Posts" and "we MIGHT need two ways
# of finding posts". Not only that, but the obtrusiveness of DI is actually a
# benefit: the code is screaming, "HEY! This is specially configured to support
# multiple strategies!"
#
# Service Locator Pattern
# I was disapponted Objects on Rails didn't touch on this, because a) it's the
# alternative to DI in acheiving the D in SOLID and b) it's more common in the
# ruby community.
#
# It's also usually less heavy-handed than DI. This post has a great
# example that progresses from DI to Service Locator:
# http://kresimirbojcic.com/2011/11/19/dependency-injection-in-ruby.html
#
# Let's say we want all Net::HTTP requests to have a configurable timeout. We could
# inject this:
class LongTimeout
def self.setup
http = Net::HTTP.new
http.read_timeout = 99999
http
end
end
SomeClass.new(LongTimeout.method(:setup))
# But we have to change the constructor to support this injection, and
# it's a little clunky. We can get the same modularily from
class Configuration
@settings = {}
def self.[](key)
@settings[key]
end
def self.[]=(key, value)
@settings[key] = value
end
end
Configuration.settings[:http] = proc {
http = Net::HTTP.new
http.read_timeout = 99999
http
}
class SomeClass
def query_google
Configuration[:http].get 'http://google.com'
end
end
# Which is basically the same thing as DI but less obtrusive and more commonly seen.
# This will also help when we want to use 3rd-party libraries that aren't expecting
# you to dependency inject everything. Carrierwave suggests overriding store_dir
# within an Uploader:
class MyUploader < CarrierWave::Uploader::Base
def store_dir
Configuration[:store_dir]
end
end
# Instead of subclassing MyUploader, modifying the constructor, and passing it
# in (or using a factory) we just reference Configuration. In addition,
# CarrierWave associates an Uploader with an ActiveRecord class by:
mount_uploader :avatar, AvatarUploader
# We don't have to look at mount_uploader and make sure our custom instance will
# work correctly. We just use Carrierwave like normal.
# This pattern is so popular you'll often see this abstracted in many gems:
# http://robots.thoughtbot.com/post/344833329/mygem-configure-block
#
# Service Locator vs DI
#
# Once again, I use DI when I WANT its obtrusiveness. If I really do have an
# abstracted domain where things are being subbed in and out, I want to call
# attention to this. If I just want a damn timeout or per_page setting, I'll
# use Service Locator.
#
# The Problem With Service Locator
#
# Configuration is a mutable global state, so if we want this value dependent
# on the request, we'll have clashes.
#
# Sidekiq (think multithreaded Resque) has this problem. It starts a bunch of
# workers in the same process. Each worker should be able to write to a common log,
# but it would be useful to include some metadata about the worker
# and about the job it's working on, so we can get something like this:
"worker 1 on job foo : Hello World!"
# It'd be pretty heavy-handed to pass around a custom logger to every class that would
# like to call Sidekiq.logger.info. This is how it's done (altered for brevity):
module Sidekiq
module Logging
class Pretty < Logger::Formatter
def call(message)
"#{Thread.current[:sidekiq_context]} : #{message}"
end
end
def self.with_context(msg)
begin
Thread.current[:sidekiq_context] = msg
yield
ensure
Thread.current[:sidekiq_context] = nil
end
end
end
end
Sidekiq::Logging.with_context("#{worker.class.to_s} JobID #{item['jid']}") do
Sidekiq.logger "Hello world!"
end
# with_context sets a thread local, a value only accessible to the current thread.
# we no longer have global mutable state, but we do have configuration we can reference
# anywhere. Take the same concept and apply to our Carrierwave example:
class ConfigurationMiddleware
def call(env)
begin
Thread.current[:__configuration__] = {:store_dir = something_via_request}
@app.call(env)
ensure
Thread.current[:__configuration__] = nil
end
end
end
class Configuration
def self.[](key)
Thread.current[:__configuration__][key]
end
end
class MyUploader < CarrierWave::Uploader::Base
def store_dir
Configuration[:store_dir]
end
end
# So now we have a Service Locator that *does not have mutable global state*.
# We have created a request local.
#
# Time Zones
#
# Rails takes the Thread.current idea and uses it without either pattern. This
# is a great example of how run-time configuration can be so simple you don't
# even have to think about it. Let's say we want time zones to be specific to
# the user. Straight from the Rails docs:
around_filter :set_time_zone
def set_time_zone
begin
old_time_zone = Time.zone
Time.zone = current_user.time_zone if logged_in?
yield
ensure
Time.zone = old_time_zone
end
end
# Underneath the hood you'll find (altered for simplicity):
def self.zone=(val)
Thread.current[:time_zone] = val
end
def self.zone
Thread.current[:time_zone]
end
# DI is often useful. But I don't usually find that the problem with a piece
# of legacy code is that it's well-written and understandable but tightly coupled.
# I do often find hard-to-navigate levels of indirection and abstraction (often
# without even bringing DI into the mix). That's why my experience has me value
# the KISS principle over dogmatic OOP. While both have their problems, I personally
# have less with the format than the latter, which is why I value it more.
@richmolj
Copy link
Author

I had a side-discussion with Lou, which I think is the best, most concise summary of each viewpoint. Please see https://gist.github.com/richmolj/5815196, I think it really distills the debate to its core concepts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment