MarcoCabazal
10/27/2014 - 7:11 PM

DCI (Data, Contexts, Interactions) paradigm example in Ruby

DCI (Data, Contexts, Interactions) paradigm example in Ruby

# DCI EXAMPLE IN RUBY (with some prototype elements)
# Blog post: https://www.ludyna.com/oleh/dci-example-in-ruby
# 
# More info: 
#
#   Creator of MVC & DCI, Trygve Reenskaug: DCI: Re-thinking the foundations of 
#   object orientation and of programming
#   http://vimeo.com/8235394
#
#   James Coplien: Why DCI is the Right Architecture for Right Now
#   http://www.infoq.com/interviews/coplien-dci-architecture
#
#   Creator of MVC & DCI, Trygve Reenskaug - Object Orientation Revisited. 
#   Simplicity and power with DCI
#   http://vimeo.com/43536416
# 
#   James Coplien: The DCI Architecture: Supporting the Agile Agenda
#   https://www.youtube.com/watch?v=SxHqhDT9WGI
# 
#   Matz reaction about this idea:
#   https://twitter.com/yukihiro_matz/status/529854155314053120
# 
# my twitter: @kajamite



# =========================================
# Proposals (ideas) to extend Ruby lang
# =========================================

# (proposal #1) New method .unextend() (or curtail()?, withdraw()?) is added to Ruby, 
# so we can effectively remove modules from instances. Optimized for speed.

# For example:
human.extend ParentInteraction    # we can do this right now in Ruby
human.unextend ParentInteraction  # but we can't do this at the moment 

# Usage example (some context method):
def do_parenting(parent, child)
  parent.extend ParentInteraction        # <= temporary extends "parent" instance 
                                         #    with ParentInteraction module
  child.extend ChildInteraction          # <= temporary extends "child" instance 
                                         #    with ChildInteraction module
  
  amount = parent.get_pocket_money_amount_today  # <= call ParentInteraction method 
                                                 
  
  child.receive_pocket_money(amount)             # <= call ChildInteraction method
                                         
  
  child.unextend ChildInteraction        # UNextend ChildInteraction module from "child"
  parent.unextend ParentInteraction      # UNextend ParentInteraction module from "parent"
end
  

# (proposal #2) New Ruby keyword "as". This keyword _temporary_ extends modules into 
# _instances_ of classes and automatically removes them when out of scope. Some more 
# appropriate word can be used instead of "as" or different syntax - it is just an idea.

# Previous example with "as" keyword
def do_parenting(parent, child)
  parent as ParentInteraction           # <= temporary extends "parent" instance 
                                        #    with ParentInteraction module
  
  child as ChildInteraction             # <= temporary extends "child" instance
                                        #    with ChildInteraction module
  
  amount = parent.get_pocket_money_amount_today       # <= call ParentInteraction method  
  child.receive_pocket_money(amount)                  # <= call ChildInteraction method
                                        
                                        
end                                     # <= out of scope, ParentInteraction module is automatically
                                        #    removed from "parent" instance and ChildInteraction 
                                        #    module is automatically removed from "child" instance


# Previous example with "as" keyword in parameters list
def do_parenting(parent as ParentInteraction, child as ChildInteraction)
  amount = parent.get_pocket_money_amount_today
  child.receive_pocket_money(amount)
end                                    
                                       

# it would be nice for "as" operator to work like this as well:
human as ParentInteraction, EmployeeInteraction # <= case when we need human object to play 
                                                # two roles at the same time


# =========================================
# Actual DCI example
# =========================================
# DCI stands for Data, Contexts and Interactions.

# DATA. What The-System-Is. Classes without interaction code.

class Human
  attr_reader :name   # name of human
  attr_reader :amount # amount of money the human has, this is very simple example
  
  def initialize(name:, amount: 0)
    self.name = name
    self.amount = amount
  end
  
  def deposit(amount)
    self.amount += amount
  end
  
  def withdraw(amount)
    self.amount -= amount
  end
  
  def say(text)
    puts text
  end
end

# INTERACTIONS (Roles). Role methods direct the execution of the Use Case.
# 
# Humans can play different roles in different contexts
# like Parent, Child, Employee, etc.

module ParentInteraction
  
  # parent decides how much it wants to spend on pocket money for child
  def get_pocket_money_amount_today
    amount = # ...     # complex logic of parent decision, probably 
                       # based on family budget and child behaviour
                       # or something like rand(20)
    
    withdraw(amount)   # <= Human method is called                      
    amount             # <= return amount
  end
  
  # other role related methods
  # ...
end

module ChildInteraction
  def receive_pocket_money(amount)
    deposit(amount)                 # <= Human method is called   
    say('Thank you')                # <= Human method is called   
  end
  
  # other role related methods
  # ...
end

module EmployeeInteraction
  
  def receive_salary(amount)
    deposit(amount)
    # ...
  end
  
  def do_one_task_from_list(tasks)
    # ...
  end
  
  # other role related methods
  # ...
  
end


# CONTEXTS. What The-System-Does.
# Contexts are essentially a Use Cases. 
# Muster objects to play the roles. 
# Inject role methods. Trigger interaction.
# Place where roles are played. A collection of related possible scenarios.
# In different contexts same human object can play different roles.

class HomeContext # at home humans might play parents and children roles
  
  # More than a subroutine - includes role / objects bindings
  def self.do_parenting(parent as ParentInteraction, child as ChildInteraction)
    amount = parent.get_pocket_money_amount_today
    child.receive_pocket_money(amount)
  end # <= out of scope, ParentInteraction is removed from 
      #    "parent" object, Child is removed from child object
  
  ...
end

class JobContext # at job humans might play employee, managers, etc. roles
  
  def self.do_your_job(human as EmployeeInteraction)
    human.do_one_task_from_list(@job.tasks)
  end # <= out of scope, EmployeeInteraction is removed 
      #    from human object
  
  def self.receive_salary(human as EmployeeInteraction)
    # over 9 thousands
    human.receive_salary(9900)
  end # <= out of scope, EmployeeInteraction is removed 
      #    from "human" object
end

# using contexts

julia   = Human.new(name: 'Julia', amount: 20)
severyn = Human.new(name: 'Severyn')

HomeContext.do_parenting(julia, severyn)  # <= "julia" instance is extended 
                                          #    with ParentInteraction module temporary, 
                                          #    and "severyn" instance is temporary extended with 
                                          #    ChildInteraction module 

JobContext.do_your_job(julia)             # <= "julia" instance is extended with 
                                          #    EmployeeInteraction module temporary
                                          
JobContext.receive_salary(julia)          # <= same here

HomeContext.do_parenting(julia, severyn)

julia.get_pocket_money_amount_today       # => Exception. Method get_pocket_money_amount_today() 
                                          #    does not exist for "julia" object. 

# And, for example if you do something like this:
julia = human as ParentInteraction
julia.receive_pocket_money(5)
# => you get exception. No such method exists in "julia" 
#   (it exists in ChildInteraction module only).


# =========================================
# DCI usage example in Rails app
# =========================================

# In case of Rails, context methods should be called from 
# controller actions, cron jobs, rake tasks.
# Often contexts replace "managers", "services" modules/classes


class ParentingController < ApplicationController
  
  def do_parenting
    child = Human.find params[:id]                # Using ActiveRecord, as we used to.
    
    HomeContext.do_parenting current_user, child  # Objects methods 
                                                  # are methods to change only their object's data.
                                                  # If you need methods that changes data of 
                                                  # more than one object - you should put those 
                                                  # methods into Context modules/classes 
                                                  # (previously known as "services", "utility" or
                                                  # "managers" classes/modules)
    
    # ...
  end
    
end  


# =========================================
# Everything looks better when using DCI
# =========================================

# !! With DCI many patterns are obsolete, like ActiveRecord or DataMapper 
# (they are just trying to "emulate" DCI ("instance plays role" part) 
# using classes - Classes Oriented Programming (COP) 
# - correct me if I am wrong)

# For example if you need to save human data object into SQL DB 
# you just create SQL database interaction (role)
human as HumanSQLDBInteraction
human.save! # <= human is saved into SQL database here

# where HumanSqlInteraction could be something like this:
module HumanSqlDBInteraction 
  include SQLDBInteraction
  
  has_many :bank_accounts
  
  scope :active_users, -> { where(active: true) }
    
  # ....
end

human.unextend HumanSqlDBInteraction  # remove SQL interaction, now we want to 
                                      # deal with Cassandra database

# do you want to save human instance to 
# "noSQL" Cassandra database? No problem
human as HumanCassadraInteraction
human.save! # <= same human instance is saved 
            #    into "noSQL" database this time

# where HumanCassandraInteraction could be:
module HumanCassandraInteraction 
  include CassandraInteraction
  
  attribute :name,   :string
  attribute :amount, :decimal
  
  partition_key :name
  
  # ...
end

# or another approach could be like this
human as HumanSqlDBInteraction, HumanCassandraInteraction
human.save!   # with "one" method call save data both 
              # into SQL and Cassandra databases.