Slide 1

Slide 1 text

Rails, Objects, and Long- Lived Code Noel Rappin Table XI

Slide 2

Slide 2 text

Instructions: 4 Please create a name tag 4 Go to https://github.com/noelrappin/ancient_city 4 Follow the instructions 4 Wifi Name: PrivateGroup 4 Wifi password: CMHgroup15

Slide 3

Slide 3 text

A Plan 4 I'm gonna talk for a few minutes 4 We're going to look at code together 4 We're going to make changes together 4 You're going to make changes on your own

Slide 4

Slide 4 text

Please 4 Ask questions 4 Disagree 4 Pair Program

Slide 5

Slide 5 text

And away we go

Slide 6

Slide 6 text

How do you structure a Rails application?

Slide 7

Slide 7 text

Rails defaults arguably don't go far enough

Slide 8

Slide 8 text

A brief, idiosyncratic history 4 Dec 2005: Rails 1.0 Released 4 Oct 2006: Jamis Buck writes "Skinny Controller, Fat Model" 4 Dec 2007: Rails 2.0 Released (REST) 4 Mar 2010: James Golick writes "Crazy, Heretical, and Awesome" 4 Aug 2010: Rails 3.0 Released

Slide 9

Slide 9 text

A brief, idiosyncratic history 4 Sep 2011: Cory Haines talk "Fast Rails Tests" 4 Nov 2011: Uncle Bob talk on "Architecture: The Lost Years" 4 Jan 2012: Avdi Grimm writes "Objects on Rails" 4 April 2012: Rails API project starts 4 Mar 2012: Jim Gay writes "Clean Ruby" 4 July 2012: Matt Wynne talk "Hexagonal Rails"

Slide 10

Slide 10 text

A brief, idiosyncratic history 4 Oct 2012: Brian Helmcamp "7 Patterns to Refactor" 4 June 2013: Rails 4.0 Released 4 Aug 2013: Ben Smith "How I architected" 4 Oct 2013: Jim Weirich on Decoupling from Rails 4 Apr 2014: DHH RailsConf keynote 4 Nov 2014: Trailblazer by Nick Sutterer

Slide 11

Slide 11 text

Where are we now? A lot of apps with the same problems: 4 My views are too complicated 4 My tests are too slow 4 My models are too big 4 Everything is too tangled

Slide 12

Slide 12 text

Where we are now? A lot of people have answers 4 Service Oriented Architecture 4 Presenters 4 Data, Context, And Interaction (DCI) 4 Hexagonal Rails 4 Trailblazer 4 Engines

Slide 13

Slide 13 text

Where we are now? A lot of people have answers 4 Dependency Injection 4 Single Responsibility Principle 4 Separation of concerns

Slide 14

Slide 14 text

"All problems in computer science can be solved by another level of indirection" 1 David Wheeler

Slide 15

Slide 15 text

"... except for the problem of too many layers of indirection." 1 Kevlin Henney

Slide 16

Slide 16 text

Some people feel they came to Rails to get away from this stuff

Slide 17

Slide 17 text

DHH insists than none of this is necessary

Slide 18

Slide 18 text

Can we build complex apps without them turning into: A ball of mud An overcomplicated mess

Slide 19

Slide 19 text

The problem:

Slide 20

Slide 20 text

All these techniques seem like crazy overkill until you realize you should have done them six months ago

Slide 21

Slide 21 text

The Bet

Slide 22

Slide 22 text

Is the extra work I'm doing worth it?

Slide 23

Slide 23 text

The Good Outcome: Add complexity, makes future change easier

Slide 24

Slide 24 text

The Bad Outcome: You don't add complexity, future change is harder

Slide 25

Slide 25 text

The other bad outcome: You add complexity, but the change never comes

Slide 26

Slide 26 text

The worst outcome: You add the wrong complexity, change is still harder

Slide 27

Slide 27 text

MATH! BreakEven = PotentialCost / PotentialSavings

Slide 28

Slide 28 text

DHH argues that the cost of not adding complexity stays lower if you keep things simple.

Slide 29

Slide 29 text

But DHH has less to fear than you do of Rails changing under him.

Slide 30

Slide 30 text

We're going to explore some techniques in an application

Slide 31

Slide 31 text

We're going to refactor to better patterns from bad ones

Slide 32

Slide 32 text

Basic Principle One: Don't Repeat Yourself

Slide 33

Slide 33 text

Every piece of logic should have exactly one representation

Slide 34

Slide 34 text

Basic Principle Two: Single Responsibility Principle

Slide 35

Slide 35 text

A unit of code should do one thing

Slide 36

Slide 36 text

Basic Principle Three: Semantically Meaningful Names

Slide 37

Slide 37 text

Basic Principle Four: Complexity can not be decreased past a certain point

Slide 38

Slide 38 text

It can only be managed

Slide 39

Slide 39 text

Deal with many small components separately

Slide 40

Slide 40 text

Use simple constructs and simple transitions

Slide 41

Slide 41 text

Three more things:

Slide 42

Slide 42 text

Software is complicated and there are lots of ways to be successful

Slide 43

Slide 43 text

The best way to find the boundaries of a technique is to cross them

Slide 44

Slide 44 text

These examples should be understood as proxies for more complex logic

Slide 45

Slide 45 text

Exercise One: Presenters

Slide 46

Slide 46 text

What's wrong with this? index.html.haml %h1.trip_index_header#headline Time Travel Adventures - @trips.each_slice(3) do |group| .row - group.each do |trip| .col-md-3.trip_entry[trip] .trip_header= link_to trip.name, trip .trip_tag= trip.tag_line .trip_dates= "#{trip.start_date.to_s(:long)} - #{trip.end_date.to_s(:long)}" .trip_image= image_tag(trip.image_name, :height => 150, :width => 150) .trip_price= number_to_currency(trip.price) .trip_links = link_to "Show Details", trip, :class => 'detail_toggle' .detail_hidden %div= trip.description

Slide 47

Slide 47 text

Possibly, nothing... But...

Slide 48

Slide 48 text

4 It's not semantically meaningful 4 It doesn't handle complexity well 4 It's not set up to be reused

Slide 49

Slide 49 text

Split the view %h1.trip_index_header#headline Time Travel Adventures - @trips.each_slice(3) do |group| .row= render partial: "one_trip", collection: group, as: :trip

Slide 50

Slide 50 text

.col-md-3.trip_entry[trip] .trip_header= link_to trip.name, trip .trip_tag= trip.tag_line .trip_dates= "#{trip.start_date.to_s(:long)} - #{trip.end_date.to_s(:long)}" .trip_image= image_tag(trip.image_name, :height => 150, :width => 150) .trip_price= number_to_currency(trip.price) .trip_links = link_to "Show Details", trip, :class => 'detail_toggle' .detail_hidden %div= trip.description

Slide 51

Slide 51 text

We could do this: class Trip < ActiveRecord::Base has_many :hotels has_many :activities validates_presence_of :description, :start_date, :end_date, :price, :tag_line def date_range_display "#{start_date.to_s(:long)} - #{end_date.to_s(:long)}" end end

Slide 52

Slide 52 text

Which is bad because... 4 Our chocolate is in our peanut butter 4 They will not taste great together

Slide 53

Slide 53 text

Where to put this code? What is this code doing?

Slide 54

Slide 54 text

Let's try this: class TripPresenter < SimpleDelegator def initialize(trip) super(trip) end def date_range_display "#{start_date.to_s(:long)} - #{end_date.to_s(:long)}" end end

Slide 55

Slide 55 text

SimpleDelegator 4 Passes all missing methods to the source object 4 Which is accessible via __getobj__

Slide 56

Slide 56 text

A little more support class HomeController < ApplicationController def index @trips = Trip.all.map { |t| TripPresenter.new(t) } end end

Slide 57

Slide 57 text

And then... .span3.trip_entry[trip.__getobj__] .trip_header= trip.link_to_trip_show_page .trip_tag= trip.tag_line .trip_dates= trip.date_range_display .trip_image= trip.main_page_image_tag .trip_price= trip.price_display .trip_links = link_to "Show Details", trip, :class => 'detail_toggle' .detail_hidden %div= trip.description

Slide 58

Slide 58 text

And... class TripPresenter < SimpleDelegator include ActionView::Helpers::UrlHelper include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::NumberHelper def initialize(trip) super(trip) end def date_range_display "#{start_date.to_s(:long)} - #{end_date.to_s(:long)}" end def link_to_trip_show_page link_to(name, Rails.application.routes.url_helpers.trip_path(__getobj__)) end def main_page_image_tag image_tag("/assets/#{image_name}", height: 150, width: 150) end def price_display number_to_currency(price) end end

Slide 59

Slide 59 text

Why is this better? 4 View code declares intent 4 Able to more easily unit test presentation logic 4 Easier to reason about presentation code 4 Potential for reuse

Slide 60

Slide 60 text

Let's back up

Slide 61

Slide 61 text

What's an object? 4 Data and behavior 4 A series of messages

Slide 62

Slide 62 text

But I thought objects were just the nouns in my system?

Slide 63

Slide 63 text

We've created a useful abstraction The intersection of noun and context

Slide 64

Slide 64 text

For you The Trip show page

Slide 65

Slide 65 text

%h1= @trip.name %h2 Booking Options = form_for Order.new do |f| .options %h3 Hotel Options .length_of_stay Number of nights = hidden_field_tag :trip_id, @trip.id = select_tag :length_of_stay, options_for_select((1 .. (@trip.end_date - @trip.start_date)).to_a), include_blank: true - @trip.hotels.each do |hotel| %div[hotel] = radio_button_tag(:hotel_id, hotel.id, false) = "#{hotel.name}: #{hotel.description} (#{number_to_currency(hotel.price)})" %h3 Activity Options - @trip.activities.each do |activity| %div[activity] = check_box_tag(:"activity_id[]", activity.id, false, id: "activity_id_#{activity.id}") = "#{activity.name}: #{activity.description} (#{number_to_currency(activity.price)})" %br = f.submit "Order"

Slide 66

Slide 66 text

Convert it to a presenter

Slide 67

Slide 67 text

Object decorators vs. view models

Slide 68

Slide 68 text

Next up

Slide 69

Slide 69 text

Let's purchase a trip

Slide 70

Slide 70 text

Steps 4 Determine Availability 4 Calculate Price 4 Process Transaction 4 Create Objects 4 Call It A Day

Slide 71

Slide 71 text

Creating Objects

Slide 72

Slide 72 text

An integration test describe "basic process" do it "creates order and line item objects" do visit("/trips/#{mayflower.id}") select('4', :from => 'length_of_stay') choose("hotel_id_#{mayflower.hotels.first.id}") check("activity_id_#{mayflower.activities.first.id}") click_button("Order") order = Order.last expect(order.order_line_items.count).to eq(3) expect(order.order_line_items.map(&:buyable)).to eq( [mayflower, mayflower.hotels.first, mayflower.activities.first]) end end

Slide 73

Slide 73 text

Passing class OrdersController < ApplicationController def create order = Order.new order.order_line_items.new(buyable: Trip.find(params[:trip_id])) order.order_line_items.new(buyable: Hotel.find(params[:hotel_id])) params[:activity_id].each do |aid| order.order_line_items.new(buyable: Activity.find(aid)) end order.save redirect_to :root end end

Slide 74

Slide 74 text

But Ugly

Slide 75

Slide 75 text

Let's try again class OrdersController < ApplicationController def create action = PurchasesOrder.new( params[:trip_id], params[:hotel_id], params[:activity_id]) action.run redirect_to :root end end

Slide 76

Slide 76 text

And the action: class PurchasesOrder def initialize(trip_id, hotel_id, activity_ids) @trip_id, @hotel_id, @activity_ids = trip_id, hotel_id, activity_ids end def trip @trip ||= Trip.find(@trip_id) end def hotel @hotel ||= Hotel.find(@trip_id) end def activities @activities ||= @activity_ids.map { |id| Activity.find(id) } end def order @order ||= Order.new end def add_line_item(buyable) order.order_line_items.new(buyable: buyable) end def run add_line_item(trip) add_line_item(hotel) activities.each { |a| add_line_item(a) } order.save end end

Slide 77

Slide 77 text

Why is this better? 4 Unit testable 4 Better able to manage future complexity 4 Declares intent

Slide 78

Slide 78 text

Calculating price

Slide 79

Slide 79 text

Algorithm The sum of: 4 Activities have a price 4 Hotels have a nights stayed times a price 4 Trips have a price

Slide 80

Slide 80 text

We've got options

Slide 81

Slide 81 text

4 Logic in existing models 4 Logic in workflow object 4 Create new calculator object 4 Some combination thereof

Slide 82

Slide 82 text

We don't have to start at the end point, as long as we have tests

Slide 83

Slide 83 text

Starting with an acceptance test preserves ambiguity it "correctly puts pricing in the line item objects" do visit("/trips/#{mayflower.id}") select('4', :from => 'length_of_stay') choose("hotel_id_#{mayflower.hotels.first.id}") check("activity_id_#{mayflower.activities.first.id}") click_button("Order") order = Order.last expect(order.trip_item.unit_price).to eq(1200) expect(order.trip_item.amount).to eq(1) expect(order.trip_item.total_price).to eq(1200) expect(order.hotel_item.unit_price).to eq(500) expect(order.hotel_item.amount).to eq(4) expect(order.hotel_item.total_price).to eq(2000) expect(order.activity_items.first.unit_price).to eq(400) expect(order.activity_items.first.amount).to eq(1) expect(order.activity_items.first.total_price).to eq(400) expect(order.total_price_paid).to eq(3600) end

Slide 84

Slide 84 text

Get to Green class Order < ActiveRecord::Base has_many :order_line_items def trip_item order_line_items.where(buyable_type: "Trip").first end def hotel_item order_line_items.where(buyable_type: "Hotel").first end def activity_items order_line_items.where(buyable_type: "Activity") end end

Slide 85

Slide 85 text

Get To Green class OrdersController < ApplicationController def create PurchasesOrder.new( params[:trip_id], params[:hotel_id], params[:activity_id], params[:length_of_stay]).run redirect_to :root end end

Slide 86

Slide 86 text

Get To Green def add_line_item(buyable, unit_price, amount) order.order_line_items.new(buyable: buyable, unit_price: unit_price, amount: amount, total_price: amount * unit_price) end def run add_line_item(trip, trip.price, 1) add_line_item(hotel, hotel.price, length_of_stay.to_i) activities.each { |a| add_line_item(a, a.price, 1) } order.total_price_paid = order.order_line_items.map(&:total_price).sum order.save end

Slide 87

Slide 87 text

Let's make this more complicated Processing Fee: 4 $10 per trip 4 $1 per 100 years the trip start date is from 2015 4 $5 per activity 4 $10 if hotel per night fee is over $250

Slide 88

Slide 88 text

Why not put this in the model? 4 Most of the time, the model doesn't care 4 SRP

Slide 89

Slide 89 text

Let's build this bottom up require 'rails_helper' describe ActivityProcessingFee do it "always returns $5" do calculator = ActivityProcessingFeeCalculator.new(double) expect(calculator.fee).to eq(Money.new(500)) end end

Slide 90

Slide 90 text

Passed By class ActivityProcessingFee def initialize(activity) end def fee Money.new(500) end end

Slide 91

Slide 91 text

Then require 'rails_helper' describe HotelProcessingFee do it "returns zero for a hotel under $250" do hotel = double(price: 100) calculator = HotelProcessingFeeCalculator.new(hotel) expect(calculator.fee).to eq(Money.zero) end it "returns $10 for a hotel that is greater than $250" do hotel = double(price: 300) calculator = HotelProcessingFeeCalculator.new(hotel) expect(calculator.fee).to eq(Money.new(1000)) end end

Slide 92

Slide 92 text

And... require 'rails_helper' describe TripProcessingFee do it "returns expected trip that is within 100 years" do trip = double(start_date: Date.parse("Jan 1, 2000")) calculator = TripProcessingFee.new(trip) expect(calculator.fee).to eq(Money.new(15)) end end

Slide 93

Slide 93 text

Passing class HotelProcessingFee attr_accessor :hotel def initialize(hotel) @hotel = hotel end def fee if hotel.price > 250 then Money.new(1000) else Money.zero end end end

Slide 94

Slide 94 text

More passing class TripProcessingFee attr_accessor :trip def initialize(trip) @trip = trip end def year trip.start_date.year end def fee Money.new(Time.now.year - year) end end

Slide 95

Slide 95 text

Acceptance it "correctly puts pricing in the activity line item objects" do order = Order.last activity = order.activity_items.first expect(activity.unit_price).to eq(400) expect(activity.amount).to eq(1) expect(activity.extended_price).to eq(400) expect(activity.processing_fee).to eq(5) end it "correctly puts pricing in the order object" do order = Order.last expect(order.total_price_paid).to eq(3600 + 3.95 + 10 + 5 + 10) end

Slide 96

Slide 96 text

Passing def add_line_item(buyable, unit_price, amount, calculator_class) extended_price = amount * unit_price processing_fee = calculator_class.new(buyable).fee order.order_line_items.new(buyable: buyable, unit_price: unit_price, amount: amount, extended_price: extended_price, processing_fee: processing_fee, price_paid: extended_price + processing_fee) end def calculate_order_price order.order_line_items.map(&:price_paid).sum + Money.new(1000) end def run add_line_item(trip, trip.price, 1, TripProcessingFee) add_line_item( hotel, hotel.price, length_of_stay.to_i, HotelProcessingFee) activities.each do |a| add_line_item(a, a.price, 1, ActivityProcessingFee) end order.total_price_paid = calculate_order_price order.save end

Slide 97

Slide 97 text

What have we done? 4 Dependency Injection

Slide 98

Slide 98 text

Let's make this more complicated

Slide 99

Slide 99 text

Coupon codes

Slide 100

Slide 100 text

Requirements 4 A code can offer a percentage discount 4 A code can apply to trips, hotels, activities, or all 3 4 A code does not apply to processing costs 4 Free items don't have processing costs

Slide 101

Slide 101 text

How to represent

Slide 102

Slide 102 text

Maybe refactor first?

Slide 103

Slide 103 text

Preserve API def add_line_item(buyable, unit_price, amount, calculator_class) OrderLineItemFactory.new(order, buyable, unit_price, amount, calculator_class).run end

Slide 104

Slide 104 text

class OrderLineItemFactory attr_accessor :order, :buyable, :unit_price, :amount, :fee_calculator_class def initialize(order, buyable, unit_price, amount, fee_calculator_class) @order, @buyable, @unit_price, @amount = order, buyable, unit_price, amount @fee_calculator_class = fee_calculator_class end def extended_price amount * unit_price end def processing_fee fee_calculator_class.new(buyable).fee.to_f end def price_paid extended_price + processing_fee end def run order.order_line_items.new(buyable: buyable, unit_price: unit_price, amount: amount, extended_price: extended_price, processing_fee: processing_fee, price_paid: price_paid) end end

Slide 105

Slide 105 text

Move Fee Calculator knowledge?

Slide 106

Slide 106 text

Passsing code through %h3 Coupon Code = text_field_tag :coupon_code def create PurchasesOrder.new( params[:trip_id], params[:hotel_id], params[:activity_id], params[:length_of_stay], params[:coupon_code]).run redirect_to :root end def initialize(trip_id, hotel_id, activity_ids, length_of_stay, code) @trip_id, @hotel_id, @activity_ids = trip_id, hotel_id, activity_ids @length_of_stay = length_of_stay @code = code end def coupon_code @coupon_code ||= CouponCode.find_by_code(@code) end def add_line_item(buyable, unit_price, amount, calculator_class) OrderLineItemFactory.new(order, buyable, unit_price, amount, coupon_code, calculator_class).run end

Slide 107

Slide 107 text

Passing logic def price_paid extended_price - discount + processing_fee end def discount return 0 unless coupon_code (coupon_code.discount_percentage * extended_price) / 100.0 end def run order.order_line_items.new(buyable: buyable, unit_price: unit_price, amount: amount, extended_price: extended_price, processing_fee: processing_fee, price_paid: price_paid, discount: discount) end

Slide 108

Slide 108 text

Coupon Code application logic require "rails_helper" describe CouponCode do describe "applicability" do let(:trip) { Trip.new } let(:hotel) { Hotel.new } let(:activity) { Activity.new } let(:code) { CouponCode.new } describe "a general code" do before { code.applies_to = :all } specify { expect(code.ok_for(trip)).to be_truthy } specify { expect(code.ok_for(hotel)).to be_truthy } specify { expect(code.ok_for(activity)).to be_truthy } end describe "a specific code" do before { code.applies_to = :trip } specify { expect(code.ok_for(trip)).to be_truthy } specify { expect(code.ok_for(hotel)).to be_falsy } specify { expect(code.ok_for(activity)).to be_falsy } end end end

Slide 109

Slide 109 text

The Code class CouponCode < ActiveRecord::Base has_many :order_line_items def ok_for(buyable) return true if applies_to.blank? || applies_to == "all" buyable.class.name.downcase == applies_to end end

Slide 110

Slide 110 text

The Call def coupon_code_applies? coupon_code && coupon_code.ok_for(buyable) end def discount return 0 unless coupon_code_applies? (coupon_code.discount_percentage * extended_price) / 100.0 end

Slide 111

Slide 111 text

And then... 4 Limit on number of times a user can use a code 4 Maximum discount limit on code 4 Code applies to specific trip, hotel, or activity 4 Code only applies when used during a specific date range

Slide 112

Slide 112 text

Third party workflows

Slide 113

Slide 113 text

Let's check availability

Slide 114

Slide 114 text

Using our fake API class AvailabilityApi def self.available?(api_id, date_string) sleep(rand * 5) result = rand if result < 0.95 true elsif result < 0.99 false else sleep(rand * 10) raise "Ooops" end end end

Slide 115

Slide 115 text

Build our own wrapper

Slide 116

Slide 116 text

Integerate into workflow

Slide 117

Slide 117 text

Now try with purchases

Slide 118

Slide 118 text

And In Conclusion...

Slide 119

Slide 119 text

Rails is not your app...

Slide 120

Slide 120 text

Unless you work for Basecamp

Slide 121

Slide 121 text

The goal is to be able to sustainably deliver features

Slide 122

Slide 122 text

Does the code seem better?

Slide 123

Slide 123 text

The right amount of process is a little bit too little process

Slide 124

Slide 124 text

The right amount of code structure might be a little too much code structure

Slide 125

Slide 125 text

"The Long Term" is a fancy way of saying "Tomorrow"

Slide 126

Slide 126 text

OO is about messages, not nouns

Slide 127

Slide 127 text

Small pieces, loosely joined

Slide 128

Slide 128 text

Reuse and maintenance are YAGNI

Slide 129

Slide 129 text

That said, keeping flexible via simplicity is a good idea

Slide 130

Slide 130 text

Ease of testing is a valid architectural concern

Slide 131

Slide 131 text

Duplication may not mean what you think

Slide 132

Slide 132 text

Noel Rappin Table XI @noelrap http://www.noelrappin.com/trdd http://pragprog.com/book/nrtest2