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

No content

Slide 5

Slide 5 text

RubyConf.ph 2016

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

My influences Go Elixir Java Ruby Crystal Prolog

Slide 10

Slide 10 text

Improving Ruby Code one line at )me

Slide 11

Slide 11 text

Structs to the rescue

Slide 12

Slide 12 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 13

Slide 13 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
 
 def refresh
 resp = Registrar::Client.get("/domains/#{name}")
 load_attributes(id: resp.id, name: resp.name)
 end
 
 # ...
 end
 end

Slide 14

Slide 14 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
 
 def refresh
 resp = Registrar::Client.get("/domains/#{name}")
 load_attributes(id: resp.id, name: resp.name)
 end
 
 # ...
 end
 end

Slide 15

Slide 15 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
 
 def refresh
 resp = Registrar::Client.get("/domains/#{name}")
 load_attributes(id: resp.id, name: resp.name)
 end
 
 # ...
 end
 end

Slide 16

Slide 16 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 17

Slide 17 text

Did we forgot that Ruby has Structs?

Slide 18

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

Slide 19

Slide 19 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 20

Slide 20 text

No content

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Stateless business logic

Slide 23

Slide 23 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 24

Slide 24 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 25

Slide 25 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 26

Slide 26 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 27

Slide 27 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 28

Slide 28 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 29

Slide 29 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 30

Slide 30 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 31

Slide 31 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 32

Slide 32 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 33

Slide 33 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(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 34

Slide 34 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 35

Slide 35 text

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


Slide 36

Slide 36 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 37

Slide 37 text

Ŏ Reduce side effects caused by states,
 improving test effecJveness and code maintainability Ŏ Simplify tests by reducing context setup Ŏ Slim down model objects removing third-party interacJons Ŏ Expose the business logic in a centralized place,
 with clear and easy to test methods

Slide 38

Slide 38 text

FuncGonal-like

Slide 39

Slide 39 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 40

Slide 40 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 41

Slide 41 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 42

Slide 42 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 43

Slide 43 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 44

Slide 44 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 45

Slide 45 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 46

Slide 46 text

Ŏ Simplify tests by being able to bePer isolate dependencies Ŏ Improved unit tesJng
 (use integraJon tests for comprehensive coverage) Ŏ Speed up tests by not having to test unnecessary code due to internal state changes

Slide 47

Slide 47 text

Dependency injecGon

Slide 48

Slide 48 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 49

Slide 49 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 50

Slide 50 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 51

Slide 51 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 52

Slide 52 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 53

Slide 53 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 54

Slide 54 text

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


Slide 55

Slide 55 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 56

Slide 56 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 57

Slide 57 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 58

Slide 58 text

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

Slide 59

Slide 59 text

Powerful error handling

Slide 60

Slide 60 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 61

Slide 61 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 62

Slide 62 text

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


Slide 63

Slide 63 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 64

Slide 64 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 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 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 68

Slide 68 text

class CustomLibrary
 class Error < StandardError
 end
 
 def execute
 this_will_fail
 rescue Timeout::Error
 raise Error, "a timeout occurred"
 end
 
 # ...
 end

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 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 71

Slide 71 text

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

Slide 72

Slide 72 text

Programming without defaults

Slide 73

Slide 73 text

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

Slide 74

Slide 74 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 75

Slide 75 text

Go has no default parameters

Slide 76

Slide 76 text

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


Slide 77

Slide 77 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 78

Slide 78 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 79

Slide 79 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 80

Slide 80 text

Value of simplicity

Slide 81

Slide 81 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 82

Slide 82 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 83

Slide 83 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 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

No content

Slide 88

Slide 88 text

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