How programming in other languages 
made my Ruby better (RubyConf.ph 2017)

How programming in other languages 
made my Ruby better (RubyConf.ph 2017)

Learning new programming languages it’s like approaching new cultures: it will largely enrich your knowledge and leverage your skills. New programming languages will not only made you a better software developer in general, but they will also help you to write better Ruby code.

This talk will provide you real world examples of Ruby code evolution, using lessons learned from other languages. We’ll see how functional language paradigms like immutability can be used to improve the quality of Ruby code, simplify unit testing and reduce side effects. We’ll also learn how more strict languages like Go can help us to properly plan a refactoring using dependency injection or simplify our Ruby methods by helping us to take the correct design decisions.

99e0b39c091e10d9c7d4452a34ca52dc?s=128

Simone Carletti

March 18, 2017
Tweet

Transcript

  1. How programming in other languages
 made my Ruby code be7er

    Simone Carle, / / @weppos
  2. None
  3. None
  4. None
  5. RubyConf.ph 2016

  6. None
  7. None
  8. "Emerging" languages Lua Kotlin Clojure Erlang Haskell Elixir Go Rust

    Swi> Julia
  9. My influences Go Elixir Java Ruby Crystal Prolog

  10. Improving Ruby Code one line at )me

  11. Structs to the rescue

  12. class Contact
 def refresh
 # fetches the contact from somewhere


    # and refreshes the state of the object
 end
 end
 
 c = Contact.new(id: 10)
 c.do_something
 c.refresh

  13. module Registrar
 class Domain
 def self.find(domain_name)
 resp = Registrar::Client.get("/domain/#{domain_name}")
 new(resp)


    end
 
 def initialize(resp)
 load_attributes(id: resp.id, name: resp.name)
 end
 
 def renew(period = 1)
 Registrar::Client.post("/renew/#{name}", ...) refresh
 end
 
 def refresh
 resp = Registrar::Client.get("/domains/#{name}")
 load_attributes(id: resp.id, name: resp.name)
 end
 
 # ...
 end
 end
  14. module Registrar
 class Domain
 def self.find(domain_name)
 resp = Registrar::Client.get("/domain/#{domain_name}")
 new(resp)


    end
 
 def initialize(resp)
 load_attributes(id: resp.id, name: resp.name)
 end
 
 def renew(period = 1)
 Registrar::Client.post("/renew/#{name}", ...) refresh
 end
 
 def refresh
 resp = Registrar::Client.get("/domains/#{name}")
 load_attributes(id: resp.id, name: resp.name)
 end
 
 # ...
 end
 end
  15. module Registrar
 class Domain
 def self.find(domain_name)
 resp = Registrar::Client.get("/domain/#{domain_name}")
 new(resp)


    end
 
 def initialize(resp)
 load_attributes(id: resp.id, name: resp.name)
 end
 
 def renew(period = 1)
 Registrar::Client.post("/renew/#{name}", ...) refresh
 end
 
 def refresh
 resp = Registrar::Client.get("/domains/#{name}")
 load_attributes(id: resp.id, name: resp.name)
 end
 
 # ...
 end
 end
  16. module Registrar
 class Domain
 def self.find(domain_name)
 resp = Registrar::Client.get("/domain/#{domain_name}")
 new(resp)


    end
 
 def initialize(resp)
 load_attributes(id: resp.id, name: resp.name)
 end
 
 def renew(period = 1)
 Registrar::Client.post("/renew/#{name}", ...)
 refresh
 end
 domain = Registrar::Domain.find("example.com")
 domain.renew(1)
  17. Did we forgot that Ruby has Structs?

  18. module Registrar
 class Domain
 def self.find(domain_name)
 resp = Registrar::Client.get("/domains/#{domain_name}")
 new(resp)


    end
 
 def self.renew(domain_name, years = 1)
 resp = Registrar::Client.post("/renew/#{domain_name}", ...)
 new(resp)
 end
 def initialize(resp) # ...
  19. module Registrar
 class Domain
 def self.find(domain_name)
 resp = Registrar::Client.get("/domains/#{domain_name}")
 Structs::Domain.new(id:

    resp.id, name: resp.name)
 end
 
 def self.renew(domain_name, years = 1)
 resp = Registrar::Client.post("/renew/#{domain_name}", ...)
 Structs::Domain.new(id: resp.id, name: resp.name)
 end
 end
 
 module Structs
 class Domain
 def initialize(attributes = {})
 def assign_attributes(attributes)
 end
 end
 end
  20. None
  21. Ŏ Separate data from execuJon logic Ŏ No state changes,

    means very loose coupling between the data and the operaJons
  22. Stateless business logic

  23. class Account < ActiveRecord::Base
 def score!
 # contact service for

    fraud score and recalculate fraud score
 end
 end
 
 class Order < ActiveRecord::Base
 def initialize(account)
 @account = account
 @items = []
 end
 
 def add_item(items)
 @items << items
 end
 
 def process!
 account.score!
 compute_total
 charge_account
 mark_as_completed
 notify_account
 end
 end
  24. class Account < ActiveRecord::Base
 def score!
 # contact service for

    fraud score and recalculate fraud score
 end
 end
 
 class Order < ActiveRecord::Base
 def initialize(account)
 @account = account
 @items = []
 end
 
 def add_item(items)
 @items << items
 end
 
 def process!
 account.score!
 compute_total
 charge_account
 mark_as_completed
 notify_account
 end
 end
  25. class Account < ActiveRecord::Base
 def score!
 # contact service for

    fraud score
 value = call_some_service_and_handle_errors
 # and recalculate fraud score
 update_attribute(score, value)
 end
 end
  26. class Account < ActiveRecord::Base
 def score!
 # contact service for

    fraud score
 value = call_some_service_and_handle_errors
 # and recalculate fraud score
 update_score(value)
 end
 
 def update_score(value)
 update_attribute(score, value)
 end
 end

  27. class Account < ActiveRecord::Base
 def score!
 # contact service for

    fraud score
 value = call_some_service_and_handle_errors
 # and recalculate fraud score
 update_score(value)
 score_account(account: self)
 end
 
 def update_score(value)
 update_attribute(score, value)
 end
 end
 
 class AccountService
 def initialize(*)
 end
 
 def score_account(account:)
 end
 end

  28. class Account < ActiveRecord::Base
 def score!
 score_account(account: self)
 end
 


    def update_score(value)
 update_attribute(score, value)
 end
 end
 
 class AccountService
 def initialize(*)
 end
 
 def score_account(account:)
 value = call_some_service_and_handle_errors
 account.update_score(value)
 end
 end

  29. class Account < ActiveRecord::Base
 def update_score(value) # ...
 end
 


    class AccountService
 def initialize(*) # ...
 def score_account(account:) # ...
 end
 
 class Order < ActiveRecord::Base
 def initialize(account) # ...
 def add_item(items) # ...
 def process!
 AccountService.new.score_account(account: account)
 compute_total
 charge_account
 mark_as_completed
 notify_account
 end
 end

  30. class Order < ActiveRecord::Base
 def initialize(account) # ...
 def add_item(items)

    # ...
 def process!
 AccountService.new.score_account(account: account)
 compute_total
 charge_account
 mark_as_completed
 notify_account
 end
 end

  31. class Order < ActiveRecord::Base
 def initialize(account) # ...
 def add_item(items)

    # ...
 def process!
 AccountService.new.score_account(account: account)
 compute_total
 charge_account
 mark_as_completed
 notify_account
 OrderService.new.process_order(order: self)
 end
 end
 
 class OrderService
 def initialize(*)
 end
 
 def process_order(order:)
 end
 end

  32. class Order < ActiveRecord::Base
 def initialize(account) # ...
 def add_item(items)

    # ...
 def process!
 OrderService.new.process_order(order: self)
 end
 end
 
 class OrderService
 def initialize(*)
 end
 
 def process_order(order:)
 AccountService.new.score_account(account: order.account)
 order.compute_total
 order.charge_account
 order.mark_as_completed
 order.notify_account
 end
 end

  33. class Order < ActiveRecord::Base
 def initialize(account) # ...
 def add_item(items)

    # ...
 def process!
 OrderService.new.process_order(order: self)
 end
 end
 
 class OrderService
 def initialize(account_service: AccountService.new)
 @account_service = account_service
 end
 
 def process_order(order:)
 @account_service.score_account(account: order.account)
 order.compute_total
 order.charge_account
 order.mark_as_completed
 order.notify_account
 end
 end

  34. class OrderController
 def create
 new_order = Order.new(current_account)
 new_order.add_items(params[:items])
 
 new_order

    = OrderService.new.process_order(order: new_order)
 end
 end

  35. class Account < ActiveRecord::Base
 def update_score(value) # ...
 end
 


    class Order < ActiveRecord::Base
 def initialize(account) # ...
 def add_item(items) # ...
 end

  36. class OrderService
 def initialize(account_service: nil, payment_service: nil) # ...
 def

    process_order(order:) # ...
 
 private
 
 def finalize_order(order) # ...
 end
 
 class AccountService
 def initialize(*) # ...
 def score_account(account:) # ...
 def notify_account(account:) # ...
 end
 
 class PaymentService
 def initialize(*) # ...
 def charge_account(account:, processor: nil) # ...
 end
  37. Ŏ Reduce side effects caused by states,
 improving test effecJveness

    and code maintainability Ŏ Simplify tests by reducing context setup Ŏ Slim down model objects removing third-party interacJons Ŏ Expose the business logic in a centralized place,
 with clear and easy to test methods
  38. FuncGonal-like

  39. class OrdersController
 def create
 new_order = Order.new(current_account)
 new_order.add_items(params[:items])
 
 new_order

    = Registry(:order_service).process_order(order: new_order)
 end
 end

  40. class OrdersController
 def create
 new_order = Order.new(current_account)
 new_order.add_items(params[:items])
 
 new_order

    = Registry(:order_service).process_order(order: new_order)
 end
 end

  41. class OrdersController
 def create
 new_order = Order.new(current_account)
 new_order.add_items(params[:items])
 
 #

    change state of new_order inside the OrderService
 Registry(:order_service).process_order(order: new_order)
 end
 end

  42. describe "GET create" do
 it "handles a successful request" do


    expect(Registry(:order_service)).to receive(:process_order).
 with(...).
 and_return(...)
 
 post :create, account_id: @_account.to_param, ...
 end
 end

  43. describe "GET create" do
 it "handles a successful request" do


    expect(Registry(:order_service)).to receive(:process_order).
 with(...).
 and_return(...) # ?!?
 
 post :create, account_id: @_account.to_param, ...
 
 # how can I set controller or view expectations # that depends on processing applied to new_order from the service?
 end
 end

  44. describe "GET create" do
 let(:created_order) { Order.new(id: 10, items: ...,

    ...) }
 
 it "handles a successful request" do
 expect(Registry(:order_service)).to receive(:process_order).
 with(...).
 and_return(created_order)
 
 post :create, account_id: @_account.to_param, ...
 
 # how can I set controller or view expectations # that depends on processing applied to new_order from the service?
 end
 end

  45. describe "GET create" do
 let(:created_order) { Order.new(id: 10, items: ...,

    ...) }
 
 it "handles a successful request" do
 expect(Registry(:order_service)).to receive(:process_order).
 with(...).
 and_return(created_order)
 
 post :create, account_id: @_account.to_param, ...
 
 expect(response).to redirect_to(
 order_url(@_account, order_id: created_order.id)
 )
 end
 end

  46. Ŏ Simplify tests by being able to bePer isolate dependencies

    Ŏ Improved unit tesJng
 (use integraJon tests for comprehensive coverage) Ŏ Speed up tests by not having to test unnecessary code due to internal state changes
  47. Dependency injecGon

  48. class CoffeeMaker
 def initializer(company)
 @company = company
 end
 
 def

    prepare_espresso
 begin
 machine.prepare(@company, :espresso)
 rescue CoffeeMachine::CoffeeIsFinished
 machine.grind_coffee_beans(@company)
 retry
 end
 end
 
 def balance
 machine.balance(@company)
 end
 
 private
 
 def machine
 @machine ||= CoffeeMachine.connect("api.dnsimple.coffee")
 end
 end
  49. class CoffeeMaker
 def initializer(company)
 @company = company
 end
 
 def

    prepare_espresso
 begin
 machine.prepare(@company, :espresso)
 rescue CoffeeMachine::CoffeeIsFinished
 machine.grind_coffee_beans(@company)
 retry
 end
 end
 
 def balance
 machine.balance(@company)
 end
 
 private
 
 def machine
 @machine ||= CoffeeMachine.connect("api.dnsimple.coffee")
 end
 end
  50. RSpec.describe CoffeeMaker do
 subject { described_class.new("company") }
 
 describe "#prepare_espresso"

    do
 it "creates and returns an espresso" do
 expect_any_instance_of(CoffeeMachine)
 .to receive(:prepare)
 .with("company", :espresso)
 .and_return(returned = Object.new)
 
 expect(subject.prepare_espresso).to be(returned)
 end
  51. class CoffeeMaker
 def initialize(company)
 @company = company
 end
 
 def

    prepare_espresso
 machine.prepare(@company, :espresso)
 rescue CoffeeMachine::CoffeeIsFinished
 machine.grind_coffee_beans(@company)
 retry
 end
 
 def balance
 machine.balance(@company)
 end
 
 private
 
 def machine
 @machine ||= CoffeeMachine.connect("api.dnsimple.coffee")
 end
 end
  52. class CoffeeMaker
 def initialize(company, machine: CoffeeMachine.connect("api.dnsimple.coffee"))
 @company = company
 @machine

    = machine
 end
 
 def prepare_espresso
 machine.prepare(@company, :espresso)
 rescue CoffeeMachine::CoffeeIsFinished
 machine.grind_coffee_beans(@company)
 retry
 end
 
 def balance
 machine.balance(@company)
 end
 
 private
 
 def machine
 @machine
 end
 end
  53. RSpec.describe CoffeeMaker do
 subject { described_class.new("company") }
 let(:machine) { CoffeeMachine.new

    }
 
 describe "#prepare_espresso" do
 it "creates and returns an espresso" do
 expect(subject).to receive(:machine).and_return(machine)
 
 expect(machine)
 .to receive(:prepare)
 .with("company", :espresso)
 .and_return(returned = Object.new)
 
 expect(subject.prepare_espresso).to be(returned)
 end

  54. RSpec.describe CoffeeMaker do
 subject { described_class.new("company", machine: machine) }
 let(:machine)

    { CoffeeMachine.new }
 
 describe "#prepare_espresso" do
 it "creates and returns an espresso" do
 expect(subject).to receive(:machine).and_return(machine)
 
 expect(machine)
 .to receive(:prepare)
 .with("company", :espresso)
 .and_return(returned = Object.new)
 
 expect(subject.prepare_espresso).to be(returned)
 end

  55. RSpec.describe CoffeeMaker do
 subject { described_class.new("company", machine: machine) }
 let(:machine)

    { TestCoffeeMachine.new }
 
 describe "#execute_something" do
 it "creates and returns something else" do
 expect(subject.execute_something).to eq("else")
 end
 end
 end
 class TestCoffeeMachine
 def prepare(*)
 end
 def grind_coffee_beans(*)
 end
 end

  56. class CoffeeMaker
 def initialize(company, machine: CoffeeMachine.connect("api.dnsimple.coffee"))
 @company = company
 @machine

    = machine
 end
 
 def prepare_espresso # ...
 def balance # ...
 
 private
 
 def machine # ...
 end
 
 CoffeeMaker.new("company")
  57. class CoffeeMaker
 def initialize(company, machine:)
 @company = company
 @machine =

    machine
 end
 
 def prepare_espresso # ...
 def balance # ...
 
 private
 
 def machine # ...
 end
 
 CoffeeMaker.new("company", machine: CoffeeMachine.connect("api.dnsimple.coffee"))
  58. Ŏ Improve decoupling between resources Ŏ Simplify tests by reducing

    unnecessary setup or code Ŏ Increase test effecJveness by reducing mock objects
  59. Powerful error handling

  60. def authorize(...)
 begin
 le = Letsencrypt.new("sandbox")
 le.authorize(...)
 rescue Letsencrypt::Error =>

    error
 # do something with error
 end
 end LetsencryptController.issue CertificateLetsencryptNewissueRequestCommand.execute
 CertificateService.issue_certificate
 CertificateService.authorize
 ...
  61. class Letsencrypt
 
 # Base error for Letsencrypt
 class Error

    < StandardError
 end
 
 class DomainValidationError < Error
 end
 def initialize(environment:, adapter: DatabaseAdapter.new)
 @environment = environment.to_str
 ENVIRONMENTS.include?(@environment) or
 raise ArgumentError, "Invalid Letsencrypt env `#{@environment}`"
 # ...
 end
  62. class Letsencrypt
 
 # Base error for Letsencrypt
 class Error

    < StandardError
 end
 
 class DomainValidationError < Error
 end

  63. class Letsencrypt
 
 # Error tag for Letsencrypt.
 module Error


    end
 
 # Base error for Letsencrypt.
 class LetsencryptError < StandardError
 include Error
 end
 
 class DomainValidationError < LetsencryptError
 end
  64. class Letsencrypt
 
 # Error tag for Letsencrypt.
 module Error


    end
 
 # Base error for Letsencrypt.
 class LetsencryptError < StandardError
 include Error
 end
 
 class DomainValidationError < LetsencryptError
 end 
 # Raised when the arguments are wrong.
 class ArgumentError < ::ArgumentError
 include Error
 end
  65. class Letsencrypt
 
 def initialize(environment:, adapter: DatabaseAdapter.new)
 @environment = environment.to_str


    ENVIRONMENTS.include?(@environment) or
 raise ArgumentError, " ... `#{@environment}`"
 # ...
 end
  66. class Letsencrypt
 
 def initialize(environment:, adapter: DatabaseAdapter.new)
 @environment = environment.to_str


    ENVIRONMENTS.include?(@environment) or
 raise Letsencrypt::ArgumentError, " ... `#{@environment}`"
 # ...
 end
  67. def authorize(...)
 begin
 le = Letsencrypt.new("sandbox")
 le.authorize(...)
 rescue Letsencrypt::LetsencryptError =>

    error
 # do something with base error
 rescue Letsencrypt::Error => error
 # do something with any error
 end
 end
  68. class CustomLibrary
 class Error < StandardError
 end
 
 def execute


    this_will_fail
 rescue Timeout::Error
 raise Error, "a timeout occurred"
 end
 
 # ...
 end
  69. None
  70. begin
 lib = CustomLibrary.new
 lib.execute
 rescue => error
 puts "Error:"


    puts " #{error.class}: #{error.message}"
 cause = error.cause
 puts "Caused by:"
 puts " #{cause.class}: #{cause.message}"
 end
 
 Error:
 CustomLibrary::Error: a timeout occurred
 Caused by:
 Timeout::Error: something took long

  71. Ŏ Facilitate error handling by wrapping all the excepJons a

    library can raise into the library namespace Ŏ Repackage excepJons, sJll giving access to the original error and without overriding backtrace
  72. Programming without defaults

  73. What would you do if Ruby would drop default parameters

    … tomorrow?
  74. def execute(task, category = "default", action = "index")
 puts "Task

    #{task}: #{category}/#{action}"
 end
 
 execute("task 1")
 execute("task 2", "cat2")
 execute("task 3", "cat3", "create")
 execute("task 11", default?, "create")
  75. Go has no default parameters

  76. func ListDomains(account string, options *DomainListOptions) (*DomainsResponse, error) {
 //
 //


    }

  77. func ListDomains(account string, options *DomainListOptions) (*DomainsResponse, error) {
 //
 //


    }
 
 type DomainListOptions struct {
 NameLike string `url:"name_like,omitempty"`
 RegistrantID int `url:"registrant_id,omitempty"`
 
 ListOptions
 }

  78. func ListDomains(account string, options *DomainListOptions) (*DomainsResponse, error) {
 //
 //


    }
 
 type DomainListOptions struct {
 NameLike string `url:"name_like,omitempty"`
 RegistrantID int `url:"registrant_id,omitempty"`
 
 ListOptions
 }
 
 type ListOptions struct {
 Page int `url:"page,omitempty"`
 PerPage int `url:"per_page,omitempty"`
 Sort string `url:"sort,omitempty"`
 }
  79. client.Domains.ListDomains("1010", nil)
 
 listOptions := &DomainListOptions{ListOptions{Page: 2, PerPage: 20}}
 client.Domains.ListDomains("1010",

    listOptions)
 
 listOptions := &DomainListOptions{}
 listOptions.Page = 2
 listOptions.PerPage = 20
 client.Domains.ListDomains("1010", listOptions)

  80. Value of simplicity

  81. Simplicity is complicated Preferable to have just one way, or

    at least fewer, simpler ways. Features add complexity. We want simplicity. Features hurt readability. We want readability. Readability is paramount. Rob Pike dotGo 2015 hPps:/ /www.youtube.com/watch?v=rFejpH_tAHM
  82. Limited use of alias def update_registry_name_servers(name_servers)
 # ...
 end 


    alias update_registry_nameservers update_registry_name_servers
 alias registry_name_servers= update_registry_name_servers
 
 def refresh_name_server_status
 # ...
 end 
 alias refresh_nameserver_status refresh_name_server_status
  83. Single, Simple, Clear API module Dnsimple
 module Services
 
 #

    DomainRegistrarService is the service responsible for the Domain operations
 # that involves registration, transfer or registrar-related actions.
 #
 # Note that these methods don't simply change data at the registrar. Occasionally,
 # they can also change data or states in the domain object, and return it.
 class DomainRegistrarService
 
 # Unlocks the domain at the registry.
 #
 # @param [Domain] domain
 # @return [Domain]
 # @raise [Registrar::Error]
 def unlock_domain(domain:)
 
 # Performs a check of the domain at the registry.
 #
 # @see Registrar::Domain.check_domain
 # @param domain [Domain]
 # @return [Registrar::Structs::Availability] containing the result of the check
 # @raise [Registrar::Error]
 def check_domain(domain:)
  84. None
  85. None
  86. None
  87. None
  88. Thanks! DNSimple dnsimple.com @dnsimple Simone CarleR simonecarle,.com @weppos