Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
# Bad because the dependency on ModelClass and ApiClass are hard-coded
module HitTheApi
def self.go
ids_to_update = ModelClass.what_ids_should_I_update
ids_to_update.each do |id|
data = ApiClass.fetch_me_data(id)
ModelClass.persist_data(id, data)
end
end
end
# Cleaner 'go' method, but now the dependencies are hard-coded and pushed deeper. Worse.
module HitTheApi
def self.go
ids_to_update.each do |id|
fetch_and_persist(id)
end
end
private
def self.ids_to_update
ModelClass.what_ids_should_I_update
end
def self.fetch_and_persist(id)
data = ApiClass.fetch_me_data(id)
ModelClass.persist_data(id, data)
end
end
# Deps passed in makes things easy to test
module HitTheApi
def self.go(model, api)
ids_to_update = model.what_ids_should_I_update
ids_to_update.each do |id|
data = api.fetch_me_data(id)
model.persist_data(id, data)
end
end
end
# If the 'go' method needs to do something more complex, breaking out private methods starts to be painful
module HitTheApi
def self.go(model, api)
ids_to_update(model).each do |id|
fetch_and_persist(api, model, id)
end
end
private
def self.ids_to_update(model)
model.what_ids_should_I_update
end
def self.fetch_and_persist(api, model, id)
data = api.fetch_me_data(id)
model.persist_data(id, data)
end
end
# Similar to 2, but now you're instantiating something for no conceptual reason
# or any real benefit to the code
module HitTheApi
def initialize(model, api)
@model = model
@api = api
end
def go
ids_to_update = @model.what_ids_should_I_update
ids_to_update.each do |id|
data = @api.fetch_me_data(id)
@model.persist_data(id, data)
end
end
end
# Retains 2's testability but without 2a's ugly internal methods, but pushing the use of
# instance variables down into internal methods feels bad.
module HitTheApi
def initialize(model, api)
@model = model
@api = api
end
def go
ids_to_update.each do |id|
fetch_and_persist(id)
end
end
private
def ids_to_update
@model.what_ids_should_I_update
end
def fetch_and_persist(id)
data = @api.fetch_me_data(id)
@model.persist_data(id, data)
end
end
# Can stub out the dependencies, but only if you stub methods on the class under test,
# which I want to avoid, and doesn't let you pass other dependencies in production
module HitTheApi
def self.go
ids_to_update = model_class.what_ids_should_I_update
ids_to_update.each do |id|
data = api_class.fetch_me_data(id)
model_class.persist_data(id, data)
end
end
def self.model_class
ModelClass
end
def self.api_class
ApiClass
end
end
# A DSL to define the 'setter injection' pattern
class HitTheApi
dependencies model: -> { ModelClass.new },
api: -> { ApiClass.new }
def go
ids_to_update.each do |id|
fetch_and_persist(id)
end
end
private
def ids_to_update
model.what_ids_should_I_update
end
def fetch_and_persist(id)
data = api.fetch_me_data(id)
model.persist_data(id, data)
end
end
class Module
def dependencies(dep_names_to_factory_lambda)
dep_names_to_factory_lambda.each do |dep_name, factory_lambda|
# a way to inject the dependency
attr_writer dep_name
# a getter that will use the injected dependency or use the default lambda
# to create one and memoize it
define_method(dep_name) do |*args|
instance_variable_get("@#{dep_name}") || instance_variable_set("@#{dep_name}", factory_lambda.call(*args))
end
end
end
end
# Dependencies could be injected as follows:
hitter = HitTheApi.new
hitter.model = TestModelClass.new
hitter.api = TestApiClass.new
hitter.go
# or, you could define something neater like
HitTheApi.with_dependencies(model: TestModelClass.new, api: TestApiClass.new).go
# with_dependencies would have to call new and set deps. Maybe that's kinda nuts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment