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. THE PATTERNS
    WE ALL NEED
    TO KNOW
    Mined from all the “pattern” books

    View full-size slide

  2. 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

    View full-size slide

  3. I Am Qualified to Speak in Britian

    View full-size slide

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

    View full-size slide

  5. 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

    View full-size slide

  6. 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

    View full-size slide

  7. 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.

    View full-size slide

  8. “SOLID” PATTERNS

    View full-size slide

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

    View full-size slide

  10. 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

    View full-size slide

  11. 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?

    View full-size slide

  12. 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?

    View full-size slide

  13. 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?

    View full-size slide

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

    View full-size slide

  15. 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

    View full-size slide

  16. 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

    View full-size slide

  17. 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

    View full-size slide

  18. 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

    View full-size slide

  19. 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

    View full-size slide

  20. 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

    View full-size slide

  21. 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

    View full-size slide

  22. 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

    View full-size slide

  23. 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

    View full-size slide

  24. MAKE MORE OBJECTS

    View full-size slide

  25. Sherlock
    Eliminate the impossible and whatever remains must be true

    View full-size slide

  26. 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

    View full-size slide

  27. 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

    View full-size slide

  28. 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

    View full-size slide

  29. 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

    View full-size slide

  30. 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

    View full-size slide

  31. 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?

    View full-size slide

  32. 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

    View full-size slide

  33. 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?

    View full-size slide

  34. 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

    View full-size slide

  35. 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

    View full-size slide

  36. 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

    View full-size slide

  37. 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

    View full-size slide

  38. 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

    View full-size slide

  39. THE CASE AGAINST
    CONDITIONALS
    AND NIL

    View full-size slide

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

    View full-size slide

  41. 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

    View full-size slide

  42. 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)

    View full-size slide

  43. 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

    View full-size slide

  44. 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)

    View full-size slide

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

    View full-size slide

  46. 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

    View full-size slide

  47. 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

    View full-size slide

  48. 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

    View full-size slide

  49. 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

    View full-size slide

  50. 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

    View full-size slide

  51. 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

    View full-size slide

  52. THE PEN AND PAPER PATTERN

    View full-size slide

  53. Downton Abbey

    View full-size slide

  54. Downton Abbey
    Sometimes the old ways really are the best

    View full-size slide

  55. 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

    View full-size slide

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

    View full-size slide