My aproach for doing “DDD on Ruby” – Introduction and Part I

Introduction

We all know that in Domain-Driven Design it is good to express our domain with simplicity and “pureness” and for that we usually go for a POxO (Plain Old {fill_with_your_preffered_language} Object) approach. The problem is that the ORM solutions I’ve found for Ruby make us either inherit from some base class or do the mappings directly into our classes. By no means am I saying that they aren’t good, but we are talking about DDD and the idea of having a pure domain layer without distractions is something we should embrace.

My first thought was to write about a full blown example, comparing side by side a common C# / Java implementations to my Ruby implementation but then I realized it would probably have some unnecessary and “boring” information. It’s better to focus on writing about my approach and have some comments here and there to explain how things would be done in other platforms. What I’m going to show here is something that is really common in other languages but not in Ruby I believe, all classes from my domain will be implemented as POROs – Plain Old Ruby Objects, totaly decoupled from any tool that we might use.

There was a talk by Eric Evans called “What I’ve learned about DDD since the book” that has been commented out here and here (slides here). Even though the building blocks of Model-Driven Design mentioned in the book (and the other one mentioned on the talk) are not essential, they are important and will be the subject of this series.

So, to illustrate my ideas I’ll use the common Customer/Order/Product for my examples with some requirements from Jimmy Nilsson – Applying Domain-Driven Design and Patterns:

  • List the orders when looking at a specific customer
  • Orders have an acceptance status that is changed by the user
  • The total value of an order must be lower or equal than one million US$
  • A Customer has a credit limit and cannot owe us more than a specified amount of money

Part I – Customer and Products

If you are not familiar with DDD and its building blocks I suggest that have a look at this glossary of DDD terms.

This is our really simple model for this part:

model

Entities

There’s no big deal here so I’ll just provide some code:

class Customer
  attr_reader :id
  attr_accessor :name
  attr_accessor :credit_limit

  def initialize(name, credit_limit)
    @name = name
    @credit_limit = credit_limit
  end
end

class Product
  attr_reader :id
  attr_accessor :price
  attr_accessor :description

  def initialize(price, description)
    @price = price
    @description = description
  end
end

Repositories

Statically typed languages like Java and C# would have an interface in order to decouple our domain from infrastructure but Ruby doesn’t have interfaces. But still… I believe that we need some kind of contract for the repositories and instead of having a class with empty methods I’ll create in-memory repositories as the pattern definition:

class CustomerRepository
  def self.store(customer)
    customer.instance_variable_set(:@id, next_id) if customer.id.nil?
    customers[customer.id] = customer
  end
  def self.find(id)
    customers[id]
  end
  def self.all
    customers.values
  end
  def self.delete(customer)
    customers.delete(customer.id)
  end  

  private  

  def self.customers
    if !defined?(@@customers)
      @@customers = {}
    else
      @@customers
    end
  end
  def self.next_id
    if !defined?(@@next_id)
      @@next_id = 1
    else
      @@next_id += 1
    end
  end
end

class ProductRepository
  # ... similar code ...
end

Maybe this code could be factored out somewhere but let’s just leave as it is for now.

Right now you might be thinking “This guy is crazy, static methods are evil!!”. Well, I think that it’s not true when it comes to Ruby class methods. Just below I will show you how we can easily add persistence to our domain.

Even if we want to set up a IoC container I don’t think it would be a problem since Ruby classes are objects as well. For example these repositories could be easily injected into a Rails controller instance variable.

Adding persistence

If we use (N)Hibernate, we would probably have the mappings file into a different package / assembly, but in Ruby I think it would be enough to just have persistence related code in another file that redefines the classes.

Mappings

class Customer
  include Clipper::Model

  orm.map(self, "customers") do |customers|
    customers.key(customers.field("id", Clipper::Types::Serial))
    customers.field("name", Clipper::Types::String.new(200))
    customers.field("credit_limit", Clipper::Types::Float(8, 2))
  end
end

class Product
  include Clipper::Model

  orm.map(self, "products") do |products|
    products.key(products.field("id", Clipper::Types::Serial))
    products.field("description", Clipper::Types::String.new(200))
    products.field("price", Clipper::Types::Float(8, 2))
  end
end

Repositories

module ClipperRepository
  def self.included(klass)
    klass.instance_eval do
      include Singleton
      include Clipper::Session::Helper

      def self.orm
        instance.orm
      end
    end
  end
end

class CustomerRepository
  include ClipperRepository

  def self.store(customer)
    orm.save(customer)
  end
  def self.delete(customer)
    orm.delete(customer)
  end
  def self.find(id)
    orm.get(Customer, id)
  end
  def self.all
    orm.all(Customer)
  end
end

class ProductRepository
  # ... similar code  ...
end

Here I’ve used Clipper but for these examples we could probably rewrite it to use DataMapper as well. Unfortunately, with this approach both DataMapper and Clipper will redefine our already defined accessors and that would be a problem if we have some business logic there. For this example this wouldn’t be a problem but I’ve checked with one of Clipper developers and they are willing to have this feature implemented in the library, I’ll have to check if DataMapper folks have something like this in mind as well. I’ve already hacked Clipper in order to have something like Hibernate property access config that defines the strategy for accessing the property value but it still lacks the feature for associations.

Testing

With this approach I believe testing becomes more simple. I’ve created tests for the pure domain and just by including a different file I can test everything hooked up with infrastructure without rewriting the tests. To make things easier, I created two Rake tasks for testing: ‘rake test:domain’ and ‘rake test:infrastructure’. I think they look a bit ugly at the moment but here we go:

namespace :test do
  task :enviroment_domain do |t|
    ENV['DDD_EXAMPLE_HELPER'] = Pathname(__FILE__).dirname + 'tests' + 'domain_helper'
  end

  task :enviroment_infrastructure do |t|
    ENV['DDD_EXAMPLE_HELPER'] = Pathname(__FILE__).dirname + 'tests' + 'infrastructure_helper'
  end

  Rake::TestTask.new(:domain => :enviroment_domain) do |t|
    t.libs << "tests"
    t.test_files = FileList["tests/domain/**/*_test.rb"]
    t.verbose = true
  end

  Rake::TestTask.new(:infrastructure => :enviroment_infrastructure) do |t|
    t.libs << "tests"
    t.test_files = FileList["tests/domain/**/*_test.rb"] + FileList["tests/infrastructure/**/*_test.rb"]
    t.verbose = true
  end
end

Test Case

require ENV['DDD_EXAMPLE_HELPER']

class CustomerRepositoryTest < Test::Unit::TestCase
  # ...
  def test_create
    customer = Customer.new('customer', 1500)
    CustomerRepository.store(customer)
    assert_not_nil(customer.id)
  end
  # ...
end

I believe this approach increases experimentation because we are free to work with our domain without worrying about persistence issues as if we were using Rails ActiveRecord. Rails AR need us to set up DB tables by running migrations in order to use the entities and have access to the properties.

Project Structure

structure

Conclusion

This whole thing might seem weird if you are used to Rails AR, but the fact that we have our domain totaly decoupled from infrastructure issues is important because as technology changes and shifts, and as our domain layer is burdened with complex computer science problems, things will most certainly change.  Keeping the heart of the software decoupled from tools allows those changes to be easier (XP ?).

That’s it! I’d like to thank Michael Brennan and Guilherme Chapiewski for reviewing and contributing with ideas for this post and Sam Smoot for also reviewing and helping me out with Clipper.