Service applications with Organ
Ever since I moved out of the MVC mindset for web I've been more inclined in having services as a layer on software applications, which only manages some input and does something about it.
It is usually a very common thing to have to coerce (format) the input given by
the user, validate it and then perform the actions the caller requested, and
possibly return some output. This is what these services are for, services are
doers, they make changes to the domain models with the given data. That is why
I always use a verb on the services name, like CreateUser
, GetArtist
,
StoreFile
, DeleteAlbum
, NotifyAdmin
, etc.
Once these services validate the data, they can start calling external modules from the domain to perform the actions. This could be an SQL DB client library, an external API for which you have a client wrapper or simply some computation you might want to do with the given input. All of these external services (DB, APIs, FS, etc), are wrapped in their own clients with their own abstractions and interfaces.
It is very important to make this services layer receive and return data structures, in the communication between the caller and the service, no objects from the domain should be present. By data structures, in Ruby, I mean hashes, arrays containing integers, strings, floats, etc. These data structures don't have any behavior, they are data abstractions, not behavior abstractions.
This is pretty much how I use Organ for web applications, I instantiate them with the request parameters, coerce the values, validate them and perform the actions or return some data. For example:
# services/create_user.rb require "organ" require "models/user" module Services class CreateUser < Organ::Form PASSWORD_FORMAT = %r{stuff..} attr_reader :user attribute(:email, :type => :string) attribute(:password, :type => :string) def validate validate_email_format(:email) validate_format(:password, PASSWORD_FORMAT) end def perform @user = MyApp::User.create(attributes).to_hash end end end # routes/user.rb on post do service = Services::CreateUser.new(params[:user]) if service.valid? service.perform res.redirect("...") else render("user/form", :user_form => service) end end
I usually also add some extensions like having them respond to worker jobs using Ost so I can have Worker services as well.
# services/extensions/worker.rb module Services module Extensions module Worker def self.queue_job(attributes) queue << JSON.generate(attributes) end def self.stop queue.stop end def self.watch_queue queue.each do |json_str| attributes = JSON.parse(json_str) new(attributes).perform end end private def self.queue Ost[self.name] end end end end # workers/email_notifier.rb module Workers class NotifyUser < Organ::Form include Extensions::Worker attribute(:email, :type => :string) attribute(:message, :type => :string) def perform # send message to email... end end end
I would also use extensions on services that return data, possibly from a cache.
# services/extensions/presenter.rb module Services module Extensions module Presenter def data @data ||= get_cache(cache_key) { perform } end def get_cache(key, &block) Cache[key] || block.call end end end end # services/get_albums.rb module Services class GetAlbums < Organ::Form include Extensions::Presenters attribute(:user_id, :type => :integer) def perform music_client.get_user_albums(user_id) end def cache_key "user:#{user_id}:albums" end end end # routes/albums.rb on get, "albums" do service = Services::GetAlbums.new(session[:user_id]) render("albums/list", :albums => service.data) end
This are just a few of the examples of how I used Organ. Organ was inspired by soveran/scrivener and by many posts on how to build a service based application.
Any comments or improvements for Organ or this blogpost are more than welcome.