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.

99e0b39c091e10d9c7d4452a34ca52dc?s=128

Simone Carletti

November 25, 2016
Tweet

Transcript

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

    Simone Carle, / / @weppos
  2. None
  3. None
  4. Men9oned at TropicalRuby 2015 RubyConf.ph 2016 RailsConf 2016

  5. None
  6. None
  7. None
  8. "Tradi9onal" languages C C++ C# Java Ruby Python JavaScript Perl

  9. "Emerging" languages Lua Kotlin Clojure Erlang Haskell Elixir Go Rust

    SwiO Julia
  10. Bridges jRuby
 hPp:/ /jruby.org/ Helix
 hPp:/ /blog.skylight.io/introducing-helix/ Erjang
 hPps:/ /github.com/trifork/erjang

  11. hPp:/ /githut.info/

  12. Programming Language Paradigms Object-Oriented

  13. Programming Language Paradigms Object-Oriented FuncVonal

  14. Programming Language Paradigms Object-Oriented FuncVonal Procedural

  15. My influences Elixir Go Java Ruby Crystal Prolog

  16. 3 languages for Rubyist

  17. None
  18. Features • Simplicity and Readability • Concurrency • ComposiVon •

    Good tooling and ecosystem support • Fast builds • Excellent network libraries • High quality standard libs
  19. 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)
 }
  20. 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
  21. 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
 }
  22. None
  23. Features • Concurrency • PaPern matching • Data Immutability •

    Excellent tools • Interoperability with Erlang • Binary manipulaVon • Iex, Mix
  24. 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
  25. 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)
  26. None
  27. Features • Fast • Type inference • Direct binding with

    C • Compile-Vme evaluaVon and generaVon of code
  28. 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
  29. 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
  30. Crystal for the Rubyist • Syntax very close to Ruby

    • Ruby style, but type inference • Immediate producVvity
  31. Improving Ruby Code one line at )me

  32. Structs to the rescue

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

  34. 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
  35. 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
  36. 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
  37. 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
  38. 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

  39. 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)
  40. 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")
  41. Did we forgot that Ruby has Structs?

  42. // 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"`
 }
  43. 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

  44. 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
  45. None
  46. 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
  47. Ŏ Separate data from execuVon logic Ŏ No state changes,

    means very loose coupling between the data and the operaVons
  48. Stateless business logic

  49. 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]
  50. 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
  51. 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
  52. 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
  53. 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

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

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

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

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

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

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

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

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

  62. 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
  63. class OrderController
 def create
 order = Order.new(current_account)
 order.add_items(params[:items])
 
 order.process!


    end
 end

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

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


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

  66. 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
  67. 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
  68. 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
  69. 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
  70. Ŏ 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
  71. Func9onal-like

  72. class OrdersController
 def create
 order = Order.new(current_account)
 order.add_items(params[:items])
 
 order.process!


    end
 end

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

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

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

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

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

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

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

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

  81. Ŏ 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
  82. Dependency injec9on

  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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

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

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

  94. 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")
  95. 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"))
  96. 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"))
  97. 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
 }
  98. 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...)
 }
  99. 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
  100. Ŏ Improve decoupling between resources Ŏ Simplify tests by reducing

    unnecessary setup or code Ŏ Increase test effecVveness by reducing mock objects
  101. Powerful error handling

  102. 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
 ...
  103. 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
  104. def authorize(...)
 begin
 le = Letsencrypt.new("sandbox")
 le.authorize(...)
 rescue Letsencrypt::Error =>

    error
 # do something with error
 end
 end
  105. 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
  106. class Letsencrypt
 
 # Base error for Letsencrypt
 class Error

    < StandardError
 end
 
 class DomainValidationError < Error
 end

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


    end
 
 # Base error for Letsencrypt.
 class LetsencryptError < StandardError
 include Error
 end
 
 class DomainValidationError < LetsencryptError
 end
  108. 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
  109. class Letsencrypt
 
 def initialize(environment:, adapter: DatabaseAdapter.new)
 @environment = environment.to_str


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


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

    error
 # do something with error
 end
 end
  112. 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
  113. None
  114. 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
  115. 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
  116. 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

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

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

  119. 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
  120. 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
  121. Ŏ 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
  122. Programming without defaults

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

    … tomorrow?
  124. 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")
  125. Go has no default parameters

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


    }

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


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

  128. 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"`
 }
  129. 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)

  130. Value of simplicity

  131. 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
  132. 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
  133. 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:)
  134. 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

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

  136. None
  137. None
  138. None
  139. None
  140. None
  141. Thanks! DNSimple dnsimple.com @dnsimple Simone CarleS simonecarle,.com @weppos