Last active
September 29, 2017 16:21
-
-
Save victormartins/91bb0158d0db0e3e252c9a5640a620b2 to your computer and use it in GitHub Desktop.
Learning Examples
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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