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.