Extract a service object in any framework

… and check why 5600+ Rails engineers read also this

Extract a service object in any framework

Extracting a service object is a natural step in any kind of framework-dependent application. In this blog post, I’m showing you an example from Nanoc, a blogging framework.

The framework calls you

The difference between a library and a framework is that you call the library, while the framework calls you.

This slight difference may cause problems in applications being too dependent on the framework. Another potential problem is when your app lives inside the framework code.

The ideal situation seems to be when your code is separated from the framework code.

The “Extract a service object” refactoring is a way of dealing with the situation. In short, you want to separate your code from the framework code.

A typical example is a Rails controller action. An action is a typical framework building block. It’s responsible for several things, including all the HTTP-related features like rendering html/json or redirecting. Everything else is probably your application code and there are gains in extracting it into a new class.

Before

We’re using the nanoc tool for blogging in our Arkency blog. It serves us very well, so far. One place, where we extended it was a custom Nanoc command.

The command is called “create-post” and it’s just a convenience function to automate the file creation with a proper URL generation.

Here is the code:

require 'stringex'

usage       'create-post [options] title'
aliases     :create_post, :cp
summary     'create a new blog post'
description 'Creates new blog post with standard template.'

flag :h, :help,  'show help for this command' do |value, cmd|
  puts cmd.help
  exit 0
end

run do |opts, args, cmd|
  unless title = args.first
    puts cmd.help
    exit 0
  end

  date = Time.now
  path = "./content/posts/#{date.strftime('%Y-%m-%d')}-#{title.to_url}.md"
  template = <<TEMPLATE
# #{title}

TEMPLATE

  unless File.exist?(path)
    File.open(path, 'w') { |f| f.write(template) }
      puts "Created post: #{path}"
  else
    puts "Post already exists: #{path}"
    exit 1
  end

  puts "URL: http://blog.arkency.com/#{date.year}/#{date.month}/#{title.to_url}"
end

It was serving us well for over 3 years without any change. I’m extracting it to a service object, mostly as an example to show how it would work.

After

require 'stringex'

usage       'create-post [options] title'
aliases     :create_post, :cp
summary     'create a new blog post'
description 'Creates new blog post with standard template.'

flag :h, :help,  'show help for this command' do |value, cmd|
  puts cmd.help
  exit 0
end

run do |opts, args, cmd|
  unless title = args.first
    puts cmd.help
    exit 0
  end
  CreateNewPostFromTemplate.new(title, Time.now).call
end


class CreateNewPostFromTemplate

  def initialize(title, date)
    @title = title
    @date  = date
  end

  def call
    unless File.exist?(path)
      File.open(path, 'w') { |f| f.write(template(@title, @date)) }
      puts "Created post: #{path}"
    else
      puts "Post already exists: #{path}"
      exit 1
    end

    puts "URL: #{likely_url_on_production}"
  end

  private

  def path
    "./content/posts/#{@date.strftime('%Y-%m-%d')}-#{@title.to_url}.md"
  end

  def likely_url_on_production
    "http://blog.arkency.com/#{@date.year}/#{@date.month}/#{@title.to_url}"
  end

  def template(title, date)
    <<TEMPLATE
# #{title}

TEMPLATE
  end
end

I’ve created a new class and passed the arguments into it. While doing it, I’ve also extracted some small methods to hide implementation details. Thanks to that the main algorith is a bit more clear.

There’s more we could do at some point, like isolating from the file system. However, for this refactoring exercise, this effect is enough. It took me about 10 minutes to do this refactoring. I don’t need to further changes now, it’s OK to do it in small steps.

It’s worth to consider this techniqe whenever you use any framework, be it Rails, Sinatra, nanoc or anything else that calls you. Isolate early.

If you’re interested in such refactorings, you may consider looking at the book I wrote: Fearless Refactoring: Rails Controllers. This book consists of 3 parts:

  • the refactoring recipes,
  • the bigger examples,
  • the “theory” chapter

Thanks to that you not only learn how to apply a refactoring but also know what are the future building blocks. The building blocks include service objects, repositories, form objects and adapters.

You might also like