Slide 1

Slide 1 text

How programming in other languages
 made my Ruby code be7er Simone Carle, / / @weppos

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Men9oned at TropicalRuby 2015 RubyConf.ph 2016 RailsConf 2016

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

hPp:/ /githut.info/

Slide 12

Slide 12 text

Programming Language Paradigms Object-Oriented

Slide 13

Slide 13 text

Programming Language Paradigms Object-Oriented FuncVonal

Slide 14

Slide 14 text

Programming Language Paradigms Object-Oriented FuncVonal Procedural

Slide 15

Slide 15 text

My influences Elixir Go Java Ruby Crystal Prolog

Slide 16

Slide 16 text

3 languages for Rubyist

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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
 }

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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)

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Improving Ruby Code one line at )me

Slide 32

Slide 32 text

Structs to the rescue

Slide 33

Slide 33 text

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


Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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


Slide 39

Slide 39 text

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)

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Did we forgot that Ruby has Structs?

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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


Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Stateless business logic

Slide 49

Slide 49 text

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]

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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


Slide 54

Slide 54 text

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


Slide 55

Slide 55 text

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


Slide 56

Slide 56 text

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


Slide 57

Slide 57 text

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


Slide 58

Slide 58 text

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


Slide 59

Slide 59 text

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


Slide 60

Slide 60 text

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


Slide 61

Slide 61 text

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


Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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


Slide 64

Slide 64 text

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


Slide 65

Slide 65 text

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


Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Ŏ 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

Slide 71

Slide 71 text

Func9onal-like

Slide 72

Slide 72 text

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


Slide 73

Slide 73 text

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


Slide 74

Slide 74 text

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


Slide 75

Slide 75 text

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


Slide 76

Slide 76 text

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


Slide 77

Slide 77 text

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


Slide 78

Slide 78 text

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


Slide 79

Slide 79 text

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


Slide 80

Slide 80 text

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


Slide 81

Slide 81 text

Ŏ 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

Slide 82

Slide 82 text

Dependency injec9on

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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


Slide 92

Slide 92 text

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


Slide 93

Slide 93 text

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


Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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
 }

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Powerful error handling

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

def authorize(...)
 begin
 le = Letsencrypt.new("sandbox")
 le.authorize(...)
 rescue Letsencrypt::Error => error
 # do something with error
 end
 end

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

class Letsencrypt
 
 # Base error for Letsencrypt
 class Error < StandardError
 end
 
 class DomainValidationError < Error
 end


Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

def authorize(...)
 begin
 le = Letsencrypt.new("sandbox")
 le.authorize(...)
 rescue Letsencrypt::Error => error
 # do something with error
 end
 end

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

No content

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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


Slide 117

Slide 117 text

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


Slide 118

Slide 118 text

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


Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

Ŏ 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

Slide 122

Slide 122 text

Programming without defaults

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

Go has no default parameters

Slide 126

Slide 126 text

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


Slide 127

Slide 127 text

func ListDomains(account string, options *DomainListOptions) (*DomainsResponse, error) {
 //
 //
 }
 
 type DomainListOptions struct {
 NameLike string `url:"name_like,omitempty"`
 RegistrantID int `url:"registrant_id,omitempty"`
 
 ListOptions
 }


Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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)


Slide 130

Slide 130 text

Value of simplicity

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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


Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

No content

Slide 137

Slide 137 text

No content

Slide 138

Slide 138 text

No content

Slide 139

Slide 139 text

No content

Slide 140

Slide 140 text

No content

Slide 141

Slide 141 text

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