Victor works on the Angular team at Google. He is interested in functional programming, the Web platform, and client-side applications. Being a language nerd he spends a lot of my time playing with Smalltalk, JS, Dart, Scala, Haskell, Clojure, Ruby, and Ioke. Victor is a DZone MVB and is not an employee of DZone and has posted 45 posts at DZone. You can read more from them at their website. View Full User Profile

DCI in Ruby (Part 1)

05.25.2012
| 1644 views |
  • submit to reddit

DCI (Data Context Interaction) is a new way to look at object-oriented programming. If you’d like to read some theory to see the difference between DCI and traditional OOP there is a nice article covering the topic:

http://www.artima.com/articles/dci_vision.html

And this presentation can be very helpful too:

http://www.infoq.com/presentations/The-DCI-Architecture

It isn’t easy to use DCI in Java as by its nature DCI requires sharing behavior between classes and Java doesn’t provide any decent ways to do it. But many modern languages do including Ruby. To demonstrate how to use mixins in Ruby for implementing DCI I’ll write a simple app.

Requirements:

  • We have users
  • We have companies
  • Users can follow users
  • Users can follow companies
  • Users are entities stored in a database
  • Companies are entities stored in a database

Basically, we have two domain classes: users and companies and use cases: when a user starts following a company and he starts following another user.

Firstly, let’s create our domain classes:

class User
  attr_reader :id, :name, :age, :followers

  def initialize id, name, age, followers = []
    @id, @name, @age, @followers = id, name, age, followers
  end
end

class Company
  attr_reader :id, :name, :country, :followers

  def initialize id, name, country, followers = []
    @id, @name, @country, @followers = id, name, country, followers
  end
end

And a simple Database class representing persistent infrastructure of a real application:

class Database

  USERS = {
    1 => User.new(1, "John", 25),
    2 => User.new(2, "Sam", 26)
  }

  COMPANIES = {
    1 => Company.new(1, "Big Company", "Canada")
  }

  def find_user_by_id id
    USERS[id]
  end

  def find_company_by_id id
    COMPANIES[id]
  end

  def update_user user
    USERS[user.id] = user
  end

  def update_company company
    COMPANIES[company.id] = company
  end
end

Domain objects in DCI aren’t smart. They don’t provide methods for all possible use cases. They don’t interact with each other in complex ways. Instead, they have a set of fields and a bunch of convenient methods to access them.

All our business logic is concentrated in roles. Role is a piece of behavior that we can mix into our domain classes to solve business problems. We’ll need two roles for our toy application:

module Follower
end

module Following
  def add_follower follower
    followers << follower
  end
end

Follower is a marker role. It isn’t necessary to create such a kind of a role but I like to do it as it clarifies my intent.

The only part that left is a context, which will extract domain objects from the database, assign some roles to them and perform a business transaction:

class FollowersListContext

  def initialize db
    @db = db
  end

  def add_follower_to_user following_user_id, follower_user_id
    following = @db.find_user_by_id following_user_id
    follower = @db.find_user_by_id follower_user_id

    following.extend Following
    follower.extend Follower

    following.add_follower follower

    @db.update_user following
  end

  def add_follower_to_company following_company_id, follower_user_id
    following = @db.find_company_by_id following_company_id
    follower = @db.find_user_by_id follower_user_id

    following.extend Following
    follower.extend Follower

    following.add_follower follower

    @db.update_company following
  end
end

#using our context
db = Database.new
context = FollowersListContext.new db
context.add_follower_to_user 1, 2

It may not be the most impressive example as we share only one line of code but it shows how all pieces work together. In a real word example roles will do much more than just adding an item to a collection. As a result this kind of decomposition will allow us to split complex behavior and avoid monster classes with thousands lines of code.

Now let’s take a look at a few ways of doing our code look a bit better.

The simplest way is to define an alias for the extend method. So instead of extending objects we’ll assign roles.

class Object
  define_method :add_role do |role|
    self.extend role
  end
end

following = db.find_company_by_id 1
follower = db.find_user_by_id 2
following.add_role Following
follower.add_role Follower

It’s also not a problem to make it the other way around and make our roles responsible for modifying objects.

class Module
  def played_by obj
    obj.extend self
  end
end

user_a = db.find_company_by_id 1
user_b = db.find_user_by_id 2
perform_operation(Following.played_by(user_a), Follower.played_by(user_b))

If you need more flexibility you can always add a function that will add required roles to an object.

module FollowerRole
  #...
end

def Follower object
  object.extend FollowerRole
end

follower = Follower(user_a)

Another fancy way of doing it is returning an object with a role assigned to it.

following = user_a.in_role Following
follower = user_b.in_role Follower

To make it look a bit more declarative we can specify what roles we want to assign to our domain objects in a array. The using method will iterate over the array and add all necessary roles. So our context will look like this:

using [user_a, :as, Following,
          user_b, :as, Follower]   do |following, follower|

end

To Sum Up

Though Ruby doesn’t have the concept of a role you can easily mimic it with mixins. There are tons of ways to do it, from the most simple ones to robust DSLs.

DCI is definitely gaining some popularity right now, so there is a chance that the next Rails app you’ll work on will use it in some form. But even if it don’t DCI will give you one more way to think about OO design.

Published at DZone with permission of Victor Savkin, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)