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
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
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
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.
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
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?
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?
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?
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.”)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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?
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
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?
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
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
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
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
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
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
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)
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
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)
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
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
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
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
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
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
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