Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Injectable Persistence Layers – a Refactoring Step by Step

This week I worked on a new web interface for our open source project License Finder.

The license_finder gem persists dependency information out to a YAML file; however, we wanted to persist these same
dependency objects to a SQL database for the website.

Step 1: Persistence base class

In order to accomplish this, I had to perform a series of refactorings that would make it possible to swap out YAML persistence
with an ActiveRecord persistence layer. The LicenseFinder::Dependency class had never been setup make persistence injectable,
so the first step was moving all persistence-related functionality out into a seperate base class, and giving it an ActiveRecord-style API:

module LicenseFinder
  module Persistence
    class Dependency
      class Database
        #... YAML 'database' implementation details
      end

      attr_accessor *LicenseFinder::DEPENDENCY_ATTRIBUTES

      class << self
        def find_by_name(name)
          attributes = database.find { |a| a['name'] == name }
          new(attributes) if attributes
        end

        def delete_all
          database.delete_all
        end

        def all
          database.all.map { |attributes| new(attributes) }
        end

        def unapproved
          all.select {|d| d.approved == false }
        end

        def update(attributes)
          database.update attributes
        end

        def destroy_by_name(name)
          database.destroy_by_name name
        end

        private
        def database
          @database ||= Database.new
        end
      end

      def initialize(attributes = {})
        update_attributes_without_saving attributes
      end

      def config
        LicenseFinder.config
      end

      def update_attributes new_values
        update_attributes_without_saving(new_values)
        save
      end

      def approved?
        !!approved
      end

      def save
        self.class.update(attributes)
      end

      def destroy
        self.class.destroy_by_name(name)
      end

      def attributes
        attributes = {}

        LicenseFinder::DEPENDENCY_ATTRIBUTES.each do |attrib|
          attributes[attrib] = send attrib
        end

        attributes
      end

      private
      def update_attributes_without_saving(new_values)
        new_values.each do |key, value|
          send("#{key}=", value)
        end
      end
    end
  end
end

With all of the persistence-related functionality in this base class, I could now update the LicenseFinder::Dependency class to inherit from this:

module LicenseFinder
  class Dependency < LicenseFinder::Persistence::Dependency
    #...
  end
end

I also created a shared example for describing how persistence should work (regardless of the underlying persistence implementation):

shared_examples_for "a persistable dependency" do
  let(:klass) { described_class }

  let(:attributes) do
    {
      'name' => "spec_name",
      'version' => "2.1.3",
      'license' => "GPLv2",
      'approved' => false,
      'notes' => 'some notes',
      'homepage' => 'homepage',
      'license_files' => ['/Users/pivotal/foo/lic1', '/Users/pivotal/bar/lic2'],
      'readme_files' => ['/Users/pivotal/foo/Readme1', '/Users/pivotal/bar/Readme2'],
      'source' => "bundle",
      'bundler_groups' => ["test"]
    }
  end

  before do
    klass.delete_all
  end

  describe '.new' do
    subject { klass.new(attributes) }

    context "with known attributes" do
      it "should set the all of the attributes on the instance" do
        attributes.each do |key, value|
          if key != "approved"
            subject.send("#{key}").should equal(value), "expected #{value.inspect} for #{key}, got #{subject.send("#{key}").inspect}"
          else
            subject.approved?.should == value
          end
        end
      end
    end

    context "with unknown attributes" do
      before do
        attributes['foo'] = 'bar'
      end

      it "should raise an exception" do
        expect { subject }.to raise_exception(NoMethodError)
      end
    end
  end

  describe '.unapproved' do
    it "should return all unapproved dependencies" do
      klass.new(name: "unapproved dependency", approved: false).save
      klass.new(name: "approved dependency", approved: true).save

      unapproved = klass.unapproved
      unapproved.count.should == 1
      unapproved.collect(&:approved?).any?.should be_false
    end
  end

  describe '.find_by_name' do
    subject { klass.find_by_name gem_name }
    let(:gem_name) { "foo" }

    context "when a gem with the provided name exists" do
      before do
        klass.new(
          'name' => gem_name,
          'version' => '0.0.1'
        ).save
      end

      its(:name) { should == gem_name }
      its(:version) { should == '0.0.1' }
    end

    context "when no gem with the provided name exists" do
      it { should == nil }
    end
  end

  describe "#config" do
    it 'should respond to it' do
      klass.new.should respond_to(:config)
    end
  end

  describe '#attributes' do
    it "should return a hash containing the values of all the accessible properties" do
      dep = klass.new(attributes)
      attributes = dep.attributes
      LicenseFinder::DEPENDENCY_ATTRIBUTES.each do |name|
        attributes[name].should == dep.send(name)
      end
    end
  end

  describe '#save' do
    it "should persist all of the dependency's attributes" do
      dep = klass.new(attributes)
      dep.save

      saved_dep = klass.find_by_name(dep.name)

      attributes.each do |key, value|
        if key != "approved"
          saved_dep.send("#{key}").should eql(value), "expected #{value.inspect} for #{key}, got #{saved_dep.send("#{key}").inspect}"
        else
          saved_dep.approved?.should == value
        end
      end
    end
  end

  describe "#update_attributes" do
    it "should update the provided attributes with the provided values" do
      gem = klass.new(attributes)
      updated_attributes = {"version" => "new_version", "license" => "updated_license"}
      gem.update_attributes(updated_attributes)

      saved_gem = klass.find_by_name(gem.name)
      saved_gem.version.should == "new_version"
      saved_gem.license.should == "updated_license"
    end
  end

  describe "#destroy" do
    it "should remove itself from the database" do
      foo_dep = klass.new(name: "foo")
      bar_dep = klass.new(name: "bar")
      foo_dep.save
      bar_dep.save

      expect { foo_dep.destroy }.to change { klass.all.count }.by -1

      klass.all.count.should == 1
      klass.all.first.name.should == "bar"
    end
  end
end

Step 2 – Make persistence autoloadable

Next, I wanted to make persistence autoloadable in the gem (so that other persistence solutions could simply create their own
LicenseFinder::Persistence::Dependency implementation before doing a require "license_finder":

module LicenseFinder
  module Persistence
    autoload :Dependency, 'license_finder/persistence/yaml/dependency'
    autoload :Configuration, 'license_finder/persistence/yaml/configuration'
  end
end

Step 3 – Create new persistence implementation

Now, creating an ActiveRecord persistence implementation was as simple as:

module LicenseFinder
  module Persistence
    class Dependency < ActiveRecord::Base
      serialize :license_files
      serialize :readme_files
      serialize :bundler_groups
      serialize :children
      serialize :parents

      belongs_to :config

      scope :unapproved, where(approved: false)
    end
  end
end

require "license_finder"

And the test for this persistence implementation:

require "spec_helper"
require_relative "path/to/LicenseFinder/spec/support/shared_examples/persistence/dependency.rb"

describe LicenseFinder::Persistence::Dependency do
  it_behaves_like "a persistable dependency"
end

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *