Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Patterns We All Need to Know

jeg2
May 12, 2013

The Patterns We All Need to Know

My talk at Scottish Ruby Conference 2013.

jeg2

May 12, 2013
Tweet

More Decks by jeg2

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. 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.
  5. 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
  6. 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?
  7. 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?
  8. 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?
  9. 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.”)
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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?
  25. 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
  26. 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?
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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)
  34. 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
  35. 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)
  36. Strategy (cont.) I said, “Client code then selects the desired

    algorithm at runtime” Ah ha: this doesn’t have to be hardcoded object matchmaking
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. A Hypermedia Blog API Now that I can see the

    connections, I know where to include links and such