# ServiceObjects [][gem] [][travis] [][gemnasium] [][codeclimate] [][coveralls] [][license] [gem]: https://rubygems.org/gems/service_objects [travis]: https://travis-ci.org/nepalez/service_objects [gemnasium]: https://gemnasium.com/nepalez/service_objects [codeclimate]: https://codeclimate.com/github/nepalez/service_objects [coveralls]: https://coveralls.io/r/nepalez/service_objects [license]: file:LICENSE The module implements two design patterns: * The [Interactor pattern] to decouple business logics from both models and delivery mechanisms, such as [Rails]. * The [Observer pattern] to follow the [Tell, don't Ask] design princible. The pattern is implemented with the help of [wisper] gem by [Kris Leech]. [Rails]: http://rubyonrails.org/ [Interactor pattern]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ [Observer pattern]: http://reefpoints.dockyard.com/2013/08/20/design-patterns-observer-pattern.html [Tell, don't Ask]: http://martinfowler.com/bliki/TellDontAsk.html [wisper]: http://www.github.com/krisleech/wisper [Kris Leech]: http://www.github.com/krisleech The module API provides 3 classes: * `ServiceObjects::Base` - for service objects. * `ServiceObjects::Listener` - for decorating objects with methods called by service notificiations. * `ServiceObjects::Message` - for messages published by service objects. ## Installation Add this line to your application's Gemfile: ```ruby gem "service_objects" ``` And then execute: ``` bundle ``` Or install it yourself as: ``` gem install service_objects ``` ## Usage The basic usage of the services by example of Rails controller: ```ruby # lib/my_gem.rb require "service_objects" # app/services/add_foo.rb class AddFoo < ServiceObjects::Base # whitelists parameters and defines #params, #bar and #baz allows_params :bar, :baz # declares external dependencies depends_on :find_foo, default: FindFoo # calls the service, sorts out and reports its results def run run! rescue Invalid => err publish :error, err.messages else publish :created, @foo, messages ensure self end private # business logic lives here def run! # ... usage of the external service find_foo postponed add_foo end # rescues errors and re-raises them as Invalid with list of #messages def add_foo escape { @foo ||= Foo.create! bar: bar, baz: baz } end end ``` ```ruby # app/controllers/foos_controller.rb class FoosController < ApplicationController def create # Create the service object with necessary parameters. # All the business logic is encapsulated inside the service, # and the controller knows nothing what the service will do. service = AddFoo.new params.allow(:bar, :baz) # Subscribe the listener for the service's notifications service.subscribe listener, prefix: :on # Run the service service.run # If the service doesn't call any listener method, # then a listener provides some default actions listener.finalize end private # The listener decorates the controller with methods # to listen the service object's notifications # (see FoosListener#otherwise method below). def listener @listener ||= FoosListener.new self end # The class to work out service object notifications # The #render method is delegated to the controller class FoosListener < ServiceObjects::Listener # The method to be called when a service publishes # the 'added' notification. def on_added(foo, *, messages) render "created", locals: { foo: foo, messages: messages } end # The method to be called when a service publishes # the 'error' notification. def on_error(*, messages) render "error", locals: { messages: messages } end # The method is called by the #finalize in case no methods has been called # by the service object. # # This allows to focuse only on a subset of service notifications above. def otherwise render "no_result" end end end ``` The service can notify several listeners (controller itself, mailer etc.). ## Base The `ServiceObjects::Base` provides base class for services. ```ruby require "service_objects" class AddFoo < ServiceObjects::Base end ``` ### Parameters declaration Define allowed parameters for objects: ```ruby class AddFoo < ServiceObjects::Base allows_params :bar, :baz end ``` Parameters are whitelisted and assigned to `#params` hash (all keys are *symbolized*). Attributes are also defined as aliases for corresponding params, so that `#bar` and `#bar=` are equivalent to `#params[:bar]`, `#params[:bar]=`. **Note**: The service ignores parameters except for explicitly declared. The client can give relevant data to the service, and leave the latter to figure them out by itself. ### Validation The `ServiceObject::Base` includes [ActiveModel::Validations] with methods `.validates`, `.validate`, `#errors`, `#valid?` and `#invalid?`. Use them to add action context - specific validations. The method `#validate!` raises the `ServiceObject::Invalid` if validation fails. ```ruby class AddFoo < ServiceObjects::Base allows_params :bar, :baz validates :bar, presence: true validates :baz, numericality: { greater_than: 1 }, allow_nil: true # ... def run! # ... validate! # ... end end ``` **Note:** You aren't restricted in selecting time for validation. Prepare attributes (either "real" or [virtual]) and run `#validate!` when necessary. [ActiveModel::Validations]: http://api.rubyonrails.org/classes/ActiveModel/Validations.html [virtual]: http://railscasts.com/episodes/16-virtual-attributes?view=asciicast ### Dependencies declaration As a rule, services uses each other to keep the code DRY. For example, the service that *adds* a new foo (whatever it means) can use another service to *find* an existing foo. To made all that dependencies injectable via [setter injection], define them explicitly: ```ruby class AddFoo < ServiceObjects::Base # ... # Providing the FindFoo is available at the moment AddFoo being defined: depends_on :find_foo, default: FindFoo end ``` Default value can be either assigned or skipped. In the last case the [Null Object] will be assigned by default. The class method is public to postpone the default implementation until it is defined: ```ruby class AddFoo < ServiceObjects::Base # ... depends_on :find_foo end # later FindFoo = Class.new AddFoo.depends_on :find_foo, default: FindFoo ``` This provides the instance attribute `#find_foo`. You can inject the dependency to the via setter: ```ruby service = AddFoo.new bar: "bar", baz: "baz" service.find_foo = GetFoo ``` [setter injection]: http://brandonhilkert.com/blog/a-ruby-refactor-exploring-dependency-injection-options/ [Null Object]: https://robots.thoughtbot.com/rails-refactoring-example-introduce-null-object ### Run method It is expected the `run` method to provide all the necessary staff and notify listeners via `#publish` method. See [wisper] for details on `#publish` and `#subscribe` methods. ```ruby class AddFoo < ServiceObjects::Base # ... # The method contains the reporting logic only def run run! rescue Found publish :found, @foo, messages rescue Invalid => err publish :error, err.messages else publish :added, @foo, messages ensure self end # ... private Found = Class.new(RuntimeError) # the internal message # Business logic lives here def run! get_foo create_foo end def get_foo # ... finds and assigns @foo somehow fail Found if @foo end def add_foo # ... end end ``` There are some helper available: * `messages` - an array of collected service messages * `add_message` - adds the new message to the array * `escape` - rescues from `StandardErrors` and re-raises them as `ServiceObject::Invalid` with collection of `#messages`. **Note** Following [command-query separation] the `#run` method (being a command) returns `self`. [command-query separation]: http://en.wikipedia.org/wiki/Command-query_separation ### External services External services should be used in just the same way as in the controller example. ```ruby class AddFoo < ServiceObjects::Base depends_on :find_foo, default: FindFoo # ... def get_foo service = find_foo.new params service.subscribe listener, prefix: :on service.run # the method runs #otherwise callback in case # no other notificaton has been received listener.finalize end # decorates the service with methods to listen to external service def listener @listener ||= FindListener.new self end class FindListener < ServiceObjects::Listener def on_found(foo, *) __getobj__.foo = foo end def on_error(*, messages) __getobj__.messages = messages end def otherwise add_message "complain", "haven't been informed" end end end ``` Here the `#get_foo` runs the external service and listens to its notifications. Instead of the long syntax above, you can use a shortcut: ```ruby # ... def get_foo run_service find_foo.new(params), listener, prefix: :on end ``` ## Listener The listener is a [decorator] that: * defines callbacks to listen to service notifications. * delegates all undefined methods to the encapsulated object (available via [__getobj__] instance method). * defines the `#finalize` method to run `#otherwise` callback in case no other methods has been checked (via `#respond_to?` method). ```ruby class FooListener < ServiceObjects::Listener def on_success(*) "Notified on success" end def otherwise "Hasn't been notified" end end listener = FooListener.new listener.finalize # => "Hasn't been notified" listener.respond_to? :on_error # => false listener.finalize # => "Hasn't been notified" listener.respond_to? :on_success # => true listener.finalize # => nil ``` [decorator]: http://nithinbekal.com/posts/ruby-decorators/ [__getobj__]: http://ruby-doc.org//stdlib-2.1.0/libdoc/delegate/rdoc/SimpleDelegator.html#method-i-__getobj__ ## Message The `ServiceObjects::Base#messages` collects messages with text, type and optional priority: ```ruby message = ServiceObjects::Message.new priority: 0, type: "bar", text: "foo" message.priority # => 0.0 message.type # => "info" message.text # => "some text" message.to_h # => { type: "bar", type: "foo" } message.to_json # => "{\"type\":\"bar\",\"text\":"\foo\"}" ``` When a priority hasn't been defined explicitly, it is set to `-1.0` for errors, and to `0.0` otherwise. Messages are sorted by priority, type and text in a "natural" order. Use the `#add_message` helper to add a message to the collection: ```ruby service = ServiceObjects::Base.new service.send :add_message type: "info", text: "some text" service.send :messages # => [<Message type="info" text="some text" priority=0.0>] ``` When a `text:` value is a symbol, it is translated in the scope of current service class: ```yaml # config/locales/en.yml --- en: activemodel: messages: models: foo: excuse: # the type of the message not_informed: "I haven't been informed on the %{subject}" ``` ```ruby service.send :add_message type: "excuse", text: :not_informed, subject: "issue" # => [<Message text="I haven't been informed on the issue" ...>] ``` ## Compatibility Tested under MRI rubies >= 2.1 RSpec 3.0+ used for testing Collection of testing, debugging and code metrics is defined in the [hexx-suit](https://github.com/nepalez/hexx-suit) gem. To run tests use `rake test`, to run code metrics use `rake check`. All the metric settings are collected in the `config/metrics` folder. ## Contributing * Fork the project. * Read the [Styleguide](file:config/metrics/STYLEGUIDE). * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with Rakefile or version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) * Send me a pull request. Bonus points for topic branches. ## License See [MIT LICENSE][license]