README.md in shirinji-0.0.3 vs README.md in shirinji-0.0.4

- old
+ new

@@ -1,333 +1,335 @@ # Shirinji -Container manager for dependency injection in Ruby. +[![Gem Version](https://badge.fury.io/rb/shirinji.svg)]( +https://badge.fury.io/rb/shirinji +) +[![Build Status](https://travis-ci.org/fdutey/shirinji.svg?branch=master)]( +https://travis-ci.org/fdutey/shirinji +) +[![Maintainability]( +https://api.codeclimate.com/v1/badges/4b1c0010788d70581680/maintainability) +](https://codeclimate.com/github/fdutey/shirinji/maintainability) -## Principles +Dependencies Injection made clean and easy for Ruby. -Dependencies Injection is strongly connected with the IOC (inversion of controls) pattern. IOC is -often seen as a "Java thing" and tend to be rejected by Ruby community. +## Supported ruby versions -Yet, it's heavily used in javascript world and fit perfectly with prototyped language. +- 2.4.x +- 2.5.x +- 2.6.x -```javascript -function updateUI(evt) { /* ... */ } +## Principles -$.ajax('/action', { onSuccess: updateUI, ... }) -``` +Remove hard dependencies between your objects and delegate object tree building +to an unobtrusive framework with cool convention over configuration. -A simple script like that is very common in Javascript and nobody is shocked by that. Yet, it's -using the IOC pattern. The `$.ajax` method is delegating the action to perform when the request -is successful to something else, focusing only on handling the http communication part. +Shirinji relies on a mapping of beans and a resolver. When you resolve a bean, +it will return (by default) an instance of the class associated to the bean, +with all the bean dependencies resolved. -Dependencies injection is nothing more than the exact same principle but applied to objects instead -of functions. - -Let's follow an example step by step from "the rails way" to a proper way to understand it better. - ```ruby -class User < ActiveRecord::Base - after_create :publish_statistics, :send_confirmation_email +class FooService + attr_reader :bar_service - private - - def publish_statistics - StatisticsGateway.publish_event(:new_user, user.id) + def initialize(bar_service:) + @bar_service = bar_service end - def send_confirmation_email - UserMailer.confirm_email(user).deliver + def call(obj) + obj.foo = 123 + + bar_service.call(obj) end end -``` -This is called "the rails way" and everybody with a tiny bit of experience knows that this way -is not valid. Your model is gonna send statistics and emails each time it's saved, even when it's -not in the context of signing up a new user (confusion between sign up, a business level operation -and create, a persistency level operation). There are plenty of situation where you actually don't -want those operations to be performed (db seeding, imports, fixtures in tests ...) +map = Shirinji::Map.new do + bean(:foo_service, klass: 'FooService') + bean(:bar_service, klass: 'BarService') +end -That's where services pattern comes to the rescue. Let's do it in a very simple fashion and just -move everything "as it is" in a service. +resolver = Shirinji::Resolver.new(map) +resolver.resolve(:foo_service) +# => <#FooService @bar_service=<#BarService>> +``` + +Shirinji is unobtrusive. Basically, any of your objects can be used +outside of its context. + ```ruby -class SignUpUserService - def call(user) - user.signed_up_at = Time.now - user.save! - StatisticsGateway.publish_event(:new_user, user.id) - UserMailer.confirm_email(user).deliver - end -end +bar_service = BarService.new +foo_service = FooService.new(bar_service: bar_service) +# => <#FooService @bar_service=<#BarService>> -## test +# tests -RSpec.describe SignUpUserService do - let(:service) { described_class.new } +RSpec.describe FooService do + let(:bar_service) { double(call: nil) } + let(:service) { described_class.new(bar_service: bar_service) } describe '.call' do - let(:message_instance) { double(deliver: nil) } - let(:user) { FactoryGirl.build_stubbed(:user, id: 1) } - - before do - allow(StatisticsGateway).to receive(:publish_event) - allow(UserMailer).to receive(:confirm_email).and_return(message_instance) - end - - it 'saves user' do - expect(user).to receive(:save!) - - service.call(user) - end - - it 'sets signed up time' do - service.call(user) - expect(user.signed_up_at).to_not be_nil - # there are better ways to test that but we don't care here - end - - it 'publishes statistics' do - expect(StatisticsGateway).to receive(:publish_event).with(:new_user, 1) - - service.call(user) - end - - it 'notifies user for identity confirmation' do - expect(UserMailer).to receive(:confirm_email).with(user) - expect(message_instance).to receive(:deliver) - - service.call(user) - end - end + # ... + end end ``` -It's a bit better. Now when we want to write a user in DB, it's not acting as a signup regardless -the context. It will act as a sign up only when we call SignUpService. +## Constructor arguments -Yet, if we look a the tests for this service, we have to mock `StatisticsGateway` and -`UserMailer` in order for the test to run properly. It means that we need a very precise knowledge -of the implementation, and we need to mock global static objects which can be a very big problem -(for example, if the same class is called twice in very different contexts in the same method) +Shirinji relies on constructor to inject dependencies. It's considering that +objects that receive dependencies should be immutables and those dependencies +should not change during your program lifecycle. -Also, if we decide to switch our statistics solution, or if we decide to change the way we notify -users for identity confirmation, our test for signing up a user will have to change. -It shouldn't. The way we sign up users should not change according to the solution we chose to -send emails. +Shirinji doesn't accept anything else than named parameters. This way, +arguments order doesn't matter and it makes everybody's life easier. -This demonstrate that our object has too many responsibilities. If you want to write efficient, -fast, scalable, readable ... code, you should restrict your objects to one and only one responsbility. +## Name resolution -```ruby -class SignUpUserService - def call(user) - user.signed_up_at = Time.now - user.save! - - PublishUserStatisticsService.new.call(user) - SendUserEmailConfirmationService.new.call(user) - # implementation omitted for those services, you can figure it out - end -end -``` +By default, when you try to resolve a bean, Shirinji will look for a bean named +accordingly for each constructor parameter. -Now, our service has fewer responsibilities BUT, testing will be even harder because mocking `new` -method on both "sub services" will be even more dirty than before. -We can solve this problem very easily +It's possible to locally override this behaviour though by using `attr` macro. ```ruby -class SignUpUserService - def call(user) - user.signed_up_at = Time.now - user.save! - - publish_user_statistics_service.call(user) - send_user_email_confirmation_service.call(user) - end - - private +class FooService + attr_reader :bar_service - def publish_user_statistics_service - PublishUserStatisticsService.new + def initialize(my_service:) + @bar_service = my_service end - - def send_user_email_confirmation_service - SendUserEmailConfirmationService.new - end end -## test - -RSpec.describe SignUpUserService do - let(:publish_statistics_service) { double(call: nil) } - let(:send_email_confirmation_service) { double(call: nil) } - - let(:service) { described_class.new } - - before do - allow(service).to receive(:publish_user_statistics_service).and_return(publish_statistics_service) - allow(service).to receive(:send_user_email_confirmation_service).and_return(send_email_confirmation_service) +map = Shirinji::Map.new do + bean(:foo_service, klass: 'FooService') do + attr :my_service, ref: :bar_service end - # ... + bean(:bar_service, klass: 'BarService') end + +resolver = Shirinji::Resolver.new(map) + +resolver.resolve(:foo_service) +# => <#FooService @bar_service=<#BarService>> ``` -Our tests are now much easier to write. They're also much faster because our test is very specialized -and focus only on the service itself. -But if you think about it, this service still has too many responsibilities. It still carrying the -responsibility of choosing which service will execute the "sub tasks" and more important, it's in -charge of creating those services instances. +## Caching and singletons -Instead of having strong dependencies to other services, we can make them "weak" and increase our -code flexibility if we want to reuse it in another project. +Shirinji provides a caching mecanism to help you improve memory consumption. +This cache is safe as long as your beans remains immutable (they should always +be). -```ruby -class SignUpUserService - attr_reader :publish_user_statistics_service, - :send_user_email_confirmation_service - - def initialize( - publish_user_statistics_service:, - send_user_email_confirmation_service:, - ) - @publish_user_statistics_service = publish_user_statistics_service - @send_user_email_confirmation_service = send_user_email_confirmation_service - end +The consequence is that any cached instance is actually a singleton. Singleton +is no more a property of your class but of it's environment, improving the +reusability of your code. - def call(user) - user.signed_up_at = Time.now - user.save! - - publish_user_statistics_service.call(user) - send_user_email_confirmation_service.call(user) - end +Singleton is the default access mode for a bean. + +```ruby +map = Shirinji::Map.new do + bean(:bar_service, klass: 'BarService', access: :instance) + bean(:foo_service, klass: 'FooService', access: :singleton) + # same as bean(:foo_service, klass: 'FooService') end + +resolver = Shirinji::Resolver.new(map) + +resolver.resolve(:foo).object_id #=> 1 +resolver.resolve(:foo).object_id #=> 1 + +resolver.resolve(:bar).object_id #=> 2 +resolver.resolve(:bar).object_id #=> 3 ``` -Now our service is completely agnostic about which solution is used to perform the "sub tasks". -It's the responsibility of it's environment to provide this information. +Cache can be reset with the simple command `resolver.reset_cache`, which can be +useful when using a development console like rails console ([shirinji-rails]( +https://github.com/fdutey/shirinji-rails) is attaching cache reset to `reload!` +command). -But in a real world example, building such a tree is a complete nightmare and impossible to -maintain. It's where Shiringji comes to the rescue. +## Other type of beans -## Usage +Dependencies injection doesn't apply only to classes. You can actually inject +anything and therefore, Shirinji allows you to declare anything as a dependency. +To achieve that, use the key `value` instead of `class`. ```ruby +module MyApp + def self.config + @config + end + + def self.load! + @config = OpenStruct.new + end +end + +class FooService + attr_reader :config + + def initialize(config:) + @config = config + end +end + +MyApp.load! + map = Shirinji::Map.new do - bean(:sign_up_user_service, klass: "SignUpUserService") - bean(:publish_user_statistics_service, klass: "PublishUserStatisticsService") - bean(:send_user_email_confirmation_service, klass: "SendUserEmailConfirmationService") + bean(:config, value: Proc.new { MyApp.config }) + + bean(:foo_service, klass: 'FooService') end resolver = Shirinji::Resolver.new(map) -resolver.resolve(:sign_up_user_service) -#=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...> +resolver.resolve(:foo_service) +#=> <#FooService @config=<#OpenStruct ...> ...> ``` -In this example, because `SingUpUserService` constructor parameters match beans with the same name, -Shirinji will automatically resolve them. +A value can be anything. `Proc` will be lazily evaluated. It also obeys the +cache mechanism described before. -In a case where a parameter name match no bean, it has to be mapped explicitly. +## Skip construction mechanism +In some cases, you need a dependency to be injected as a class and not an +instance. In such case, you could use value beans, returning the class itself, +but you would lose the benefit of scopes (see below). +Instead, Shirinji provides a parameter to skip the object construction. + +A real life example is a Job where `deliver_now` and `deliver_later` are +class methods. + ```ruby map = Shirinji::Map.new do - bean(:sign_up_user_service, klass: "SignUpUserService") do - attr :publish_user_statistics_service, ref: :user_publish_statistics_service - end - - # note the name is different - bean(:user_publish_statistics_service, klass: "PublishUserStatisticsService") - bean(:send_user_email_confirmation_service, klass: "SendUserEmailConfirmationService") + bean(:foo_job, klass: 'FooJob', construct: false) end resolver = Shirinji::Resolver.new(map) -resolver.resolve(:sign_up_user_service) -#=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...> +resolver.resolve(:foo_job) #=> FooJob ``` -Shirinji provides scopes to help you organize your dependencies +## Scopes +Building complex objects mapping leads to lot of repetition. That's why Shirinji +also provides a scope mechanism to help you dry your code. + ```ruby map = Shirinji::Map.new do scope module: :Services, suffix: :service, klass_suffix: :Service do + bean(:foo, klass: 'Foo') + # same as bean(:foo_service, klass: 'Services::FooService') + scope module: :User, prefix: :user do - bean(:signup, klass: 'Signup') - end + bean(:bar, klass: 'Bar') + # same as bean(:user_bar_service, klass: 'Services::User::BarService') + end end - - # is the same as - bean(:user_signup_service, klass: 'Services::User::SignupService') end ``` -If you need a dependency to return a class instead of an instance, you can disable -the bean construction +Scopes also come with an `auto_klass` attribute to save even more time for +common cases ```ruby map = Shirinji::Map.new do - bean(:foo, klass: 'Foo', construct: false) + scope module: :Services, + suffix: :service, + klass_suffix: :Service, + auto_klass: true do + bean(:foo) + # same as bean(:foo_service, klass: 'Services::FooService') + end end - -resolver.resolve(:foo) #=> Foo ``` -Shirinji also provide a caching mecanism to achieve singleton pattern without having to implement -the pattern in your classes. It means the same class can be used as a singleton AND a regular class -at the same time without any code change. +Scopes also provides an `auto_prefix` option -Singleton is the default access mode for a bean. +```ruby +map = Shirinji::Map.new do + scope module: :Services, + suffix: :service, + klass_suffix: :Service, + auto_klass: true do + + # Do not use auto prefix on root scope or every bean will be prefixed + # with `services_` + scope auto_prefix: true do + bean(:foo) + # same as bean(:foo_service, klass: 'Services::FooService') + + scope module: :User do + # same as scope module: :User, prefix: :user + + bean(:bar) + # same as bean(:user_bar_service, klass: 'Services::User::BarService') + end + end + end +end +``` +Finally, for mailers / jobs ..., Scopes allow you to specify a global value +for `construct` + ```ruby map = Shirinji::Map.new do - bean(:foo, klass: 'Foo', access: :singleton) # foo is singleton - bean(:bar, klass: 'Bar', access: :instance) # bar is not + scope module: :Jobs, + suffix: :job, + klass_suffix: :Job, + auto_klass: true, + construct: false do + bean(:foo) + # bean(:foo_job, klass: 'Jobs::FooJob', construct: false) + end end +``` -resolver = Shirinji::Resolver.new(map) +Scopes do not carry property `access` -resolver.resolve(:foo).object_id #=> 1 -resolver.resolve(:foo).object_id #=> 1 +## Code splitting -resolver.resolve(:bar).object_id #=> 2 -resolver.resolve(:bar).object_id #=> 3 -``` +When a project grows, dependencies grows too. Keeping them into one single file +leads to headaches. One possible solution to keep everything under control is +to split your dependencies into many files. -You can also create beans that contain single values. It will help you to avoid referencing global -variables in your code. +To include a "sub-map" into another one, you can use `include_map` method. ```ruby -map = Shirinji::Map.new do - bean(:config, value: Proc.new { Application.config }) - bean(:foo, klass: 'Foo') +# dependencies/services.rb +Shirinji::Map.new do + bean(:foo_service, klass: 'FooService') end -resolver = Shirinji::Resolver.new(map) +# dependencies/queries.rb +Shirinji::Map.new do + bean(:foo_query, klass: 'FooQuery') +end -class Foo - attr_reader :config +# dependencies.rb + +root = Pathname.new(File.expand_path('../dependencies', __FILE__)) + +Shirinji::Map.new do + bean(:config, value: -> { MyApp.config }) - def initialize(config:) - @config = config - end + # paths must be absolute + include_map(root.join('queries.rb')) + include_map(root.join('services.rb')) end - -resolver.resolve(:foo) -#=> <#Foo @config=<#OpenStruct ...> ...> ``` -Values can be anything. A `Proc` will be lazily evaluated. They also obey the singleton / instance -strategy. - ## Notes -- It is absolutely mandatory for your beans to be stateless to use the singleton mode. If they're - not, you will probably run into trouble as your objects behavior will depend on their history, leading - to unpredictable effects. -- Shirinji only works with named arguments. It will raise errors if you try to use it with "standard" - method arguments. +- It is absolutely mandatory for your beans to be stateless to use the singleton + mode. If they're not, you will probably run into trouble as your objects + behavior will depend on their history, leading to unpredictable effects. +- Shirinji only works with named arguments. It will raise `ArgumentError` if you + try to use it with "standard" method arguments. + +## TODOS +- solve absolute paths problems for `include_map` (`instance_eval` is a problem) + ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/fdutey/shirinji. +Bug reports and pull requests are welcome on GitHub at +https://github.com/fdutey/shirinji.