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
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
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.
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
"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?
"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?
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?
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.”)
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
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
require_relative "better_db_record" class MyBetterDBRecord < BetterDBRecord def before_validation puts "Fixing something required by validation..." end end MyBetterDBRecord.new.save
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
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
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
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
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
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
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
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
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?
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
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?
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
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
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
to forget email management class BetterUserController < ApplicationController def create user = EmailOnSave.new(User.new(params[:user]), :welcome) if user.save # ... else # ... end end end
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)
choices require "json" class JSONGenerator def generate(data) JSON.generate(data) end end class PrettyJSONGenerator def generate(data) JSON.pretty_generate(data) end end
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)
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
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
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
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
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
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