Slide 1

Slide 1 text

Maintaining a 5yo project TropicalRuby 2015 Shark Edi=on Simone Carle, //@weppos

Slide 2

Slide 2 text

@weppos

Slide 3

Slide 3 text

Saluzzesi

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Let's talk about sharks!

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

h9ps://www.youtube.com/watch?v=2tS2aapOEQk

Slide 13

Slide 13 text

http://www.projectaware.org/resource/aware-shark-conservation-study-guide

Slide 14

Slide 14 text

Cycle of Maintenance the DNSimple app ConvenIons Pa9erns Code ! " # $ %

Slide 15

Slide 15 text

Cycle of Maintenance ! " # $ % &

Slide 16

Slide 16 text

day 1

Slide 17

Slide 17 text

day 2

Slide 18

Slide 18 text

day 7

Slide 19

Slide 19 text

day 14

Slide 20

Slide 20 text

day 90

Slide 21

Slide 21 text

day 180

Slide 22

Slide 22 text

year 1 ☺

Slide 23

Slide 23 text

year 1.5

Slide 24

Slide 24 text

year 2

Slide 25

Slide 25 text

year 2,5

Slide 26

Slide 26 text

year 3

Slide 27

Slide 27 text

Game ver!

Slide 28

Slide 28 text

the DNSimple app ! " # $ % &

Slide 29

Slide 29 text

It's mostly a Ruby app It contains Ruby, Go and some other languages. This is just a part of DNSimple. ! " # $ % &

Slide 30

Slide 30 text

It was a monolithic Rails app And it's sIll a big Rails-based app ! " # $ % &

Slide 31

Slide 31 text

This is not a talk about Rails But it may contain Rails code ! " # $ % &

Slide 32

Slide 32 text

The Stack Erlang Go(lang) Rails Gems Libraries Lotus Sidekiq

Slide 33

Slide 33 text

Gimme numbers please It's hard to find reasonable code metrics ! " # $ % &

Slide 34

Slide 34 text

Lines of code are useful only when bragging to project managers. ! " # $ % &

Slide 35

Slide 35 text

> cloc .

Slide 36

Slide 36 text

How can we keep our code maintainable

Slide 37

Slide 37 text

KEEP CALM AND GO DIVING with sharks ™

Slide 38

Slide 38 text

convention |kənˈvɛnʃ(ə)n| 1 a way in which something is usually done: to attract the best patrons the movie houses had to ape the conventions and the standards of theatres. • [ mass noun ] behaviour that is considered acceptable or polite to most members of a society: he was an upholder of convention and correct form | [ count noun ] : the law is felt to express social conventions. 2 an agreement between states covering particular matters, especially one less formal than a treaty. the convention, signed by the six states bordering on the Black Sea, aims to prevent further pollution. ! " # $ % &

Slide 39

Slide 39 text

Formalized Code Style How many of you wrote or adopt a ! " # $ % &

Slide 40

Slide 40 text

the GitHub Ruby style guide h9ps://github.com/styleguide/ruby

Slide 41

Slide 41 text

the DNSimple wiki

Slide 42

Slide 42 text

the DNSimple wiki

Slide 43

Slide 43 text

Codeguides

Slide 44

Slide 44 text

Conven=ons are important to ensure that the en=re team speaks the same language ! " # $ % &

Slide 45

Slide 45 text

pattern |ˈpat(ə)n| 2 a regular and intelligible form or sequence discernible in the way in which something happens or is done: a complicating factor is the change in working patterns. 4 an excellent example for others to follow: he set the pattern for subsequent study. ! " # $ % &

Slide 46

Slide 46 text

Iden=fy PaUerns IdenIfy recurring pa9erns in your code and extract them ! " # $ % &

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

Experiment SomeImes the code is not ready for extracIon ! " # $ % &

Slide 49

Slide 49 text

Stand out When you experiment, write code that the team will noIce ! " # $ % &

Slide 50

Slide 50 text

__ is also easy to grep

Slide 51

Slide 51 text

"Stupid" Names Catch the a9enIon of the other developers ! " # $ % &

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

Measure Changes Is a pa9ern really introducing quality? ! " # $ % &

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

Text conven=ons Homogeneous text and names are easier to search and refactor. ! " # $ % &

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

PaUerns are useful to promote code reusability improve long-term maintainability and speed up code refactoring ! " # $ % &

Slide 60

Slide 60 text

Code ! " # $ % &

Slide 61

Slide 61 text

Ruby is an Object Oriented language friendly reminder ! " # $ % &

Slide 62

Slide 62 text

Experiment This is one of the key principle in DNSimple ! " # $ % &

Slide 63

Slide 63 text

Custom API ! " # $ % &

Slide 64

Slide 64 text

Any external dependency we introduce, we wrap it behind a custom API ! " # $ % &

Slide 65

Slide 65 text

Wrappers Composi=on ! " # $ % &

Slide 66

Slide 66 text

require 'phony' module Dnsimple # Phone is a wrapper for the current phone validation library. # # It provides a custom API to the Phony validation library, as well some additional DNSimple-oriented features, # such as a whitelist approach that allows us to define a list of always allowed numbers. # # The whitelist was introduced because of the need to provide an immediate solution for users trying to purchase # a service and being stopped by a validation bug. module Phone WHITELIST = { "+389123456789" => "+389 123456789", } # Makes a plausibility check on the number. # # If the number is in whitelist, the method always returns true. # Otherwise Phony is used. # # number - The String phone number # # Returns true if it's a plausible phone number. def self.plausible?(number) whitelisted?(number) || Phony.plausible?(number) end def self.format(number) whitelisted?(number) || Phony.formatted(Phony.normalize(number), format: :international) end def self.whitelist WHITELIST end def self.whitelisted?(input) number = input.gsub(/[^\d\+]/, "") whitelist[number] end end end

Slide 67

Slide 67 text

module Dnsimple module Phone WHITELIST = {...} # Makes a plausibility check on the number. # # If the number is in whitelist, the method always returns true. # Otherwise Phony is used. # # number - The String phone number # # Returns true if it's a plausible phone number. def self.plausible?(number) whitelisted?(number) || Phony.plausible?(number) end def self.format(number) whitelisted?(number) || Phony.formatted(Phony.normalize(number), format: :international) end def self.whitelist WHITELIST end def self.whitelisted?(input) number = input.gsub(/[^\d\+]/, "") whitelist[number] end end end

Slide 68

Slide 68 text

$ gem update Do you have a policy for gem updates? ! " # $ % &

Slide 69

Slide 69 text

Tes=ng TesIng doesn't require intensive stubbing. ! " # $ % &

Slide 70

Slide 70 text

module Dnsimple class AliasResolver class << self def adapter @adapter ||= NullAdapter.new end # .resolve the ALIAS name and returns a result. # # name - The String FQDN to resolve. # # Returns a AliasResolver::Result. def resolve(name) adapter.resolve(name) end # .enable_test! enables the test adapter. # # Returns nothing. def enable_test! self.adapter = test_adapter.new end end

Slide 71

Slide 71 text

# TestAdapter is a special adapter designed for unit testing. # # It provides the following extra capabilities: # # - stub: to stub a specific resolver call # - clear: to clear the stub list # class TestAdapter def resolve(*) result = @stubs.shift result ? Result.new(*result) : Result.new end def stub(name = nil, ipv4: [], ipv6: [], error: nil) @stubs.push [ipv4, ipv6, error] end end # GoAdapter is an adapter that relies on the dnsimple-alias go binary. class GoAdapter BIN = File.join(Rails.root, "bin", "dsalias") def resolve(name) # ... end end

Slide 72

Slide 72 text

# This is a configuration line in our test suite Dnsimple::AliasResolver.enable_test! describe "a methods that interacts with the resolver" do before do AliasResolver.adapter.test :stub, "fully.qualified.host" end it "does something" do # ... end end

Slide 73

Slide 73 text

Ac=veRecord We have special guidelines ! " # $ % &

Slide 74

Slide 74 text

Just to name a few • Methods defined in AR::Base are not allowed outside the Models • Models must expose custom API to perform operaIons ! " # $ % &

Slide 75

Slide 75 text

Just to name a few • Callbacks are allowed only for data integrity • Callbacks should not interact with other enIIes ! " # $ % &

Slide 76

Slide 76 text

Just to name a few • Finder methods in AR::Base are not allowed outside the Models or Finders • Scopes can't be invoked directly outside Models or Finders • Scopes exists only to support Finders * ! " # $ % &

Slide 77

Slide 77 text

class TaskLog < ActiveRecord::Base def self.log!(name, metadata = nil) metadata = metadata.inject([]) { |t, (k,v)| t << "#{k.to_s}:#{v.to_s}" }.join("/") if metadata create!(name: name, logged_at: Time.current, metadata: metadata) end def self.logged_less?(name, times) where("name = ?", name).count < times end def self.logged_since?(name, time) where("name = ? AND logged_at > ?", name, time).count != 0 end def system? name ~= /^system\./ end end

Slide 78

Slide 78 text

These guidelines are the result of years of experiments, discussions and refactorings ! " # $ % &

Slide 79

Slide 79 text

Experiments may fail! ! " # $ % &

Slide 80

Slide 80 text

class ArpaDomain < ActiveRecord::Base include ModelPersistence # more code here module Persistence def create_arpa_domain(name, account, attributes = {}) self.attributes = attributes self.name = name self.account = account optionally_parse creating do assign_to_auth_name_server create_system_records async_zone_create end end def delete_arpa_domain transaction do if destroy # :async_zone_delete true end end end end include Persistence end

Slide 81

Slide 81 text

We're not afraid to be mistaken. Learn from failed experiments ! " # $ % &

Slide 82

Slide 82 text

Params ! " # $ % &

Slide 83

Slide 83 text

Objects Extract code into classes ! " # $ % &

Slide 84

Slide 84 text

h9p://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-acIverecord-models/

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

require_relative 'params_allowed' class ContactParams include ParamsAllowed def self.allowed [:label, :first_name, :last_name, :address, :city, :state, :postal_code, :country, :email, :phone] end end # Params encapsulate the logic to handle some parameters from an untrusted source. # # A Params object generally doesn't include the Param module directly. Instead, it relies to one of the # more specific implementations, such as ParamsAllowed. module Params def initialize(params) @params = ActiveSupport::HashWithIndifferentAccess.new(params.to_h) end end module ParamsAllowed def self.included(base) base.include Params end def to_h @params.slice(*self.class.allowed).to_hash.symbolize_keys! end end

Slide 87

Slide 87 text

No content

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

Beware of Mixins ! " # $ % &

Slide 90

Slide 90 text

Pulling out methods from a large class into mixins is like hiding the dust under the carpet. ! " # $ % &

Slide 91

Slide 91 text

– Anonymous “Any application with an app/concerns directory is concerning.”

Slide 92

Slide 92 text

It looks cleaner at the surface, but it actually makes it harder to iden=fy and implement the decomposi=ons and extrac=ons necessary to clarify the domain model. ! " # $ % &

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

module ZoneWorkerConcern def errors @errors ||= [] end def finish unless errors.empty? raise Dnsimple::ZoneServerError.new(errors.join("\n")) end end protected def domain_name options['name'] end def http_options {basic_auth: {username: C_.zone_server_identifier, password: C_.zone_server_token}} end end

Slide 95

Slide 95 text

Interactor Objects ! " # $ % &

Slide 96

Slide 96 text

aka • Interactor (CollecIveIdea)
 h9ps://github.com/collecIveidea/interactor • OperaIons
 h9p://www.trailblazerb.org/ • MutaIons
 h9ps://github.com/cypriss/mutaIons ! " # $ % &

Slide 97

Slide 97 text

We use "Command" For historical reasons. Rename in progress… ! " # $ % &

Slide 98

Slide 98 text

No content

Slide 99

Slide 99 text

class ContactCreateCommand include Command::Command def execute(account, contact_attributes) contact = Contact.new if contact.create_contact(account, contact_attributes.to_h) Activity.track_contact(context.actor, contact, :create) else result.error = I18n.t("app.message_error_validation_failed") end ensure result.value = contact end end

Slide 100

Slide 100 text

def create @result = ContactCreateCommand.execute(command_context, this_account, contact_params) @contact = @result.value respond_to do |format| if @result.successful? format.html { redirect_to contact_path(@contact), notice: t("flash.contact_create_success") } format.json { render json: @contact, status: 201 } else format.html { render action: "new" } format.json { render json: { message: @result.error, errors: @contact.errors }, status: 400 } end end end def create @result = ContactCreateCommand.execute(command_context, this_account, contact_params) @contact = @result.value if @result.successful? redirect_to contact_path(@contact), notice: t("flash.contact_create_success") else render action: "new" end end

Slide 101

Slide 101 text

Error handling ! " # $ % &

Slide 102

Slide 102 text

class CertificateSubmitCommand include Command::Command def execute(certificate, approver_email) return if error_certificate_notpaid(certificate) certificate.approver_email = approver_email certificate.do_submit rescue ActiveRecord::RecordInvalid => e result.error = I18n.t("app.message_error_validation_failed") rescue Enom::EnomError => e result.error = e.message end private def error_certificate_notpaid(domain) if something result.error = "gimme money first!" end end end

Slide 103

Slide 103 text

Finder Objects ! " # $ % &

Slide 104

Slide 104 text

class ContactsController < ApplicationController def index @contacts = Contact.where(account: current_account).order(:name) end end module Api::V2::Controllers::Contacts class Index include Lotus::Action def call(params) @contacts = Contact.where(account: current_account).order(:name) end end end

Slide 105

Slide 105 text

ContactFinder ! " # $ % &

Slide 106

Slide 106 text

class ContactsController < ApplicationController def index @contacts = ContactFinder.account_contacts_listing(current_account) end end module Api::V2::Controllers::Contacts class Index include Lotus::Action def call(params) @contacts = ContactFinder.account_contacts_listing(current_account) end end end

Slide 107

Slide 107 text

Don't inherit from Ac=veRecord ! " # $ % &

Slide 108

Slide 108 text

class EmailForwardFinder include ModelFinder::Finder # Finds the email forward with identifier associated with domain. def self.find_domain_email_forward!(identifier, domain) domain_email_forwards(domain).find_by_id(identifier) or not_found!(identifier) end # Lists the domain email forwards. def self.domain_forwards_listing(domain) domain_email_forwards(domain).order(:to) end # Selects the domain email forwards in descending update order. def self.domain_recently_updated_forwards(domain) domain_email_forwards(domain).order(updated_at: :desc) end private def self.not_found!(identifier) raise(ActiveRecord::RecordNotFound, "Email forward `#{identifier}' not found") end def self.domain_email_forwards(domain) domain.email_forwards end end

Slide 109

Slide 109 text

find() Full control of find() implementaIon ! " # $ % &

Slide 110

Slide 110 text

class TemplateFinder include ModelFinder::Finder def self.find(identifier) case identifier when Fixnum, ModelFinder::PATTERN_ID find_by_id(identifier) else find_by_slug(identifier) end end def self.find!(identifier) find(identifier) or not_found!(identifier) end def self.find_by_id(identifier) scope.where(id: identifier).first end def self.find_by_slug(identifier) query = identifier.to_s.downcase scope.where(short_name: query).first end def self.find_permitted_template(identifier, context) scoped(context.templates).find(identifier) end

Slide 111

Slide 111 text

Finders advantages • Custom API • Reduce model complexity • Isolate queries • Avoid temptaIon to use AR methods ! " # $ % &

Slide 112

Slide 112 text

Wrapping Up ! " # $ % &

Slide 113

Slide 113 text

Experiment Don't take my word for it ! " # $ % &

Slide 114

Slide 114 text

Code Monkey Don't be a ! " # $ % &

Slide 115

Slide 115 text

Break it! Don't be afraid to experiment with your code ! " # $ % &

Slide 116

Slide 116 text

Small steps Don't throw away your enIre codebase ! " # $ % &

Slide 117

Slide 117 text

Pay aUen=on to Code Smells ! " # $ % &

Slide 118

Slide 118 text

Programming Languages Take a look at other ! " # $ % &

Slide 119

Slide 119 text

Start today! ! " # $ % &

Slide 120

Slide 120 text

Thanks to…

Slide 121

Slide 121 text

Photos https://www.flickr.com/photos/marcodewaal/14009944759 https://www.flickr.com/photos/alastair_pollock/5266243108/ https://www.flickr.com/photos/rodrigocvc/3734737309 https://www.flickr.com/photos/goya74/8584790873

Slide 122

Slide 122 text

Ques=ons? @weppos simonecarle,.com