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

  3. I Am Qualified to Speak in Britian

    View Slide

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

    View Slide

  5. PATTERNS

    View Slide

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

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

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

  9. “SOLID” PATTERNS

    View Slide

  10. View Slide

  11. Coupling

    View Slide

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

    View Slide

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  27. MAKE MORE OBJECTS

    View Slide

  28. Sherlock

    View Slide

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

    View Slide

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

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

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

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

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

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

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

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

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

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

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

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

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

  43. THE CASE AGAINST
    CONDITIONALS
    AND NIL

    View Slide

  44. Doctor Who

    View Slide

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

    View Slide

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

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

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

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

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

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

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

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

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

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

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

  57. THE PEN AND PAPER PATTERN

    View Slide

  58. Downton Abbey

    View Slide

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

    View Slide

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

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

    View Slide

  62. THANKS!

    View Slide