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

How programming in other languages 
made my Ruby code better (RubyDay 2016)

How programming in other languages 
made my Ruby code better (RubyDay 2016)

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.

Simone Carletti

November 25, 2016
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 full-size slide

  2. Men9oned at
    TropicalRuby 2015
    RubyConf.ph 2016
    RailsConf 2016

    View full-size slide

  3. "Tradi9onal" languages
    C
    C++
    C#
    Java
    Ruby
    Python
    JavaScript
    Perl

    View full-size slide

  4. "Emerging" languages
    Lua
    Kotlin
    Clojure
    Erlang
    Haskell
    Elixir
    Go
    Rust
    SwiO
    Julia

    View full-size slide

  5. Bridges
    jRuby

    hPp:/
    /jruby.org/
    Helix

    hPp:/
    /blog.skylight.io/introducing-helix/
    Erjang

    hPps:/
    /github.com/trifork/erjang

    View full-size slide

  6. hPp:/
    /githut.info/

    View full-size slide

  7. Programming Language Paradigms
    Object-Oriented

    View full-size slide

  8. Programming Language Paradigms
    Object-Oriented
    FuncVonal

    View full-size slide

  9. Programming Language Paradigms
    Object-Oriented
    FuncVonal
    Procedural

    View full-size slide

  10. My influences
    Elixir
    Go
    Java
    Ruby
    Crystal
    Prolog

    View full-size slide

  11. 3 languages for Rubyist

    View full-size slide

  12. Features
    • Simplicity and Readability
    • Concurrency
    • ComposiVon
    • Good tooling and ecosystem support
    • Fast builds
    • Excellent network libraries
    • High quality standard libs

    View full-size slide

  13. package dnsimple


    import (

    "encoding/json"

    "github.com/google/go-querystring/query"

    )


    const libraryVersion = "0.13.0"


    type Client struct {

    HttpClient *http.Client

    Credentials Credentials

    }


    func (c *Client) get(path string, obj interface{}) (*http.Response, error) {

    req, err := c.NewRequest("GET", path, nil)

    if err != nil {

    return nil, err

    }


    return c.Do(req, obj)

    }


    func (c *Client) post(path string, payload, obj interface{}) (*http.Response, error) {

    req, err := c.NewRequest("POST", path, payload)

    if err != nil {

    return nil, err

    }


    return c.Do(req, obj)

    }

    View full-size slide

  14. Go for the Rubyist
    • Excellent replacement for network operaVons or
    background tasks
    • APenVon to details and language expressiveness
    • Real concurrency
    • Excellent at manipulaVng JSON
    • Good soluVon for small web services
    • It will drasVcally change your percepVon of code quality

    View full-size slide

  15. func (c *Client) NewRequest(method, path string, payload interface{}) (*http.Request, error) {

    url := c.BaseURL + path


    body := new(bytes.Buffer)

    if payload != nil {

    err := json.NewEncoder(body).Encode(payload)

    if err != nil {

    return nil, err

    }

    }


    req, err := http.NewRequest(method, url, body)

    if err != nil {

    return nil, err

    }


    req.Header.Set("Content-Type", "application/json")

    req.Header.Add("Accept", "application/json")

    req.Header.Add("User-Agent", formatUserAgent(c.UserAgent))

    for key, value := range c.Credentials.Headers() {

    req.Header.Add(key, value)

    }


    return req, nil

    }

    View full-size slide

  16. Features
    • Concurrency
    • PaPern matching
    • Data Immutability
    • Excellent tools
    • Interoperability with Erlang
    • Binary manipulaVon
    • Iex, Mix

    View full-size slide

  17. defmodule Error do

    defexception message: "default message"

    end


    defmodule Client do

    @default_base_url "https://api.dnsimple.com"


    defstruct access_token: nil, base_url: @default_base_url, user_agent: nil


    def options do

    ~w(access_token base_url user_agent username password)a

    end


    def sum_odd_numbers(limit) do

    1..limit |> Enum.filter(fn(x) -> rem(x,2)== 1 end) |> Enum.sum

    end


    defp process_request_body(headers, nil), do: {headers, []}

    defp process_request_body(headers, body) do

    case get_header(headers, "Accept") do

    {_, "application/json"} -> {Map.put(headers, "Content-Type", @json), Poison.encode!(body)}

    _ -> {headers, body}

    end

    end


    defp url(%Client{base_url: base_url}, path) do

    base_url <> path

    end

    View full-size slide

  18. Elixir for the Rubyist
    • Syntax very close to Ruby
    • Phoenix
    • Iex, Mix
    • Community composed by a lot of ex-Ruby developers
    • Short learning curve (apart from funcVonal paradigm)

    View full-size slide

  19. Features
    • Fast
    • Type inference
    • Direct binding with C
    • Compile-Vme evaluaVon and generaVon of code

    View full-size slide

  20. class HTTP::Server

    @wants_close = false


    def self.new(port, handlers : Array(HTTP::Handler))

    new("127.0.0.1", port, handlers)

    end


    def self.new(port, handler)

    new("127.0.0.1", port, handler)

    end


    def initialize(@host : String, @port : Int32, &handler : Context ->)

    @processor = RequestProcessor.new(handler)

    end


    def port

    if server = @server

    server.local_address.port.to_i

    else

    @port

    end

    end

    View full-size slide

  21. require "http/server"


    server = HTTP::Server.new "0.0.0.0", 8080 do |context|

    context.response.headers["Content-Type"] = "text/plain"

    context.response.print("Hello world!")

    end


    puts "Listening on http://0.0.0.0:8080"

    server.listen

    View full-size slide

  22. Crystal for the Rubyist
    • Syntax very close to Ruby
    • Ruby style, but type inference
    • Immediate producVvity

    View full-size slide

  23. Improving Ruby Code
    one line at )me

    View full-size slide

  24. Structs to the rescue

    View full-size slide

  25. 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 full-size slide

  26. module Registrar

    class Domain

    def self.find(domain_name)

    resp = Registrar::Client.get("/domain/#{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)

    load_attributes(id: resp.id, name: resp.name)

    end


    def refresh

    resp = Registrar::Client.get("/domains/#{name}")

    load_attributes(id: resp.id, name: resp.name)

    end


    private


    def load_attributes(attributes) # ...

    end

    end

    View full-size slide

  27. module Registrar

    class Domain

    def self.find(domain_name)

    resp = Registrar::Client.get("/domain/#{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)

    load_attributes(id: resp.id, name: resp.name)

    end


    def refresh

    resp = Registrar::Client.get("/domains/#{name}")

    load_attributes(id: resp.id, name: resp.name)

    end


    private


    def load_attributes(attributes) # ...

    end

    end

    View full-size slide

  28. module Registrar

    class Domain

    def self.find(domain_name)

    resp = Registrar::Client.get("/domain/#{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)

    load_attributes(id: resp.id, name: resp.name)

    end


    def refresh

    resp = Registrar::Client.get("/domains/#{name}")

    load_attributes(id: resp.id, name: resp.name)

    end


    private


    def load_attributes(attributes) # ...

    end

    end

    View full-size slide

  29. module Registrar

    class Domain

    def self.find(domain_name)

    resp = Registrar::Client.get("/domain/#{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)

    load_attributes(id: resp.id, name: resp.name)

    end


    def refresh

    resp = Registrar::Client.get("/domains/#{name}")

    load_attributes(id: resp.id, name: resp.name)

    end


    private


    def load_attributes(attributes) # ...

    end

    end

    View full-size slide

  30. 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


    View full-size slide

  31. 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 full-size slide

  32. 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 self.renew(domain_name, years = 1)

    resp = Registrar::Client.post("/renew/#{domain_name}", ...)

    new(resp)

    end

    domain = Registrar::Domain.renew("example.com")

    View full-size slide

  33. Did we forgot that Ruby has Structs?

    View full-size slide

  34. // DomainsService handles communication with the domain related

    // methods of the DNSimple API.

    type DomainsService struct {

    client *Client

    }


    // Domain represents a domain in DNSimple.

    type Domain struct {

    ID int `json:"id,omitempty"`

    AccountID int `json:"account_id,omitempty"`

    Name string `json:"name,omitempty"`

    }


    // DomainListOptions specifies the optional parameters you can provide

    // to customize the DomainsService.ListDomains method.

    type DomainListOptions struct {

    NameLike string `url:"name_like,omitempty"`

    RegistrantID int `url:"registrant_id,omitempty"`

    ListOptions

    }


    // DomainResponse represents a response from an API method that returns a Domain struct.

    type DomainResponse struct {

    Response

    Data *Domain `json:"data"`

    }

    View full-size slide

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

    load_attributes(id: resp.id, name: resp.name)

    end


    View full-size slide

  36. 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 full-size slide

  37. My code today
    • Use Struct-like objects for objects that primarily exists
    for represenVng and passing around data
    • Do not mix heavily data-oriented objects with
    operaVons

    View full-size slide

  38. Ŏ Separate data from execuVon logic
    Ŏ No state changes, means very loose coupling between
    the data and the operaVons

    View full-size slide

  39. Stateless business logic

    View full-size slide

  40. names = [:alpha, :beta, :gamma]
    List.delete_at(names, 0) [:beta, :gamma]
    List.delete_at(names, 0) [:beta, :gamma]
    List.delete_at(names, 0) [:beta, :gamma]

    View full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

  51. class Order < ActiveRecord::Base

    def initialize(account) # ...

    def add_item(items) # ...

    def process! # ...

    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 full-size slide

  52. class Order < ActiveRecord::Base

    def initialize(account) # ...

    def add_item(items) # ...

    def process! # ...

    end


    class OrderService

    def initialize(account_service: AccountService.new)

    @account_service = account_service

    end


    def process_order(order:)

    account = order.account

    @account_service.score_account(account: account)

    order.compute_total

    order.charge_account

    order.mark_as_completed

    @account_service.notify_account(account: account)

    end

    end


    View full-size slide

  53. class Order < ActiveRecord::Base

    def initialize(account) # ...

    def add_item(items) # ...

    def process! # ...

    end


    class OrderService

    def initialize(account_service: nil, payment_service: nil)

    @account_service = account_service || AccountService.new

    @payment_service = payment_service || PaymentService.new

    end


    def process_order(order:)

    account = order.account

    order.processing do

    @account_service.score_account(account: account)

    finalize_order(order)

    @payment_service.charge_account(account: account)

    @account_service.notify_account(account: account)

    end

    end

    end

    View full-size slide

  54. class OrderController

    def create

    order = Order.new(current_account)

    order.add_items(params[:items])


    order.process!

    end

    end


    View full-size slide

  55. 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 full-size slide

  56. class Account < ActiveRecord::Base

    def update_score(value) # ...

    end


    class Order < ActiveRecord::Base

    def initialize(account) # ...

    def add_item(items) # ...

    end


    View full-size slide

  57. 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 full-size slide

  58. Examples in the wild
    @spec flatten(deep_list) :: list when deep_list: [any | deep_list]

    def flatten(list) do

    :lists.flatten(list)

    end


    @spec flatten(deep_list, [elem]) :: [elem] when elem: var, deep_list: [elem | deep_list]

    def flatten(list, tail) do

    :lists.flatten(list, tail)

    end


    @spec foldl([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var

    def foldl(list, acc, function) when is_list(list) and is_function(function) do

    :lists.foldl(function, acc, list)

    end

    View full-size slide

  59. Examples in the wild
    @spec flatten(deep_list) :: list when deep_list: [any | deep_list]

    def flatten(list) do

    :lists.flatten(list)

    end


    @spec flatten(deep_list, [elem]) :: [elem] when elem: var, deep_list: [elem | deep_list]

    def flatten(list, tail) do

    :lists.flatten(list, tail)

    end


    @spec foldl([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var

    def foldl(list, acc, function) when is_list(list) and is_function(function) do

    :lists.foldl(function, acc, list)

    end

    View full-size slide

  60. My code today
    • Use "funcVonal"-oriented service objects to code
    business logic
    • Model objects represent the data to be manipulated
    and expose a custom, well tested API to persist the
    data

    View full-size slide

  61. Ŏ Reduce side effects caused by states,

    improving test effecVveness and code maintainability
    Ŏ Simplify tests by reducing context setup
    Ŏ Slim down model objects removing third-party
    interacVons
    Ŏ Expose the business logic in a centralized place,

    with clear and easy to test methods

    View full-size slide

  62. Func9onal-like

    View full-size slide

  63. class OrdersController

    def create

    order = Order.new(current_account)

    order.add_items(params[:items])


    order.process!

    end

    end


    View full-size slide

  64. class OrdersController

    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 full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

  67. 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 full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

  71. 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 full-size slide

  72. Ŏ Simplify tests by being able to bePer isolate
    dependencies
    Ŏ Improved unit tesVng

    (use integraVon tests for comprehensive coverage)
    Ŏ Speed up tests by not having to test unnecessary code
    due to internal state changes

    View full-size slide

  73. Dependency injec9on

    View full-size slide

  74. 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 full-size slide

  75. 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 full-size slide

  76. 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 full-size slide

  77. 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 full-size slide

  78. describe "#prepare_espresso" do

    it "re-fills the machine when coffee is finished" do

    expect(subject).to receive(:machine).at_least(1).and_return(machine)


    # first time fail, missing coffee

    expect(machine)

    .to receive(:prepare)

    .with("company", :espresso)

    .and_raise(CoffeeMachine::CoffeeIsFinished)

    expect(machine)

    .to receive(:grind_coffee_beans)


    # now we have enough coffee

    expect(machine)

    .to receive(:prepare)

    .with("company", :espresso)

    .and_return(returned = Object.new)


    expect(subject.prepare_espresso).to be(returned)

    end

    end

    View full-size slide

  79. 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 full-size slide

  80. 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 full-size slide

  81. 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 full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

  84. 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 full-size slide

  85. 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 full-size slide

  86. 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 full-size slide

  87. class CoffeeMaker

    def initialize(company, machine: Registry.coffee_machine)

    @company = company

    @machine = machine

    end


    def prepare_espresso # ...

    def balance # ...


    private


    def machine # ...

    end


    CoffeeMaker.new("company") # CoffeeMachine.connect("api.dnsimple.coffee"))
    CoffeeMaker.new("company", machine: CoffeeMachine.connect("api.dnsimple.test"))

    View full-size slide

  88. Examples in the wild
    func Print(a ...interface{}) (n int, err error) {

    return Fprint(os.Stdout, a...)

    }


    func Fprint(w io.Writer, a ...interface{}) (n int, err error) {

    p := newPrinter()

    p.doPrint(a, false, false)

    n, err = w.Write(p.buf)

    p.free()

    return

    }

    View full-size slide

  89. Examples in the wild
    func Print(a ...interface{}) (n int, err error) {

    return Fprint(os.Stdout, a...)

    }
    func Printf(format string, a ...interface{}) (n int, err error) {

    return Fprintf(os.Stdout, format, a...)

    }


    func Println(a ...interface{}) (n int, err error) {

    return Fprintln(os.Stdout, a...)

    }

    View full-size slide

  90. My code today
    • Inject database connecVons such as Redis
    • Inject service dependencies into a service object
    • Inject environment-based adapters or strategies
    • Inject no-op test adapters to replace external services

    View full-size slide

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

    View full-size slide

  92. Powerful error handling

    View full-size slide

  93. 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 full-size slide

  94. 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 full-size slide

  95. def authorize(...)

    begin

    le = Letsencrypt.new("sandbox")

    le.authorize(...)

    rescue Letsencrypt::Error => error

    # do something with error

    end

    end

    View full-size slide

  96. 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 full-size slide

  97. class Letsencrypt


    # Base error for Letsencrypt

    class Error < StandardError

    end


    class DomainValidationError < Error

    end


    View full-size slide

  98. class Letsencrypt


    # Error tag for Letsencrypt.

    module Error

    end


    # Base error for Letsencrypt.

    class LetsencryptError < StandardError

    include Error

    end


    class DomainValidationError < LetsencryptError

    end

    View full-size slide

  99. 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 full-size slide

  100. class Letsencrypt


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

    @environment = environment.to_str

    ENVIRONMENTS.include?(@environment) or

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

    # ...

    end

    View full-size slide

  101. class Letsencrypt


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

    @environment = environment.to_str

    ENVIRONMENTS.include?(@environment) or

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

    # ...

    end

    View full-size slide

  102. def authorize(...)

    begin

    le = Letsencrypt.new("sandbox")

    le.authorize(...)

    rescue Letsencrypt::Error => error

    # do something with error

    end

    end

    View full-size slide

  103. 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 full-size slide

  104. class CustomLibrary

    class Error < StandardError

    end


    def execute

    this_will_fail

    rescue Timeout::Error

    raise Error, "a timeout occurred"

    end


    def this_will_fail

    raise Timeout::Error, "something took long"

    end

    end

    View full-size slide

  105. class CustomLibrary

    class Error < StandardError

    end


    def execute

    this_will_fail

    rescue Timeout::Error

    raise Error, "a timeout occurred"

    end


    def this_will_fail

    raise Timeout::Error, "something took long"

    end

    end

    View full-size slide

  106. 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


    View full-size slide

  107. 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 full-size slide

  108. 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 full-size slide

  109. class CustomLibrary

    class Error < StandardError

    end


    def execute

    this_will_fail

    rescue Timeout::Error

    raise Error, "a timeout occurred"

    end


    def this_will_fail

    raise Timeout::Error, "something took long"

    end

    end

    View full-size slide

  110. My code today
    • Provide an Error module to tag excepVon from a
    specific package
    • Provide a default package excepVon, that is called
    PackageError and include Error
    • Subclass PackageError when needed

    View full-size slide

  111. Ŏ Facilitate error handling by wrapping all the excepVons
    a library can raise into the library namespace
    Ŏ Repackage excepVons, sVll giving access to the original
    error and without overriding backtrace

    View full-size slide

  112. Programming without defaults

    View full-size slide

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

    View full-size slide

  114. 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 full-size slide

  115. Go has no default parameters

    View full-size slide

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

    //

    //

    }


    View full-size slide

  117. 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 full-size slide

  118. 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 full-size slide

  119. 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 full-size slide

  120. Value of simplicity

    View full-size slide

  121. 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 full-size slide

  122. 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 full-size slide

  123. 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 full-size slide

  124. Simplicity vs Verbosity
    class DomainCheckCommand

    include Command::Command


    def execute(domain_name)

    domain = Domain.new(name: domain_name.to_str)

    # ...

    check = DomainRegistrarService.new.check_domain(domain: domain)

    # ...

    end

    end


    class DomainRegistrarService

    def check_domain(domain:)

    Registrar::Domain.check_domain(domain.name)

    end

    end


    View full-size slide

  125. $ go fmt
    $ go fmt ./...
    cmd/gen/gen.go
    publicsuffix/publicsuffix.go
    publicsuffix/publicsuffix_test.go

    View full-size slide

  126. Thanks!
    DNSimple
    dnsimple.com
    @dnsimple
    Simone CarleS
    simonecarle,.com
    @weppos

    View full-size slide