Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Simone Carletti

March 18, 2017
Tweet

More Decks by Simone Carletti

Other Decks in Programming

Transcript

  1. How programming in other languages

    made my Ruby code be7er
    Simone Carle, /
    / @weppos

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. RubyConf.ph
    2016

    View Slide

  6. View Slide

  7. View Slide

  8. "Emerging" languages
    Lua
    Kotlin
    Clojure
    Erlang
    Haskell
    Elixir
    Go
    Rust
    Swi>
    Julia

    View Slide

  9. My influences
    Go
    Elixir
    Java
    Ruby
    Crystal
    Prolog

    View Slide

  10. Improving Ruby Code
    one line at )me

    View Slide

  11. Structs to the rescue

    View Slide

  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


    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  17. Did we forgot that Ruby has Structs?

    View Slide

  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) # ...

    View Slide

  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

    View Slide

  20. View Slide

  21. Ŏ Separate data from execuJon logic
    Ŏ No state changes, means very loose coupling between
    the data and the operaJons

    View Slide

  22. Stateless business logic

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  35. class Account < ActiveRecord::Base

    def update_score(value) # ...

    end


    class Order < ActiveRecord::Base

    def initialize(account) # ...

    def add_item(items) # ...

    end


    View Slide

  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

    View Slide

  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

    View Slide

  38. FuncGonal-like

    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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

    View Slide

  47. Dependency injecGon

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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


    View Slide

  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


    View Slide

  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


    View Slide

  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")

    View Slide

  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"))

    View Slide

  58. Ŏ Improve decoupling between resources
    Ŏ Simplify tests by reducing unnecessary setup or code
    Ŏ Increase test effecJveness by reducing mock objects

    View Slide

  59. Powerful error handling

    View Slide

  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

    ...

    View Slide

  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

    View Slide

  62. class Letsencrypt


    # Base error for Letsencrypt

    class Error < StandardError

    end


    class DomainValidationError < Error

    end


    View Slide

  63. class Letsencrypt


    # Error tag for Letsencrypt.

    module Error

    end


    # Base error for Letsencrypt.

    class LetsencryptError < StandardError

    include Error

    end


    class DomainValidationError < LetsencryptError

    end

    View Slide

  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

    View Slide

  65. class Letsencrypt


    def initialize(environment:, adapter: DatabaseAdapter.new)

    @environment = environment.to_str

    ENVIRONMENTS.include?(@environment) or

    raise ArgumentError, " ... `#{@environment}`"

    # ...

    end

    View Slide

  66. class Letsencrypt


    def initialize(environment:, adapter: DatabaseAdapter.new)

    @environment = environment.to_str

    ENVIRONMENTS.include?(@environment) or

    raise Letsencrypt::ArgumentError, " ... `#{@environment}`"

    # ...

    end

    View Slide

  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

    View Slide

  68. class CustomLibrary

    class Error < StandardError

    end


    def execute

    this_will_fail

    rescue Timeout::Error

    raise Error, "a timeout occurred"

    end


    # ...

    end

    View Slide

  69. View Slide

  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


    View Slide

  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

    View Slide

  72. Programming without defaults

    View Slide

  73. What would you do if Ruby
    would drop default parameters
    … tomorrow?

    View Slide

  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")

    View Slide

  75. Go has no default parameters

    View Slide

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

    //

    //

    }


    View Slide

  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

    }


    View Slide

  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"`

    }

    View Slide

  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)


    View Slide

  80. Value of simplicity

    View Slide

  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

    View Slide

  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

    View Slide

  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:)

    View Slide

  84. View Slide

  85. View Slide

  86. View Slide

  87. View Slide

  88. Thanks!
    DNSimple
    dnsimple.com
    @dnsimple
    Simone CarleR
    simonecarle,.com
    @weppos

    View Slide