These are the font sizes and colours used in this talk
class Foo
def bar
@ivar == CONSTANT
end
end(Plain text)
Element
Objects talking to objects

Gavin Morrice
aka Bodacious

We're hiring!


Objects talking to objects
- Fundamental ideas in OOP
- Process in complicated systems
- Anti-patterns to these ideas

I think it's really awesome that I have tools for building things that reflect the way I think and how I understand the world
a [programming] language of description that serves as an interface between the models in the human mind and those in computing hardware

OOP lets us create code that is modelled on how we understand the the real world... and the real world is made up of things—we call those objects
Identity
State
Behaviour
What is an object?
What is an object?
Identity


What is an object?
Identity
class Kettle
end
kettle_a = Kettle.new
kettle_b = Kettle.new
puts kettle_a.object_id # => 3096
puts kettle_b.object_id # => 3104
kettle_a.equal?(kettle_b) # => falseWhat is an object?
State

What is an object?
State
class Kettle
def initialize
@water_level = 0
@temperature = 0
@heating = false
end
def on?
@heating
end
def water_level
@water_level
end
def temperature
@temperature
end
endWhat is an object?
Behaviour

What is an object?
Behaviour
class Kettle
TARGET_TEMP_C = 100
def initialize
@water_level = 0
@temperature = 0
@heating = false
@heating_element = HeatingElement.new
end
def on?
@heating
end
def water_level
@water_level
end
def temperature
@temperature
end
def boiled?
temperature == TARGET_TEMP_C
end
def fill(volume)
@water_level += volume
end
def empty(volume)
@water_level -= volume
end
def turn_on
turn_off if water_level < MIN_WATER_LEVEL
heat
sleep 0.5 until temperature >= TARGET_TEMPERATURE
turn_off
end
def turn_off
heating_element.turn_off
end
protected
def heat
heating_element.heat
end
def heating_element
@heating_element
end
endEvery component ...should be able to present itself in a meaningful way for observation and manipulation.

Everything in Ruby is an Object
hash == { foo: 'bar' }
hash.equal?({foo:'bar'}) # => false
hash.empty? # => false
hash.transform_keys!(&:to_s)Identity
State
Behaviour
If one part of the system works differently from all the rest, that part will require additional effort to control.

Recursive design


Recursive design
@kettle = Kettle.new
@heating_element = @kettle.heating_element
@coil = @heating_element.coil
# ...Messaging

Summary
Objects have three characteristics
- Identity
- State
- Behaviour
We can combine objects to make more complicated objects
Objects collaborate to become more than they are separately
OOP is a powerful paradigm that allows us to build complex things out of simple things, just like in the real world
4 Core Principles of OOP
4 Core Principles of OOP
Abstraction
Encapsulation
Extension
Polymorphism
Abstraction
Capturing the essence of something by ignoring irrelevant detail
Abstraction
Distilling something down to its essential characteristics, stripping away all non-essential aspects.
class Kettle
# ...
def add_water(volume) ... end
def pour_water(volume) ... end
def boiled? ... end
def water_level ... end
def on_button ... end
def steam_sensor ... end
def power_supply ... end
def lid ... end
def heating_element ... end
def electric_contacts ... end
# ...
endAbstraction
Distilling something down to its essential characteristics, stripping away all non-essential aspects.
class Kettle
# ...
def add_water(volume) ... end
def pour_water(volume) ... end
def boiled? ... end
def water_level ... end
def on_button ... end
def steam_sensor ... end
def power_supply ... end
def lid ... end
def heating_element ... end
def electric_contacts ... end
# ...
endclass Kettle
def fill(volume)
# ...
end
def empty(volume)
# ...
end
def boiled?
# ...
end
def heat
# ...
end
endAbstraction
Distilling something down to its essential characteristics, stripping away all non-essential aspects.
def fill(volume)
# ...
end
def empty(volume)
# ...
end
def boiled?
# ...
end
def heat
# ...
end
Abstraction
Distilling something down to its essential characteristics, stripping away all non-essential aspects.
class WaterHeater
def fill(volume)
# ...
end
def empty(volume)
# ...
end
def boiled?
# ...
end
def heat
# ...
end
end
Abstraction
kettle = Kettle.new
kettle.turn_on
until kettle.temperature < 100
sleep(1)
end
kettle.turn_offAbstraction
Distilling something down to its essential characteristics, stripping away all non-essential aspects.
kettle = Kettle.new
kettle.boil!Polymorphism
Defining interactions based on what objects do, rather than what they are.
Polymorphism
Defining interactions based on what objects do, rather than what they are.
class TeaMaker
def initialize(milk_bottle: MilkBottle.oldest,
tea_jar:,
sugar_bowl:)
@milk_bottle = milk_bottle
@tea_jar = tea_jar
@sugar_bowl = sugar_bowl
@spoon = Spoon.new
@kettle = Kettle.new
end
def make_tea(cup:, milk_volume: 0, sugars: 0)
@kettle.fill_water(cup.fill_volume)
@kettle.boil
cup.add(@tea_jar.take)
cup.add(@kettle.empty_water(cup.fill_volume))
cup.add(@milk_bottle.empty(milk_volume))
cup.add(@sugar_bowl.take(sugars))
@spoon.stir(cup)
cup
end
endPolymorphism
Defining interactions based on what objects do, rather than what they are.
class TeaMaker
def initialize(milk_source: MilkBottle.oldest,
water_heater: Kettle.new,
tea_source:,
sugar_source:)
@milk_source = milk_source
@tea_source = tea_source
@water_heater = water_heater
@sugar_source = sugar_source
@spoon = Spoon.new
end
def make_tea(cup:, milk_volume: 0, sugars: 0)
@water_heater.fill_water(cup.fill_volume)
@water_heater.boil
cup.add(@tea_source.take)
cup.add(@water_heater.empty_water(cup.fill_volume))
cup.add(@milk_bottle.empty(milk_volume))
cup.add(@sugar_bowl.take(sugars))
@spoon.stir(cup)
cup
end
endPolymorphism
class TeaMaker
def initialize(milk_source: MilkBottle.oldest,
stirrer: Spoon.new,
tea_source:,
water_heater:,
sugar_source:)
@milk_source = milk_source
@stirrer = stirrer
@tea_source = tea_source
@water_heater = water_heater
@sugar_source = sugar_source
end
def make_tea(cup:, milk_volume: 0, sugars: 0)
@water_heater.fill_water(cup.fill_volume)
@water_heater.boil
cup.add(@tea_source.take)
cup.add(@kettle.empty_water(cup.fill_volume))
cup.add(@milk_bottle.empty(milk_volume))
cup.add(@sugar_bowl.take(sugars))
@stirrer.stir(cup)
cup
end
endTeaMaker.new({
tea_source: TeaJar.main,
water_heater: Pot.new
})Extension
Adding new behaviour to an existing abstraction by building on top of it, without altering the original
Extension
Adding new behaviour to an existing abstraction by building on top of it, without altering the original.

Extension
Adding new behaviour to an existing class or abstraction to create a new specialised version, without altering the original.
class Kettle
def fill(volume)
# ...
end
def empty(volume)
# ...
end
def heat
# ...
end
def boiled?
# ...
end
endExtension
class Kettle
def fill(volume)
# ...
end
def empty(volume)
# ...
end
def heat
# ...
end
def boiled?
# ...
end
endclass KettleWithTemperatureDisplay < Kettle
def initialize
super
@temperature_display = TemperatureDisplay.new
end
def temperature
temperature_display.temperature
end
end
class KettleWithTemperatureDisplay
extend Forwardable
def_delegators :@kettle, :fill,
:empty, :heat, :boiled?
def_delegator :@temperature_display,
:temperature
def initialize
@kettle = Kettle.new
@temperature_display = TemperatureDisplay.new
end
endExtension
class Kettle
def fill(volume)
# ...
end
def empty(volume)
# ...
end
def heat
# ...
end
def boiled?
# ...
end
endclass KettleWithTemperatureDisplay < Kettle
def initialize
super
@temperature_display = TemperatureDisplay.new
end
def temperature
temperature_display.temperature
end
end
class KettleWithTemperatureDisplay
extend Forwardable
def_delegators :@kettle, :fill,
:empty, :heat, :boiled?
def_delegator :@temperature_display,
:temperature
def initialize
@kettle = Kettle.new
@temperature_display = TemperatureDisplay.new
end
endEncapsulation
Hiding the information about an object's internal state and workings, exposing only a tightly controlled interface to the outside world.
Encapsulation
Hiding the information about an object's internal state and workings, exposing only a tightly controlled interface to the outside world.
Encapsulation
class TeaMaker
BOILING_TEMP_C = 100
def make_tea
# ...
kettle.heating_element.heat!
sleep(1) until kettle.internal_temp == BOILING_TEMP_C
kettle.heating_element.turn_off!
# ...
end
end
endEncapsulation
class TeaMaker
BOILING_TEMP_C = 100
def make_tea
# ...
kettle.heating_element.heat!
sleep(1) until kettle.internal_temp == BOILING_TEMP_C
kettle.heating_element.turn_off!
# ...
end
end
endclass TeaMaker
def make_tea
# ...
kettle.boil!
# ...
end
end4 Core Principles of OOP
Abstraction
Encapsulation
Extension
Polymorphism
4 Core Principles of OOP
Abstraction
Encapsulation
Extension
Polymorphism
Lets us hide details; Keeps code manageable, but also flexible
Lets us use multiple objects in new and different ways. Allows for cleaner code and maximal flexibility
Lets us introduce new behaviour; Allows for reuse without disruption
Lets us hide implementation and protect internal consistency; Allows for flexibility, robustness, and safety
The promise of OOP
If you follow these principles, the system you build should be maximally scalable, flexible, and robust; while minimising the cost of future change and disruption
???
Where is process defined in a complex/complicated system?









🖥️🤔
class UserSubscriberService
end
class UserSubscriberService
def self.call
end
end
class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
else
#
end
end
end
class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
return Result.new(:success)
else
return Result.new(:failure, "Payment failed")
end
end
end
Service Objects
class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
return Result.new(:success)
else
return Result.new(:failure, "Payment failed")
end
end
end
State?
Behaviour?
Identity?
Service Objects

class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
return Result.new(:success)
else
return Result.new(:failure, "Payment failed")
end
end
end

Centalised control
Object
Object
Object
Procedure
Object
Object
Object
Object
Object
Object
Distributed control
Object
Object
Object
Caller
Object
Object
Object
Object
Distributed control
Object
Object
Object
Caller
Object
Object
Object
Object
In OOP, process is not centrally defined; but emerges as the result of objects talking to objects
class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
return Result.new(:success)
else
return Result.new(:failure, "Payment failed")
end
end
end
Many dependencies
Object
Object
Object
Procedure
Object
Object
Object
Object
Object
Object
Few dependencies
Object
Object
Object
Caller
Object
Object
Object
Object
class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
return Result.new(:success)
else
return Result.new(:failure, "Payment failed")
end
end
end
Object
Object
Object
Procedure
Object
Object
Object
Object
Object
Object
1 degree
2 degrees
3 degrees
Object
Object
Object
Caller
Object
Object
Object
Object
class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
return Result.new(:success)
else
return Result.new(:failure, "Payment failed")
end
end
end
Object
Procedure
Property A
Property B
Method A
Object
Caller
Property A
Property B
Method A

"Real OOP"
—Alan Kay
Build like nature builds
Anaemic domain model

Anaemic domain model

... that there is hardly any behavior on these [domain] objects, making them little more than bags of getters and setters. ... Instead there are a set of service objects which capture all the domain logic, carrying out all the computation and updating the model objects with the results. These services live on top of the domain model and use the domain model for data.
Anaemic domain model
class UserSubscriberService
PROMO_DISCOUNT_PERCENT = Rational(85, 100) # 15% OFF
PLAN_NAME = 'premium'
def self.call(user)
plan = Plan.find_by(name: PLAN_NAME)
user.subscription.plan = plan
user.subscription.amount = plan.amount * PROMO_DISCOUNT_PERCENT
payment = user.subscription.payments.create(
payment_source: user.credit_card
)
if payment.persisted?
user.account.increment(:credit_balance, plan.credits)
user.subscription.status = 'active'
user.subscription.subscribed_at = Time.now
return Result.new(:success)
else
return Result.new(:failure, "Payment failed")
end
end
end
class User < ApplicationRecord
has_many :subscriptions
has_one :account
end
class Account < ApplicationRecord
end
class SubscriptionPlan < ApplicationRecord
end
class Subscription < ApplicationRecord
belongs_to :plan
belongs_to :user
has_many :payments
end
class Payment < ApplicationRecord
end
Some tips for using Procedure Objects
1. Don't.
2. Keep domain knowledge out of them
Some tips for using Procedure Objects
2. Keep domain knowledge out of them
class UserSubscriberService
def self.call(user:, plan:, email_service: EmailSender.new)
if Subscription.subscribe_user_to_plan(user:, plan:)
email_service.send_email(user, :subscription_successful)
Result.new(:success)
else
Result.new(:failure, "Failed to add subscription")
end
end
endSome tips for using Procedure Objects
1. Don't.
2. Keep domain knowledge out of them
3. Do not call them from other procedure objects
Some tips for using Procedure Objects
3. Do not call them from other procedure objects
class SubscriptionsController < ApplicationController
def create
result = UserSubscriberService.call(user: current_user
subscription_plan_name: params[:plan_name],
discount_code: params[:discount_code],
cadence_months: params[:cadence])
if result.success?
redirect_to welcome_url
else
render :new, assigns: { result: result }, status: :unprocessable_entity
end
end
end
Putting it all together
OOP is a really powerful paradigm that allows us to build scalable, complex systems that are robust and resilient to change
Patterns that step out of this paradigm should be treated with scepticism
Process should not be centrally defined, but should emerge through objects talking to objects
Thank you!
(Slides)
Objects talking to objects III
By Gavin Morrice
Objects talking to objects III
Exploring what makes OOP so great in Ruby
- 90