Note: This version is a simplified version of this gist; it removes all the unnecessary features (like validations, required parameters, etc).
DCI is a modeling pattern by Trygve Reenskaug, creator of MVC, that is a replacement of sorts for typical OOP.
DCI seperates the domain model (D - Data) from the use cases (C - Context) and identifies the roles that the objects play (I - Interaction). Data objects represent what an object is -- imagine, pure data and attributes. Interaction objects defines what an object does. Those are implemented as Ruby mixins that are dynamically extended into objects. Contexts will find objects using a repository, assign roles to objects within a context and then executes them.
Behavior and logic (called Roles or Interactions) are contained in modules.
These modules get mixed into objects by the context. Behavior and logic should only communicate with each other via other Roles. Having a seperate class for the logic and behavior of your data allows the user to easy create other patterns from that -- Command, EventSourcing, Serialization, Events, etc.
Data are your nouns, Interaction (or Roles) are your verbs, and the Context is a use-case that puts it all together.
This example uses a very simple Data class and a simple Interaction module. The Interaction defines what the Data object "does." All of an object's domain knowledge should be defined in these simple methods.
However, the Context class has all the meta-programming fun to help avoid some of the boilerplate type code.
The MoneyTransfer context can define expected roles and data objects using a "param" method:
class MoneyTranfer
include DCI::Context
param :from, roles: Transfer, type: Account
param :to, roles: Transfer, type: Account
repository :find
def run
from.transfer.to(amount)
end
end
This makes it easy to build and use the Context classes. In this example, we combine calling the context directly with an object and passing in an "id."
src, dest_id = Source.find(1), 2
mt = MoneyTransfer.run(from: src, to: dest_id, amount: 200)
# => Calls Account.find(dest_id = 2)
new
and run
will be called with parameters. For each param
defined,
the roles specified will be mixed in. Also, the type
will be verified.
If the type does not match, then we use a repository to try and load
the object specified.
After loading the object and extending them with their required roles, the objects
will bind into the current context. The run
method can use these values
directly.
require 'dci'
require 'tranaction' # fictional module
class Account
include DCI::Data
attr_accessor :balance
end
module TransactionalSave
include DCI::Interaction
end
module Transfer
include DCI::Interaction
# identifies an additional role that this role uses internally
include TransactionalSave
def withdraw(amount)
raise Error.new "Not enough money for withdrawal: #{amount}" if balance < amount
balance -= amount
end
def deposit(amount)
balance += amount
end
#
# "to" and "from" are bound directly via the context
# "transaction" is just a fictional library to add rollback and commit
def transfer(to, amount)
transaction do |t|
# internally, we use a "save" role included above
from.withdraw(amount).save
to.deposit(amount).save
t.commit
rescue
t.rollback
end
end
end
class MoneyTranfer
include DCI::Context
#
# "param" defines a named role that is expected as a parameter for "new" or "run".
# The module or modules defined in the "roles" attribute will be mixed into the object.
#
# A "param" without assigned "roles" or "type" is just bound to the context object.
#
param :from, roles: Transfer, type: Account
param :to, roles: Transfer, type: Account
param :amount
#
# If the value of the parameter "is_a" object that matches the "type" paramter
# then we will use that object directly. That way, contexts can take objects as
# parameters as opposed to ids.
#
# to,from = Account.find(1), Account.find(2)
# MoneyTransfer.run(to: to, from: from, amount: 100)
#
# However, if you set the parameters to something other than a matching object,
# then it will use the repository method (i.e. find) on the class to locate the object
# using the parameters passed in and assign that to the object.
#
# to_id, from_id = 1, 2
# MoneyTransfer.run(to: to_id, from: from_id, 100)
# # => Calls Account.find(1) and Account.find(2)
#
repository :find
#
# If the repository is a string or symbol, it will call that method on the object's class.
# If it's a lambda, it will execute that block passing in the parameter values. If you
# pass in a Module, it will call a "find(clazz, parameters)" on that module.
#
# repository :find
# repository -> clazz,params { clazz.find(params) }
# repository RedisRepository # calls find(clazz,params) on the Module
#
# You can override the repostory for a parameter using a "repository" attribute
# param :from, roles: Transfer, type: Account, repository: 'find'
#
def new(*params)
super(params)
end
#
# "from" and "to" are bound to the context via the parameters in the constructor
#
def run
from.transfer.to(amount)
end
def self.run(*params)
new(params).run
end
end
src, dest_id = Source.find(1), 2
mt = MoneyTransfer.run(from: src, to: dest_id, amount: 200)
From Steen Lehmann
https://gist.github.com/1315636 https://gist.github.com/1315637
DCI has strong ties with object-composition. It favors composition over typical OOP techniques like polymorphism and inheritance. The group for DCI is aptly named object-composition..
The concept behind DCI can be expanded by some of the ideas from DDD book, Domain Driven Design. Some of the concepts from the DDD book apply to a large DCI system including an Entity class, Value Objects, Boundries for Aggregates, Repositories, and Factories.
For example, a Data object could use an Entity class for its global identity and be composed of several Value Objects to hold all the data. These Entity-Value Object aggregates could be created via Factories and found and queried via Repositories. The Interaction would respect the Aggregate Boundry by only communicating with the Entity through established Roles.
The previous examples do not do any aggregation into entities and value objects.