Skip to content

Instantly share code, notes, and snippets.

@izbor izbor/dci_example.rb
Last active Mar 20, 2018

Embed
What would you like to do?
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.