Skip to content

Instantly share code, notes, and snippets.

@randito
Created May 7, 2012 05:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save randito/2626116 to your computer and use it in GitHub Desktop.
Save randito/2626116 to your computer and use it in GitHub Desktop.
(Simplified) Example README for a DCI Ruby project in the works

Note: This version is a simplified version of this gist; it removes all the unnecessary features (like validations, required parameters, etc).

Data, Context, and Interaction

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.

Exploring DCI

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.

Money Transfer use-case example

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)

Other Examples

From Steen Lehmann

https://gist.github.com/1315636 https://gist.github.com/1315637

Domain Driven Design and Object Composition

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment