Yet Another Dependencies Manager
Simple implementation of dependency container.
Imagine, you have two coupled classes. For example BillingManager and EmailNotifier.
BillingManager uses notifier to notify user about each money transaction.
Typically in Ruby you'll define methods anywhere (e. g. in controllers) you'll need BillingManager like that:
def billing_manager
BillingManager.new(EmailNotifier.new)
endBut hey! What shall happens if you'll need to use another notifier, for example SmsNotifier?
Or add extra parameters to BillingManager's constructor? Or completely change notifiers' constructor?
That's it! You'll have to fix all BillingManager.new calls.
And that how it could be solved with Yadm:
# on application initialize
manager = Yadm::Manager.new
manager.register(:notifier){ EmailNotifier.new }
manager.register(:billing_manager){ |m| BillingManager.new(m.resolve(:notifier)) } Now your controllers should not know about billing manager concrete class or it's dependencies.
def billing_manager
manager.resolve(:billing_manager)
endAnd all you have to do if you need change something is to fix initialization:
# on application initialize
manager = Yadm::Manager.new
manager.register(:notifier) do |m|
SmsNotifier.new(m.resolve(:sms_notifier_dependency))
end
manager.register(:billing_manager) do |m|
BillingManager.new(m.resolve(:notifier), m.resolve(:other_billing_dependency))
endThis is just my experiment, a part of my hobby. It wasn't ever tested in real projects and possibly contains tons of bugs.
First of all you need to initialize object.
manager = Yadm::Manager.newNow manager is ready for your dependencies.
You can use manager to store object. It possible to store whatever: String, Lambda, Integer, Class or any other kind of object.
manager.register_object(:str, "my string")
manager.register_object(:lambda, ->{ "something" })
manager.register_object(:int, 42)
manager.register_object(:class, Float)
manager.register_object(:arr, [])Now you can resolve any previously stored object.
manager.resolve(:str) #=> "my string"
manager.resolve(:lambda).call #=> "something"
manager.resolve(:int) #=> 42
manager.resolve(:class) #=> Float
manager.resolve(:arr) #=> []Warning Actually, objects could be mutable.
manager.resolve(:arr).push(100500)
manager.resolve(:arr) #=> [100500]But please, make sure, that you really need this kind of global objects. It could thread unsafe.
Method #register provides more powerful and flexible way to register objects.
Unlike #register_object it don't takes initialized object, but block, which initialize it.
In block you can use previously registered objects as dependencies. And it is no matter if dependency is already registered or not. Block will be used only when you'll need object.
manager.register(:foo) { Foo.new }
manager.register(:bar) do |m|
foo = m.resolve(:foo) # already registered
baz = m.resolve(:baz) # will be registered later
Bar.new(foo, baz)
end
manager.register_object(:baz, "Baz")
manager.resolve(:bar) #=> #<Bar @foo=#<Foo> @baz="Baz">Blocks could be useful for dynamic code reload. E.g. Rails development mode.
Objects, registered with #register normally will be initialized every time #resolve is called.
If you need to reduce count of memory allocations and you don't need to deal with code hot reload (e.g. at production) #prepare! could be useful for you.
It initializes and saves all registered objects to cache. So #resolve will return same object for same key every time.
manager.register(:c_time) { Time.now }
manager.resolve(:c_time) #=> 2018-01-01 20:11:47 +0300
manager.resolve(:c_time) #=> 2018-01-01 20:11:48 +0300
manager.resolve(:c_time) #=> 2018-01-01 20:11:49 +0300
# etc
manager.prepare!
manager.resolve(:c_time) #=> 2018-01-01 20:12:27 +0300
manager.resolve(:c_time) #=> 2018-01-01 20:12:27 +0300
manager.resolve(:c_time) #=> 2018-01-01 20:12:27 +0300 Warning 1
Like object, registered with #register_object, prepare object could be mutable.
manager.register(:prepared_array) { [] }
manager.prepare!
manager.resolve(:prepared_array).push(42)
manager.resolve(:prepared_array) #=> [42]So this is still unsafe to use it this way.
Warning 2
#prepare will cache only already registered objects.
If you have registered object after manager was prepared - that object will be initialized every time #resolve is called.
manager.register(:foo) { Foo.new }
manager.prepare!
manager.register(:bar) { Bar.new }
manager.resolve(:foo) # same instance every time
manager.resolve(:bar) # new instance every time
manager.prepare! #recache all object
manager.resolve(:foo) # same instance every time, but not the same as before
manager.resolve(:bar) # same instance every timeFor people who like programming configs. Simply call Yadm.load_yaml and specify .yml file as parameter.
At root it should have array. Each element is object to register. Order of objects isn't important.
Each one should have name and class.
name - identifier (key) to register object
class - string representation of constant to use (e.g. "Array" or "Foo" or "::Bar::Baz")
dependencies array should include identifiers for objects that will be used as dependencies. Order is important.
Imagine that conf.yml contains:
- name: "foo"
class: "Foo"
- name: "bar"
class: "Bar"
dependencies:
- "foo"
- "baz"
- name: "baz"
class: "Baz"manager = Yadm.load_yaml("conf.yml")
manager.resolve(:bar) #=> #<Bar @foo=#<Foo> @baz=#<Baz>>