Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save victormartins/91bb0158d0db0e3e252c9a5640a620b2 to your computer and use it in GitHub Desktop.
Save victormartins/91bb0158d0db0e3e252c9a5640a620b2 to your computer and use it in GitHub Desktop.
Learning Examples
# Source: https://www.destroyallsoftware.com/talks/boundaries
# @garybernhardt
#
# This talk is about using simple values (as opposed to complex objects) not just for holding data,
# but also as the boundaries between components and subsystems.
# It moves through many topics: functional programming; mutability's relationship to OO;
# isolated unit testing with and without test doubles; and concurrency, to name some bar.
# The "Functional Core, Imperative Shell" screencast mentioned at the end is available as part of
# season 4 of the DAS catalog.
# Summary:
# - Testing functinal code is almost always easier than testing imperative code.
# - The tests are fast naturally even without doing Stubs or Mocks.
# - There are no call boundary risks, because we don't stub or mock. We don't have real boundaries,
# the boundaries are values and we use real values in the specs.
# - Concurrency gets easier, at least the actor model.
# - A system should be composed of:
#  • An Imperative Shell that has state but makes no or mininial decisions.
#  • A functional Core that has all the inteligence of the system.
# An example of this system can be seen here: https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell
# - We get higher code mobility in general, between classes, between sub systems and between processes.
#
# Example of the Twitter app: https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell
# | Mutation | Data & Code |
# Procedural | YES | SEPARATE |
# OO | YES | COMBINED |
# Functional | NO | SEPARATE |
# ------------------------------------|
# Hybrid | NO | COMBINED | (Not a true paradigm but a FauxOO)
# Procedural Code ------------------------------------------------------------------------
# This code is procedural for two reasons:
# 1. The each is telling that something destructive is going on.
# 2. Because we know the deep structure of the rat. We know it has a stomach and that
# we can shovel things into it.
def feeding_time
rats.each do |rat|
rat.stomach << Cheese.new
end
end
# Object Oriented Code (very similar to procedural) --------------------------------------
#
# This code is still destructive since we use the each method
# but we hide the stomach details (deep structure) by using the eat methid.
# So the main different between the OO and the Procedural version is that the
# internals are hidden.
def feeding_time
rats.each do |rat|
rat.eat(Cheese.new)
end
end
class Rat
def eat(food)
@stomach << food
end
end
# Functional Code ------------------------------------------------------------------------
# Instead of each we use map, so we are building an array of new Rats
# instead of mutating existing ones.
def feeding_time
rats.map do |rat|
eat(rat, 'cheese')
end
end
# Imagine rat to be an Hash, the stomach an Array and food a String
def eat(rat, food)
# build a new stomach that is the old stomach plus new food ([A] + [B] = [A, B])
stomach = rat.fetch(:stomach) + [food]
# now we replace the stomach of the rat
rat.merge(stomach: stomach)
end
# Hybrid (FauxOO) Code ------------------------------------------------------------------------
# We use map instead of the destructive each like in functional programming.
# We tell the rat to eat like in the Object Oriented Programming
def feeding_time
rats.map do |rat|
rat.eat(Cheese.new)
end
end
def Rat
def eat(food)
# Its constructing a new object instead of a new primitive
Rat.new(@name, @stomach + [food])
end
end
# Typical code tested with Stubs and Mocks
describe Sweeper do
context 'when a subscription is expired' do
let(:bob) do
stub(
active: true,
paid_at: 2.months.ago
)
end
let(:users) { [bob] }
before { User.stub(:all) { users } }
end
it 'emails the user' do
UserMailer.should_receive(:billing_problem).with(bob)
Sweeper.sweep
end
end
# ! Depends on User and UserMailer
class Sweeper
def self.sweep
User.all.select do |user|
user.active? && user.paid_at < 1.month.ago
end.each do |user|
UserMailer.billing_problem(user)
end
end
end
# ------------------------------------------------
# Removing Mocks and Stubs
describe Sweeper do
context 'when a subscription is expired' do
let(:bob) do
# This object is a Struct not a live object with behaviour
User.new(
active: true,
paid_at: 2.months.ago
)
end
let(:users) { [bob] }
it 'emails the user' do
ExpiredUsers.for_users(users).should == [bob]
end
end
end
# Class responsible to only find expired users.
# Easy to test because:
# - Self contained! No dependencies.
# - Value IN, Value OUT
# The main responsability of this class is to make decisions.
class ExpiredUsers
def self.for_users(users)
users.select do |user|
user.active? && user.paid_at < 1.month.ago
end
end
end
# How to compose the UserDB the ExpiredUsers and the Mailer?
# We recreate the Sweeper class using the ExpiredUsers
# The main responsability of this class is to contain all the dependencies (composition)
class Sweeper
def self.sweep
ExpiredUsers.for_users(User.all).each do |user|
UserMailer.billing_problem(user)
end
end
end
# The Functional Core which contains things like the ExpireUsers class has lots of paths but few or none dependencies.
# A path is a possible code flow. Each each statement for example creates at least two paths.
# This is the place for unit tests since it is naturally isolated code that will originate simple tests.
# The Imperative Shell that contains classes like the Sweeper has lots of dependencies but very few paths
# which is exactly where integration is good at. They are good to test integration but very difficult to test many different paths.
# The Actor model seems to be the most generic of the concurrency models.
# Queue is the inbox of process 2
# Process 1 will send messages to it.
queue = Queue.new
# For process 1 we will fork a thread that goes in to an infinite loop, reads from the StandardIn and
# pushes it to the queue.
Thread.new { loop { queue.push(gets)} }
# For process 2 we will have a thread that forks a thread that will read from the queue and writes to StandardOut.
# This creates an echo program.
Thread.new { loop { puts(queue.pop) } }.join
# Every value in the system is a possible message between the processes.
# The actor pattern is simply a system where actors send messages to other actors inboxes.
# The more potential messages we have to send the more natural this form of concurrency is.
# This is a special case of "the value is the boundary". If a value is a boundery between two methods,
# it is also the boundery between classes, between sub systems, between processes.
# Lets convert the Sweeper class into the style.
# Original:
def sweep
expired_users(User.all).each do |user|
notify_of_billing_problem(user)
end
end
def expired_users(users)
users.select do |user|
user.active? && user.paid_at < 1.month.ao
end
end
def notify_of_billing_problem(user)
UserMailer.billing_problem(user)
end
# Modification:
# Step 1. Create make the Sweeper an Actor
# It will fetch everything from the DB and push the expired users one by one.
# Very unperformant, but it is just an example :)
actor Sweeper
User.all.each { |user| ExpiredUsers.push(user) }
die # die to prevent infinit loop.
end
# Step 2. Make the ExpireUsers Actor
# It will continually pop a user out of is inbox, makes decisions with the user state to
# see if it will send it to the UserMailer.
actor ExpiredUsers
user = pop
late = user.active? && user.paid_at < 1.month.ago
BillingProblemNotification.push(user) if late
end
# Step 3. Make the BillingProblemNotification actor
# This actor just pops a user and sends the email.
actor BillingProblemNotification
UserMailer.billing_problem(pop)
end
# This system is naturally parallel and it can use up to 3 cores.
# This was possible due ot the fact that the user value is easy to push around.
# Values afford shifting process boundaries but really they can shift any boundary: classes, methods etc.
# We now have Sweeper =user value=> ExpiredUsers =user value=> Mailer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment