Speech of Orban Botond, Ruby Developer at Toptal, at Ruby Meditation 27, Dnipro, 19.05.2019
Slideshare -
Next conference - http://www.rubymeditation.com/
Software development is a domain where everybody can make a beautiful sculpture or can quickly build an unsolvable maze. According to my observations the later happens more often unfortunately.
In the title for my presentation both the If Jungle and the Civilised Railway Station are methaphors representing the opposite ends of the scale of quality code.
In my presentation I am going to present my personal experiences on how to get out from the trap of the if jungle by making the code to adhere to the SRP and DRY principles. I am also going to show the advantages of the Railway Oriented Programing using the 3 different libraries.
The examples are going to be stereotypical errors, fun and easy to follow.
Announcements and conference materials https://www.fb.me/RubyMeditation
News https://twitter.com/RubyMeditation
Photos https://www.instagram.com/RubyMeditation
The stream of Ruby conferences (not just ours) https://t.me/RubyMeditation
5. Railway Oriented Development
With 3 libraries in Parallel!
-classic if jungle
-dry transactions
-monad do notation
-trailblazer operations
6. def create
user = User.create user_params[:user]
if user.valid?
package = Package.create package_params[:user]
if package.valid?
user.package = package
user.save
SmsService.send_registration_msg(user, package)
EmailService.send_registration_msg(user, package)
SystemNotifierService.send_registration_msg(user,
package)
else
render ...
end
else
render ...
end
end
True If Jungle
Cyclomatic complexity: 2
7. def create
user = User.create user_params[:user]
if user.valid?
package = Package.create package_params[:user]
if package.valid?
package.user = user
package.save
if params[:coupon].present?
if coupon = Coupon.exists? params[:coupon]
discount = Discount.create params[:coupon]
package.discount = discount
package.save
else
render …
end
end
SmsService.send_registration_msg(user, package)
EmailService.send_registration_msg(user, package)
SystemNotifierService.send_registration_msg(user, package)
else
render ...
end
else
render ...
end
end
True If Jungle
Cyclomatic complexity: 4
8. True If Jungle
def create
user = User.create user_params[:user]
if user.valid?
package = Package.create package_params[:user]
if package.valid?
package.user = user
package.save
if params[:coupon].present?
if Coupon.exists? params[:coupon]
discount = Discount.create params[:coupon]
if discount.allowsUser? user
package.discount = discount
package.save
else
render ...
end
else
render ...
end
end
SmsService.send_registration_msg(user, package)
EmailService.send_registration_msg(user, package)
SystemNotifierService.send_registration_msg(user, package)
else
render ...
end
else
render ...
end
end
Cyclomatic complexity: 5
11. If Jungle Implementation
Specs:
f(a,b) = a+b, a is infinite
context 'add' do
subject { add.call params: params }
context 'negative cases' do
context 'params infinite' do
let(:params) { [(1.0/0.0), 2] }
specify 'Be a failure with a proper error
message' do
expect(subject[:validation]).to eq 'must
be a real number'
end
end
end
12. add = ->(params:) do
return { validation: 'must be a real
number' } if params.any?{|x|x.infinite?}
end
Code:
If Jungle Implementation
f(a,b) = a+b, a is infinite
13. Specs:
context 'add' do
subject { add.call params: params }
let(:params) { [2,3,4] }
context 'negative cases' do …
context 'positive cases' do
specify 'Be a success with the proper correct
output' do
expect(subject[:operation_result]).to
eq(params.reduce(0) { |acc, x| acc + x })
end
end
end
If Jungle Implementation
f(a,b) = a+b,
14. Code:
add = ->(params:) do
return { validation: 'must be a real
number' } if params.any?{|x|x.infinite?}
result = params.reduce(0) { |acc, x| acc
+ x }
return { operation_result: result }
end
If Jungle Implementation
f(a,b) = a+b,
15. linear_function = ->(params:) do
result = multiply.call(params: params[-2..-1])
if(result[:operation_result])
return add.call(params:
[result[:operation_result], params[0]])
else
return result
end
end
rspec spec/railway_oriented_development/if_jungle_spec.rb
.......
7 examples, 0 failures
If Jungle Implementation
f(a,b,x) = a.x+b, Linear function
16. Building Blocks
Add Operation
-Guard Condition
-Business Logic: +
Multiply Operation
-Guard Condition
-Business Logic: *
LinearFunction
-Delegation
-Conditional
-Delegation
-Return a Result
If Jungle Implementation
18. Specs:
context 'add' do
subject { DryTransactions::Add.new.call params }
context 'negative cases' do
context 'params infinite' do
let(:params) { {params:[(1.0/0.0), 2]} }
specify 'Be a failure with a proper error
message' do
expect(subject).to be_failure
expect(subject.failure).to eq 'must be a
real number'
end
end
end
Dry Transactions
f(a,b) = a+b, a is infinite
21. Code:
module DryTransactions
class Add
…
private
def validate(input)
return Failure('must be a real number') unless input.all?{|x|
x.finite?}
Success(input)
end
def add(input)
ret = input.reduce(0) { |acc, x| acc + x }
Success(ret)
end
…
Dry Transactions
f(a,b) = a+b, a is infinite
26. Specs:
context 'add' do
subject { MonadDoNotation::Add.new.call params }
context 'negative cases' do
context 'params infinite' do
let(:params) { [(1.0/0.0), 2] }
specify 'Be a failure with a proper error
message' do
expect(subject).to be_failure
expect(subject.failure[:validation]).to eq
'must be a real number'
end
end
end
Monad Do Notation
f(a,b) = a+b, a is infinite
27. Code:
class Add
include Dry::Monads::Result::Mixin
include Dry::Monads::Do::All
def call(arguments)
validation_result = yield validate(arguments)
operation_result = yield add(arguments)
Success validation_result.merge( operation_result)
end
…
end
Monad Do Notation
f(a,b) = a+b, a is infinite
28. Code:
class Add
…
def validate(input)
return Failure(validation: 'must be a real
number') unless input.all?{|x|x.finite?}
Success(validation: :ok)
end
def add(input)
ret = input.reduce(0) { |acc, x| acc + x }
Success(operation_result: ret)
end
end
…
Monad Do Notation
f(a,b) = a+b, a is infinite
29. class LinearOperation
include Dry::Monads::Result::Mixin
include Dry::Monads::Do::All
def call(input)
multiplication = yield
multiply(input[-2..-1])
addition = yield add([input[0],
multiplication[:operation_result]])
Success(addition)
end
private …
Monad Do Notation
f(a, b, x) = a.x+b,
30. class LinearOperation
include Dry::Monads::Result::Mixin
include Dry::Monads::Do::All
…
private
def multiply(args)
Multiply.new.call args
end
def add(args)
Add.new.call args
end
end
rspec spec/railway_oriented_development/monad_do_notation_spec.rb
.......
7 examples, 0 failures
Monad Do Notation
f(a, b, x) = a.x+b,
33. Specs:
context 'add' do
subject { TrailblazerOperations::Add.call params: params }
context 'negative cases' do
context 'params infinite' do
let(:params) { [(1.0/0.0), 2] }
specify 'Be a failure with a proper error message' do
expect(subject).to be_failure
expect(subject[:validation]).to eq 'must be a real
number'
end
end
end
Trailblazer Operations
f(a,b) = a+b, a is infinite
36. Code:
module TrailblazerOperations
class Add < Trailblazer::Operation
…
private
def validate(options, params:)
unless params.all?{|x|x.finite?}
options[:validation] = 'must be a real number'
return Railway.fail!
end
Railway.pass!
end
def add(options, params:, **rest)
ret = params.reduce(0) { |acc, x| acc + x }
options[:operation_result] = ret
end
end
end
end
37. Code:
class LinearOperation < Trailblazer::Operation
step Nested( Multiply,
input: -> (options, params:, **) do
options.merge params: params[-2..-1]
end
)
step Nested( Add,
input: -> (options, params:, **) do
options.to_hash.except(:operation_result).merge
params: [params[0], options[:operation_result]]
end
)
end
rspec spec/railway_oriented_development/trailblazer_operations_spec.rb
.......
7 examples, 0 failures
Trailblazer Operations
f(a, b, x) = a.x+b,
38. Add Operation
-Validate Step
-Business Logic Step: +
Multiply Operation
-Validate Step
-Business Logic Step: *
LinearFunction
-Delegates to the Multiply Operation by Nesting
-Delegates to the Add Operation by Nesting
Note:
-no conditional compared to the If Jungle Solution!
-linear execution!
-DSL for reuse!
Trailblazer Implementation
39. Dry-Transaction Monad Do Notation Trailblazer
Steps Steps
Ruby Code Wrapped
With Yield
Steps
Code Reuse Ruby Call
Ruby Code Wrapped
With Yield
DSL for other
Operation Reuse!
40. True If Jungle
def create
user = User.create user_params[:user]
if user.valid?
package = Package.create package_params[:user]
if package.valid?
package.user = user
package.save
if params[:coupon].present?
if Coupon.exists? params[:coupon]
discount = Discount.create params[:coupon]
if discount.allowsUser? user
package.discount = discount
package.save
else
render ...
end
else
render ...
end
end
SmsService.send_registration_msg(user, package)
EmailService.send_registration_msg(user, package)
SystemNotifierService.send_registration_msg(user, package)
else
render ...
end
else
render ...
end
end
Cyclomatic complexity: 5
41. Railway Oriented Development
Cyclomatic complexity: 1-2
class Add < Trailblazer::Operation
step :persist_user
step :persist_package
step :add_coupon_based_discount
step :notify_about_registration
...
end
42. Railway Oriented Development
Cyclomatic complexity: 1-2
class Add < Trailblazer::Operation
step :persist_user
failure :log_user_persistance
step :persist_package
failure :log_package_persistance
step :add_coupon_based_discount
failure :log_discount_creation
step :notify_about_registration
...
end
43. Railway Oriented Development
Cyclomatic complexity: 1-2
class Add < Trailblazer::Operation
step :persist_user
failure :log_user_persistance
step :persist_package
failure :log_package_persistance
step :add_coupon_based_discount
failure :log_discount_creation
step :notify_about_registration
step :notify_facebook_friends
…
end
44. Contract for every input (entity & other use case)
Basement:
-Operations for CRUD
Crud (and Other) Reuse (DRY):
-Operations for Onboarding
-Operations for Admin
-Operations for handling Business Use Cases
Trailblazer Operations & Contracts (Reform)
My Best Practice
45.
46.
47.
48.
49. Thank you ;)
Botond Orban Enthusiast IT Guy, Architect
Enthusiastic about Ruby
https://github.com/orbanbotond
@orbanbotond