Slide 1

Slide 1 text

THE PATTERNS WE ALL NEED TO KNOW Mined from all the “pattern” books

Slide 2

Slide 2 text

James Edward Gray II JEG2 on Twitter, GitHub, etc. I have been in Rubyland for many years, contributing in various ways I am a regular panelist on the Ruby Rogues

Slide 3

Slide 3 text

I Am Qualified to Speak in Britian

Slide 4

Slide 4 text

I Am Qualified to Speak in Britian I know what good television is

Slide 5

Slide 5 text

PATTERNS

Slide 6

Slide 6 text

What is a Pattern? "Each pattern describes a problem that occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice."–Christopher Alexander "Patterns are useful starting points, but they are not destinations."–Martin Fowler

Slide 7

Slide 7 text

Which Ones Did I Include? OK, OK, I didn’t really read all the pattern books But I’ve read a lot of great ones I had to pick and choose which patterns to discuss I tried to stick with those that gave me an “Ah ha!” moment

Slide 8

Slide 8 text

Which Ones Did I Ignore? A lot! Some I don’t think are too valuable: Singleton Some you already know: Iterator Some are (sort of) duplicates: Form Template Method No major “Ah ha!” moment, lack of time/space, no matching British TV show, etc.

Slide 9

Slide 9 text

“SOLID” PATTERNS

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Coupling

Slide 12

Slide 12 text

Coupling Their shtick: showing the same thing from multiple points of view

Slide 13

Slide 13 text

Composed Method “Divide your program into methods that perform one identifiable task.”— Smalltalk Best Practice Patterns “Keep all of the operations in a method at the same level of abstraction.”— Smalltalk Best Practice Patterns The Single Responsibility Principle says, “A class should have only a single responsibility.” Ah ha: SRP applies to methods too

Slide 14

Slide 14 text

Checking DB Existence This probably isn’t the best way require "shellwords" class PostgreSQLDatabase def initialize(name) @name = name end def exist? safe_name = Shellwords.escape(@name) output = `psql -c 'SELECT 1;' #{safe_name} 2>&1` if $?.success? true elsif output.include?(@name) # a DB error vs the expected 1 false else fail(!output.empty? ? output : "Couldn't check databases") end end end p PostgreSQLDatabase.new("my_db").exist?

Slide 15

Slide 15 text

Size Extraction exist?() now has a dual nature problem require "shellwords" class BetterPostgreSQLDatabase def initialize(name) @name = name end def exist? safe_name = Shellwords.escape(@name) output = `psql -c 'SELECT 1;' #{safe_name} 2>&1` parse_exist_command_output($?, output) end private def parse_exist_command_output(status, output) if status.success? true elsif output.include?(@name) # a DB error vs the expected 1 false else fail(!output.empty? ? output : "Couldn't check databases") end end end p BetterPostgreSQLDatabase.new("my_db").exist?

Slide 16

Slide 16 text

Abstraction Extraction Extra credit: the method names hint at another object require "shellwords" class EvenBetterPostgreSQLDatabase def initialize(name) @name = name end def exist? safe_name = escape_db_name_for_exist_command status, output = run_exist_command(safe_name) parse_exist_command_output(status, output) end private def escape_db_name_for_exist_command; Shellwords.escape(@name) end def run_exist_command(safe_name) [`psql -c 'SELECT 1;' #{safe_name} 2>&1`, $?].reverse end def parse_exist_command_output(status, output) if status.success? then true elsif output.include?(@name) then false else fail(!output.empty? ? output : "Couldn't check databases") end end end p EvenBetterPostgreSQLDatabase.new("my_db").exist?

Slide 17

Slide 17 text

Template Method Defines the skeleton of an algorithm Leaves the details to subclasses Details can be abstract methods (required) or hook methods (optional) Ah ha: this is the Open/closed Principle in action (“Software entities should be open for extension, but closed for modification.”)

Slide 18

Slide 18 text

A Save Algorithm Imagine an ActiveRecord style library… class DBRecord # ... def valid? puts "Checking validations..." true end def save if valid? persist end end def persist puts "Writing to disk..." end end

Slide 19

Slide 19 text

Add Before Validation This works, but it introduces some coupling require_relative "db_record" class MyDBRecord < DBRecord def fix_something_before_validation puts "Fixing something required by validation..." end def valid?(*) fix_something_before_validation super end end MyDBRecord.new.save

Slide 20

Slide 20 text

Planned Extension Points This is the Template Method pattern (with hooks) class BetterDBRecord # ... def before_validation; end def after_validation; end def before_save; end def after_save; end def valid? before_validation; puts "Checking validations..."; after_validation true end def save if valid? before_save; persist; after_save end end def persist puts "Writing to disk..." end end

Slide 21

Slide 21 text

Less Coupling It’s like super, without the need for super! require_relative "better_db_record" class MyBetterDBRecord < BetterDBRecord def before_validation puts "Fixing something required by validation..." end end MyBetterDBRecord.new.save

Slide 22

Slide 22 text

Dependency Injection Removes hardcoded dependencies Allows them to change at runtime (or compile time) Doesn’t have to be complex This is the Dependency Inversion Principle in action (“Depend upon abstractions. Do not depend upon concretions.”) Ah ha: just pass the Class

Slide 23

Slide 23 text

Timing Things Imagine a trivial Timer class… class Timer attr_reader :elapsed def start @started = Time.now end def stop raise "You must call start first" unless defined? @started @elapsed = Time.now - @started end end

Slide 24

Slide 24 text

Testing It Our tools are so powerful they almost hide the coupling require_relative "timer" describe Timer do let(:timer) { Timer.new } it "measures elapsed time" do elapsed = 42 started = Time.now Time.stub(:now).and_return(started, started + elapsed) timer.start # ... timer.stop expect(timer.elapsed).to eq(elapsed) end end

Slide 25

Slide 25 text

Pass the Class Dependency Injection in its simplest form class BetterTimer attr_reader :elapsed def start(clock = Time) @started = clock.now end def stop(clock = Time) raise "You must call start first" unless defined? @started @elapsed = clock.now - @started end end

Slide 26

Slide 26 text

No Fancy Tricks Needed We can now pass in whatever we need require_relative "better_timer" describe BetterTimer do let(:timer) { BetterTimer.new } it "measures elapsed time" do elapsed = 42 started = Time.now timer.start(stub(now: started)) # ... timer.stop(stub(now: started + elapsed)) expect(timer.elapsed).to eq(elapsed) end end

Slide 27

Slide 27 text

MAKE MORE OBJECTS

Slide 28

Slide 28 text

Sherlock

Slide 29

Slide 29 text

Sherlock Eliminate the impossible and whatever remains must be true

Slide 30

Slide 30 text

Parameter Object Bundle the parameters to a method up into an object of their own This can make long parameter lists more manageable This is also good for “data clumps” (data that should stay together) Ah ha: we don’t have to cram an entire interface into one method call

Slide 31

Slide 31 text

Booking Hotels Spot the data clump require "date" class HotelStay def initialize(start_date, end_date) @start_date = start_date @end_date = end_date end def book days = (@end_date - @start_date).to_i puts "Reserving a room for #{days} day#{'s' unless days == 1}..." end end today = Date.today HotelStay.new(today, today + 5).book

Slide 32

Slide 32 text

A Parameter Object Notice how this gives us a place to hang convenience functionality require "date" class DateRange def self.days_from(date, days) new(date, date + days) end def self.days_from_today(days) days_from(Date.today, days) end # ... other constructors, if needed... def initialize(start_date, end_date) @start_date = start_date @end_date = end_date end attr_reader :start_date, :end_date def days (end_date - start_date).to_i end end

Slide 33

Slide 33 text

Objects Working Together The date manipulation code left this class for a place that makes more sense require_relative "date_range" class BetterHotelStay def initialize(date_range) @date_range = date_range end def book days = @date_range.days puts "Reserving a room for #{days} day#{'s' unless days == 1}..." end end BetterHotelStay.new(DateRange.days_from_today(5)).book

Slide 34

Slide 34 text

Method Object Moves the body of a complex method into a class of its own This can really help clean up methods that have a lot of parameters and/or use a lot of temporary variables Method objects are easier to refactor, because they’re in their own scope Ah ha: processes can be represented as just another type of object

Slide 35

Slide 35 text

Remember This Example? The private method names are trying to tell us there’s another object here require "shellwords" class EvenBetterPostgreSQLDatabase def initialize(name) @name = name end def exist? safe_name = escape_db_name_for_exist_command status, output = run_exist_command(safe_name) parse_exist_command_output(status, output) end private def escape_db_name_for_exist_command; Shellwords.escape(@name) end def run_exist_command(safe_name) [`psql -c 'SELECT 1;' #{safe_name} 2>&1`, $?].reverse end def parse_exist_command_output(status, output) if status.success? then true elsif output.include?(@name) then false else fail(!output.empty? ? output : "Couldn't check databases") end end end p EvenBetterPostgreSQLDatabase.new("my_db").exist?

Slide 36

Slide 36 text

A Process As an Object I extracted the code and refactored a tiny bit require "shellwords" class PostgreSQLExistCommand def initialize(database) @database = database end def result parse(*run) end private def run output = `psql -c 'SELECT 1;' #{Shellwords.escape(@database)} 2>&1` [$?, output] end def parse(status, output) if status.success? then true elsif output.include?(@database) then false else fail(!output.empty? ? output : "Couldn't check databases") end end end

Slide 37

Slide 37 text

Back to Objects We’ve dropped the external process details from here, where they didn’t fit require_relative "postgresql_exist_command" class EvenMoreBetterPostgreSQLDatabase def initialize(name) @name = name end def exist? PostgreSQLExistCommand.new(@name).result end end p EvenMoreBetterPostgreSQLDatabase.new("my_db").exist?

Slide 38

Slide 38 text

Decorator This pattern extends the functionality of some object The object is passed to the constructor and the decorator wraps the methods to change with the additional code Ah ha: this is one form of OO layering

Slide 39

Slide 39 text

Too Tightly Coupled What if I want to build a new User in the console without sending an email? class User < ActiveRecord::Base after_commit :send_welcome_email, on: :create def send_welcome_email UserMailer.welcome(self).deliver end end

Slide 40

Slide 40 text

Too Loosely Coupled Want if an import task also creates Users that should receive an email? class UserController < ApplicationController def create user = User.new(params[:user]) if user.save UserMailer.welcome(user).deliver # ... else # ... end end end

Slide 41

Slide 41 text

Layered Functionality This allows us to add in email sending when needed class EmailOnSave def initialize(model, email, mailer = "#{model.class}Mailer".constantize) @model = model @email = email @mailer = mailer end def save(*args) result = @model.save(*args) @mailer.send(@email, @model).deliver if result result end end

Slide 42

Slide 42 text

Choose Which Layers to Use Less manual and less easy to forget email management class BetterUserController < ApplicationController def create user = EmailOnSave.new(User.new(params[:user]), :welcome) if user.save # ... else # ... end end end

Slide 43

Slide 43 text

THE CASE AGAINST CONDITIONALS AND NIL

Slide 44

Slide 44 text

Doctor Who

Slide 45

Slide 45 text

Doctor Who I’ll give the Dalek’s something worth exterminating

Slide 46

Slide 46 text

Strategy Defines a family of interchangeable algorithms Client code then selects the desired algorithm at runtime Ah ha: many conditional branches are just objects waiting to be built

Slide 47

Slide 47 text

Two Ways to “Render” Some JSON Imagine we have a need for efficient and readable outputs… require "json" class Article def initialize(title, body) @title = title @body = body end def to_json(pretty = false) data = {title: @title, body: @body} if pretty JSON.pretty_generate(data) else JSON.generate(data) end end end article = Article.new("Strategy Pattern", "Objects making decisions...") puts article.to_json puts article.to_json(:pretty)

Slide 48

Slide 48 text

Branch to Object First we make simple objects for the choices require "json" class JSONGenerator def generate(data) JSON.generate(data) end end class PrettyJSONGenerator def generate(data) JSON.pretty_generate(data) end end

Slide 49

Slide 49 text

Just Objects Sending Messages This could easily support other rendering schemes too require_relative "generators" class BetterArticle def initialize(title, body) @title = title @body = body end def to_json(generator = JSONGenerator.new) generator.generate(title: @title, body: @body) end end article = BetterArticle.new("Strategy Pattern", "Objects making decisions...") puts article.to_json puts article.to_json(PrettyJSONGenerator.new)

Slide 50

Slide 50 text

Strategy (cont.) I said, “Client code then selects the desired algorithm at runtime” Ah ha: this doesn’t have to be hardcoded object matchmaking

Slide 51

Slide 51 text

A Trivial Command Interpreter The branch selection is determined by the input require "shellwords" class Vocalizer def run loop do print "> " input = gets if input.nil? || input =~ /\Aquit\b/i exit elsif input =~ /\Aspeak\s+(.+)/i system("say #{Shellwords.escape($1)}") else "Command not found." end end end end Vocalizer.new.run

Slide 52

Slide 52 text

match?() and execute() Commands decide if input is for them and handle it require "shellwords" class QuitCommand def match?(input) input.nil? || input =~ /\Aquit\b/i end def execute exit end end class SpeakCommand def match?(input) @words = input[/\Aspeak\s+(.+)/i, 1] end def execute system("say #{Shellwords.escape(@words)}") end end

Slide 53

Slide 53 text

Dynamic Command Selection Notice that I didn’t quite kill all of the conditionals here require_relative "commands" class BetterVocalizer def commands @commands ||= [QuitCommand, SpeakCommand] end def run loop do print "> " input = gets matched = commands.map(&:new).find { |command| command.match?(input) } if matched matched.execute else "Command not found." end end end end BetterVocalizer.new.run

Slide 54

Slide 54 text

Special Case/Null Object These two patterns are similar “I see Null Object as special case of Special Case.”—Martin Fowler Replace code that handles some special case with an object that matches the normal case, but does the special behavior Checks for nil are always special case code Ah ha: avoiding nil simplifies code and makes it more expressive

Slide 55

Slide 55 text

Add One More Command This is just the special case in normal case clothing # ... class NotFoundCommand def match?(_) true end def execute puts "Command not found." end end

Slide 56

Slide 56 text

No if, No nil The code just gets simpler and simpler require_relative "commands" class EvenBetterVocalizer def commands @commands ||= [QuitCommand, SpeakCommand, NotFoundCommand] end def run loop do print "> " input = gets commands.map(&:new).find { |command| command.match?(input) }.execute end end end EvenBetterVocalizer.new.run

Slide 57

Slide 57 text

THE PEN AND PAPER PATTERN

Slide 58

Slide 58 text

Downton Abbey

Slide 59

Slide 59 text

Downton Abbey Sometimes the old ways really are the best

Slide 60

Slide 60 text

State Machine This pattern details how and object can change behavior based on its internal state It is a design pattern that comes up in code quite a bit Ah ha: it’s also extremely helpful as a thinking tool when planning how an object changes over time or design request interactions, like Hypermedia API’s

Slide 61

Slide 61 text

A Hypermedia Blog API Now that I can see the connections, I know where to include links and such

Slide 62

Slide 62 text

THANKS!