Decoupling Persistence from your Domain

Note: this post is actually a README I’ve written available on github (along with example source code).


In this README, I’ll show you two simple ways to isolate your business logic from your persistence concerns. The first example uses inheritance and a naming convention; the second, mixins.

However, I want you to walk through a refactoring to understand it. In general, I don’t recommend starting with a pattern unless you know it so well that refactoring into it would feel like a truly needless exercise.

Note: Checkout the readme_simple_example directory for a working example of the code in this README. Or, checkout the active_record_example directory for a larger example with both an in-memory persistence plugin and an ActiveRecord persistence plugin. (Caveat: I’ve only tested this code on MRI 1.9.3-p125).

Twitter

Let’s develop twitter. OK, not really, but let’s start with the following rspec spec:

require 'ostruct'
require_relative '../twitter_user'

describe Twitter::User do
  let(:user)          { Twitter::User.new }
  let(:tweet)         { OpenStruct.new    }
  let(:tweet_factory) { -> do tweet end   }

  before do
    user.tweet_factory = tweet_factory
  end

  describe "#tweet" do
    it "uses the tweet factory to generate a new tweet" do
      user.tweet("hi").should == tweet
    end
  end
end

Basically, we’ve written a spec that says a “user” should be able to tweet. Since that’s the single most essential feature of the Twitter application, I thought it would makes sense to start with that. If we can’t get this abstraction right, then we’re doomed.

The “tweet_factory” bit may seem a little odd. Essentially, we don’t want to tightly couple our User model to a Tweet model; instead, we’d simply like to inject a method for creating tweets into it at runtime. This makes it simpler to test, and makes our User model simpler to maintain. (If you’d like to learn more about this sort of dependency injection, I highly recommend purchasing Avdi’s ebook “Objects on Rails”).

Run the spec, watch it fail, then write some code until it passes. You might end up with something like this:

module Twitter
  class User
    attr_writer :tweet_factory

    def tweet(content)
      @tweet_factory.call
    end
  end
end

Great! There’s likely a couple other features of our tweet method that we’ll want to go ahead and add:

#...
it "sets the content of the tweet to the desired text" do
  user.tweet("hi").content.should == "hi"
end

it "associates the tweet with the user" do
  user.tweet("hi").user.should == user
end

Simple enough. Let’s get these tests passing:

module Twitter
  class User
    attr_writer :tweet_factory

    def tweet(content)
      @tweet_factory.call.tap do |t|
        t.content = content
        t.user    = self
      end
    end
  end
end

We’re done! We’ve just implemented twitter! Oh wait, what about persistence?

Saving users

Clearly, before we can launch our app into the real world, we’re going to need to persist our objects and retrieve them in various ways. Let’s start by simply adding specs for saving users, and for finding all users.

describe Twitter::User do
  #...

  describe ".all" do
    it "should default to empty" do
      Twitter::User.all.should be_empty
    end
  end

  describe "#save!" do
    it "should add the user to the list of all users" do
      user.save!
      Twitter::User.all.should include(user)
    end
  end
end

Seems simple enough. Let’s update our user class and make these specs pass:

module Twitter
  class User
    def self.all
      @users ||= []
    end

    attr_writer :tweet_factory

    def tweet(content)
      @tweet_factory.call.tap do |t|
        t.content = content
        t.user    = self
      end
    end

    def save!
      self.class.all << self
    end
  end
end

Great! There are, of course, flaws in our implementation. For starters, it’s not really even a persistence layer. These objects will die the second our script exits, never to return. Also, there’s a bug. Calling “#save!” multiple times will persist duplicate objects into “.all”. And if we build any more persistence specs, we will absolutely need a “User.truncate!” method that destroys all the users (we’ll want to run that before every test to ensure isolation between tests).

To make this article short, however, let’s ignore those problems and move on to some refactoring.

Refactoring out persistence

We have a problem. Originally, we started out our User spec by describing what a User actually does on Twitter (they tweet!). But now we’ve muddied up our domain with the concerns of our persistence layer.

Is that really such a big deal? Maybe not. I mean, if you’re cool with creating inflexible, tightly coupled systems, then carry on.

There’s all kinds of different ways to solve this problem. There’s the “Rails Way” - pretend it’s not a problem ;-). There’s the data mapper pattern (described notably in Martin Fowler’s “Patterns of Enterprise Application Architecture”). There’s the Active Record pattern (of which the powerful ActiveRecord library is an implementation of). There’s Avdi Grim’s “fig leaf” approach described in his “Objects on Rails” book. There’s Piotr Solnica’s compositional approach described here. And I’m sure many, many more that I’m not aware of.

All of those approaches have their merits. In terms of level of effort, the approach I’m about to show may be the simplest (well, except for the “Rails Way”, of course), though it doesn’t go as far with decoupling as the Data Mapper pattern or Solnica’s method.

Let’s start by refactoring out the persistence into a seperate class:

module Twitter
  module Persistence
    class User
      def self.all
        @users ||= []
      end

      def save!
        self.class.all << self
      end
    end
  end
end

module Twitter
  class User < Twitter::Persistence::User
    attr_writer :tweet_factory

    def tweet(content)
      @tweet_factory.call.tap do |t|
        t.content = content
        t.user    = self
      end
    end
  end
end

Now run the tests again. They should still all pass.

Next, move the Twitter::Persistence::User class into it’s own file. I called mine “in_memory_persistence.rb” - since what we’ve written is actually a simple in memory persistence solution.

We can also move the persistence specs into their own spec file:

require_relative '../in_memory_persistence'

User = Twitter::Persistence::User

describe User do
  describe ".all" do
    it "should default to empty" do
      User.all.should be_empty
    end
  end

  describe "#save!" do
    it "should add the user to the list of all users" do
      user = User.new
      user.save!
      User.all.should include(user)
    end
  end
end

Now we simply need an abstract persistence layer standin to unit test our business logic. Create another file called “abstract_persistence_layer.rb” and place the following code:

module Twitter
  module Persistence
    class User; end
  end
end

Now you can require this file at the top of your user spec to get those tests to pass again.

Wins

In a way, we’ve isolated our persistence layer from our business logic. We can test them completely independently of each other. When we look at our domain models in this application, they should scream “TWITTER”, not “DATABASE”.

You may have noticed, but we’ve also written an integration test suite for our persistence layer. If we wanted to replace our in-memory persistence layer with a file-system persistence layer, or a database persistence layer, we could test it by simply replacing the require_relative '../in_memory_persistence' in our persistence spec with require_relative '../file_persistence' or require_relative '../database_persistence'. That seems like a nice win.

In reality, you won’t likely be replacing your persistence layer a lot. However, a nice side effect of this sort of de-coupling is that it makes it possible to parallelize the work on our project. We could have one team develop the persistence layer while another develops the domain models. Other teams could work on various delivery mechanisms (e.g., a website, a REST api, an smartphone app, etc.) by requiring both the business logic layer and a persistence layer.

Note that we could have used mixins instead of inheritance to seperate our persistence layer. In fact, I’d prefer that. We could remove the inheritance from our domain model completely, and simply let the persistence layer inject modules into our domain models for supporting the persistence concerns:

#user.rb
module Twitter
  class User
    def tweet(content)
      #...
    end
  end
end


#in_memory_persistence.rb
require_relative 'user'
module Twitter
  module Persistence
    module User
      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def all
          #...
        end
      end

      def save!
        #...
      end
    end
  end
end

Twitter::User.send :include, Twitter::Persistence::User

Now we no longer need to define an abstract persistence layer standin for testing our user domain model. The only problem with this approach is that it would likely make working with ORMs like ActiveRecord tricky, since they assume that they’re bolted on to your models with inheritance.

Sunday, March 18, 2012 — 1 note   ()
  1. moonmaster9000 posted this