Giving some examples of how to implement services, as known from Domain Driven Design, in Ruby.

This article is quite old.Time flies when you're having fun. I've been writing for my blog for a long time. Stuff changes fast, especially in the Ruby world. That's why I've put this warning on old posts. The article might still be valid, though.

Building Blocks of DDD: Services

As promised, here is an example of how to use the idea of services from Domain Driven Design to help you design your code better. Let's start with some theoretical stuff, before we dive into some example code.

In Theory

Services are best defined by what they're not. They're not entities or value objects. To recap: entities are identifiable objects, that have real meaning in your domain. This might be a user or a post (if you're making a blog). Value objects are objects that are not defined by their identity, but by their value. An address of a person is usually a good example. It doesn't matter which address object it really is, as long as it contains the data it is supposed to have. Services are none of these. Services do something with the entities in your domain.

A good example of services are classes. By their very nature, classes are services for initializing objects. In Ruby this is idea is emphasized by the fact that classes are objects too. They have state and behavior, just as any other Ruby object. But their behavior is always aimed at something else, not themselves. Services tend to have no state, or very little at the most.

What's the advantage of thinking of classes as being services? Well, in my opinion it leads you to organize your code better. Class methods are only allowed to do something with creating instances of that class. If they are not doing that, they shouldn't be class methods.

It also answers the question: where should this behavior go? If it's not obvious, it's probably a service. The 'fat model, skinny controller'-principle has gotten some news lately. This principle was invented because developers (me included) were putting too much logic into the controllers. It made the controllers skinny and readable; but it had the side affect that everybody jammed the behavior into their models (entities), making them big and unwieldy. Services will help you create small and manageable classes.

In Practice

Time for an example. Suppose you have an admin interface which allows the user to find their entities in many different ways, filtering on attributes and ordering them around until he finds the entities he wants. It might be part of a advanced search box. This filtering business is an excellent candidate for a service.

There are many ways of making a filter service. I've made something similar to this recently. I'll start by making a basic filter, with nothing specific.

module Filter
  class Base

    class_inheritable_array :filters
    self.filters = []

    # Call filter to define which filters are available.
    # These will all be run in the order you specified
    def self.filter(*filters)
      self.filters.unshift(*filters)
    end

    # The params are the parameters you might have entered in your form.
    attr_reader :params
    def initialize(params)
      @params = params
    end

    # Get all filtered results. This is the public facing method that
    # you'd want to call when getting the results of the filter.
    def all
      apply_filters
      scope
    end

    private

    # As part of the contract, set the default scope by overriding this method.
    def scope
      raise NotImplementedError
    end

    # Run all the filters, specified in subclasses.
    def apply_filters
      filters.each { |filter| send(filter) }
    end

    # Probably every filter should be able to paginate the results.
    # remember to call pagination last, because will_paginate won't return
    # a real ActiveRecord::Relation object.
    def pagination
      @scope = scope.paginate(:page => page, :per_page => per_page)
    end

    def page
      params[:page] || 1
    end

    def per_page
      params[:per_page] || 20
    end

    # Similar to pagination, sorting is something common to all filters,
    # The default order is :id, because that will be available on every
    # model. You can override it easily however.
    def sort(default = :id)
      @scope = scope.order(params[:order] || default)
      @scope = scope.reverse_order if params[:direction] == 'desc'
    end

  end
end

Now, you can make filters for every model. This means creating a class that inherits from this base class and implementing the simple contract we put in place. I'll use the (t)rusty Post model as an example implementation.

module Filter
  class Posts < Base

    # Here I define the filter methods that will be used for posts.
    filter :published, :by_name, :sort, :pagination

    # The default scope for posts is a plain Post class without any scope added.
    # You can apply some permissions here, for instance.
    def scope
      @scope ||= ::Post
    end

    # Only show published posts, when the 'only_published' checkbox has been
    # checked in the form.
    def published
      @scope = scope.published if params[:only_published] == '1'
    end

    # Provide a simple name field to filter on the name of the post
    def by_name
      if params[:name].present?
        @scope = scope.where('name LIKE ?', "%#{params[:name]}%")
      end
    end

    # I want to sort by the published_at column by default
    def sort(default = :published_at)
      super(default)
    end

  end
end

To use this filter, call it from the controller:

class Backend::PostsController < ApplicationController

  respond_to :html, :json, :xml

  def index
    @posts = Filter::Posts.new(params[:filter]).all
    respond_with @posts
  end

end

As you can see, this places the logic of filtering in its proper place. It's not part of the model, since it's not part of initializing objects, or behavior of individual post objects. It's a separate service, doing something with something else.

These services are incredibly easy to make. Just think about the objects it's trying to handle. These are probably the arguments of your initializer. Store those methods with an instance variable. All methods you create do something with either these objects or call methods that do so.

Don't forget to run reek on your code to see if you have any Low Cohesion or Feature Envy warnings. If you get any, than that method probably doesn't belong here, or you've got you're initial parameters wrong.

Some bonus material

If you wish to simplify the interface even further, you can create a class method on the service to make it even easier (that's a service to create a service, so to speak).

module Filter
  class Base

    def self.all(*args)
      new(*args).all
    end

    # ... rest of the base class ...

  end
end

This reduces the connascence needed to use the service. Not really needed here, but it's a nice way of cleaning up your interface.

You might ask where do I put this into my Rails application. I make a folder app/services and (because I use RSpec) a folder named spec/services. If you use autotest, you need to tell it to pick up changes in these directories. You'll need to add the file .autotest to your application root folder, containing this bit of code to do the mapping:

Autotest.add_hook :initialize do |at|
  at.add_mapping(%r%^spec/(.*)_spec\.rb$%) { |filename, _| filename }
  at.add_mapping(%r%^app/(.*)\.rb$%) { |_, m| ["spec/#{m[1]}_spec.rb"] }
end
comments powered byDisqus