Skip to content

Instantly share code, notes, and snippets.

@elct9620
Last active October 20, 2022 09:55
Show Gist options
  • Save elct9620/db00bad943762049bcfa35a8ba15d8f5 to your computer and use it in GitHub Desktop.
Save elct9620/db00bad943762049bcfa35a8ba15d8f5 to your computer and use it in GitHub Desktop.
The Data Context Interaction (DCI) in Ruby

The Data Context Interaction in Ruby

Concept

Under Domain-Driven Design, we are trying to "Model" the real world to the virtual world. However, it cannot clearly describe the context/interaction with the domain.

Data

The raw data is a "number" or "string" and cannot explain the meaning in specifying the domain. Therefore we have to create a "value object" to assign the domain meaning and use "entity" to compose mapping a real-world object.

  • HealthPoint is a value in this domain to respond to health status
  • Actor is an entity in this domain to respond to an object which can take action
  • Player and Monster is an entities but have different "roles" in the "NewbieTraining" context

Role

The role means can take specify an action in the context. The Ruby mixin the module and include specify the action to the class.

  • Attacker can apply damage to Defender in the context
  • Defender can receive damage and update its data (HealthPoint) in the context

In the Attacker role the damage is applied to Defender only which means we only check it is Defender (or duck-typing to use respond_to(:damage))

Context

The context is a set of actions about roles in this context.

  • NewbitTraining context describes a #attack_by action that can lead Attacker to apply damage to Defender

It is similar to Domain Service to describe one or more entities to co-work together.

class NewbieTraining
def attack_by(player, target:)
player.attack(target)
end
end
class Actor
DEFAULT_HP = HealthPoint.new(100)
attr_reader :id, :name, :hp
def initialize(id:, name:, health: DEFAULT_HP)
@id = id
@name = name
@hp = health
end
end
class Player < Actor
include Attacker
end
class Monster < Actor
include Defender
end
player = Player.new(id: 'P1', name: 'Aotoki')
monster = Monster.new(id: 'M1', name: 'Goblin')
context = NewbieTraining.new
context.attack_by(player, target: monster)
puts "#{player.name}'s health is #{player.hp}"
puts "#{monster.name}'s health is #{monster.hp}"
# => Aotoki is attacking Goblin
# => Goblin is damaged 10 HP
# => Aotoki's health is 100
# => Goblin's health is 90
RSpec.describe NewbieTraining do
subject(:context) { NewbieTraining.new }
describe '#attack_by' do
subject(:attack) { context.attack_by(player, target: monster) }
let(:player) { Player.new(id: 'P1', name: 'Aotoki') }
let(:monster) { Monster.new(id: 'M1', name: 'Goblin') }
it { expect { attack }.to output(/Aotoki is attacking Goblin/).to_stdout }
it { expect { attack }.to output(/Goblin is damaged \d+ HP/).to_stdout }
end
end
module Attacker
def attack(defender)
raise ArgumentError, 'not a valid defender' unless defender.is_a?(Defender)
puts "#{name} is attacking #{defender.name}"
defender.damage(HealthPoint.new(10))
end
end
module Defender
def damage(amount)
puts "#{name} is damaged #{amount} HP"
@hp -= amount
end
end
class HealthPoint < Numeric
MIN_VALUE = 0
def initialize(value)
@value = value
end
def to_s
inspect
end
def inspect
@value.to_s
end
def to_i
@value
end
def -(other)
HealthPoint.new([MIN_VALUE, to_i - other.to_i].max)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment