DCI in Ruby (Part 1)
Join the DZone community and get the full member experience.
Join For FreeDCI (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.
Opinions expressed by DZone contributors are their own.
Comments