Skip to content

a-strug/ruby-patterns

 
 

Repository files navigation

ruby-patterns

Examples of Patterns in Ruby

Table of Contents

  1. Adapter
  2. Builder
  3. Command
  4. Composite
  5. Decorator
  6. Facade
  7. Factory
  8. Interpreter
  9. Iterator
  10. Observer
  11. Proxy
  12. Singleton
  13. State
  14. Strategy
  15. Template

Adapter

  • Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.[link]
class Quest
  attr_accessor :difficulty, :hero

  def initialize(difficulty)
    @difficulty = difficulty
    @hero = nil
  end

  def finish
    @hero.exp += calculate_experience
  end

  def calculate_experience
    @difficulty * 50 / @hero.level
  end
end

class Hero
  attr_accessor :level, :exp, :quests

  def initialize
    @level = 1
    @exp = 0
    @quests = []
  end

  def take_quest(quest)
    @quests << (quest.hero = self)
  end

  def finish_quest(quest)
    quest.finish
    @quests.delete quest
  end
end

class OldQuest
  attr_accessor :hero, :difficulty, :experience

  def initialize
    @difficulty = 3
    @experience = 10
  end

  def done
    difficulty * experience
  end
end

class QuestAdapter
  attr_accessor :hero

  def initialize(old_quest, difficulty)
    @old_quest = old_quest
    @old_quest.difficulty = difficulty
    @hero = nil
  end

  def finish
    @hero.exp += @old_quest.done
  end
end

# Usage
hero = Hero.new
quest = Quest.new 5
hero.take_quest quest
hero.finish_quest quest
puts hero.exp
# => 250
some_old_quest = OldQuest.new
old_quest_adapted = QuestAdapter.new(some_old_quest, 5)
hero.take_quest old_quest_adapted
hero.finish_quest old_quest_adapted
puts hero.exp
# => 300

Back to top

Builder

  • Separate the construction of a complex object from its representation so that the same construction process can create different representations.[link]
class BoardBuilder
  def initialize(width, height)
    @board = Board.new
    @board.width = width
    @board.height = height
    @board.tiles = []
    @board.monsters = []
  end

  def add_tiles(n)
    n.times{ @board.tiles << Tile.new }
  end

  def add_monsters(n)
    n.times{ @board.monsters << Monster.new }
  end

  def board
    @board
  end
end

class Board
  attr_accessor :width, :height, :tiles, :monsters
  def initialize
  end
end

class Tile; end
class Monster; end

# Usage
builder = BoardBuilder.new 2, 3
puts builder.board
# => Board Object
board = builder.board
puts board.width
# => 2
builder.add_tiles(3)
builder.add_monsters(2)
puts board.tiles.size
# => 3
puts board.monsters.size
# => 2

Back to top

Command

  • Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.[link]
class Turn
  def initialize
    @commands = []
  end

  def run_command(command)
    command.execute
    @commands << command
  end

  def undo_command
    @commands.pop.unexecute
  end
end

class Hero
  attr_accessor :money, :health

  def initialize
    @money = 0
    @health = 100
  end
end

class GetMoneyCommand
  def initialize(hero)
    @hero = hero
  end

  def execute
    @hero.money += 10
  end

  def unexecute
    @hero.money -= 10
  end
end

class HealCommand
  def initialize(hero)
    @hero = hero
  end

  def execute
    @hero.health += 10
  end

  def unexecute
    @hero.health -= 10
  end
end

# Usage
hero = Hero.new
get_money = GetMoneyCommand.new hero
heal = HealCommand.new hero
turn = Turn.new
turn.run_command(heal)
puts hero.health
# => 110
turn.run_command(get_money)
puts hero.money
# => 10
turn.undo_command
puts hero.money
# => 0

Back to top

Composite

  • Composition over inheritance. Compose objects into tree structures to represent part-whole hierarchies.[link]
class CompositeQuest
  def initialize
    @tasks = []
  end

  def <<(task)
    @tasks << task
  end

  def reward
    @tasks.inject(0){ |sum, task| sum += task.reward }
  end
end

class MegaQuest < CompositeQuest
end

class Quest < CompositeQuest
end

class MonsterTask
  attr_reader :reward

  def initialize
    @reward = 100
  end
end

class PuzzleTask
  attr_reader :reward

  def initialize
    @reward = 200
  end
end

# Usage
quest1 = Quest.new
quest1 << MonsterTask.new
quest1 << PuzzleTask.new
puts quest1.reward
# => 300

quest2 = Quest.new
quest2 << MonsterTask.new
quest2 << PuzzleTask.new
megaquest = MegaQuest.new
megaquest << quest1
megaquest << quest2
puts megaquest.reward
# => 600

Back to top

Decorator

  • Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.[link]
class ItemDecorator
  def initialize(item)
    @item = item
  end
  # this needs to be delegated with other efective way
  def use
    @item.use
  end
end

class MagicItemDecorator < ItemDecorator
  def price
    @item.price * 3
  end

  def description
    @item.description + "Magic"
  end
end

class MasterpieceItemDecorator < ItemDecorator
  def price
    @item.price * 2
  end

  def description
    @item.description + "Masterpiece"
  end
end

class Item
  attr_reader :price, :description
  def initialize
    @price = 10
    @description = "Item "
  end

  def use
    "do something"
  end
end

# Usage
item = Item.new
magic_item = MagicItemDecorator.new(item)
puts magic_item.price
# => 30
puts magic_item.description
# => Item Magic

masterpiece_item = MasterpieceItemDecorator.new(item)
puts masterpiece_item.price
# => 20
puts masterpiece_item.description
# => Item Masterpiece

# all next lines puts "do something"
item.use
magic_item.use
masterpiece_item.use

Back to top

Facade

  • The goal of the Facade Pattern is to provide a unified interface to a set of interfaces in a subsystem. This means you'd just have some object that can send back other objects.[link]
class Hero
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def join(level)
    puts "#{self.name} join #{level}\n"
  end

  def attack(enemy)
    puts "#{self.name} kick #{enemy}\n"
  end
end

class Enemy
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def dead(hero)
    puts "#{self.name} killed by #{hero}"
  end
end

class Level
  attr_reader :stage

  def initialize(stage)
    @stage = stage
  end

  def to_s
    stage
  end
end

class GameFacade
  attr_reader :hero, :enemy, :level

  def initialize
    @hero  = Hero.new('Sonic')
    @enemy = Enemy.new('Eggman')
    @level = Level.new('Green Hill')
  end

  def start_game
    hero.join(level)
    hero.attack(enemy.name)
    enemy.dead(hero.name)
  end
end

game = GameFacade.new
game.start_game
# => Sonic join Green Hill
#    Sonic kick Eggman
#    Eggman killed by Sonic

Back to top

Factory

  • Define an interface for creating an object, but let subclasses decide which class to instantiate.[link]
class Party
  attr_reader :members
  def initialize(factory)
    @members = []
    @factory = factory
  end

  def add_warriors(n)
    n.times{ @members << @factory.create_warrior }
  end

  def add_mages(n)
    n.times{ @members << @factory.create_mage }
  end
end

class HeroFactory
  def create_warrior
    Warrior.new
  end

  def create_mage
    Mage.new
  end
end

class Hero
  def initialize
  end
end

class Warrior < Hero
end

class Mage < Hero
end

# Usage
party = Party.new(HeroFactory.new)
party.add_warriors(3)
party.add_mages(2)
puts party.members.size
# => 5
puts party.members.count{ |member| member.class == "Mage" }
# => 2

Back to top

Interpreter

  • This pattern provides an interpreter to deal with an abstract language. Using classes we can understand the inputs for parse them.[link]
class Word
  def initialize(value)
    @value = value
  end

  def execute
    @value
  end
end

class Plus
  def initialize(first, second)
    @first = first
    @second = second
  end

  def execute
    @first.execute + @second.execute
  end
end

class Minus
  def initialize(first, second)
    @first = first
    @second = second
  end

  def execute
    index = @first.execute =~ /#{@second.execute}/
    second_index = index + @second.execute.length
    @first.execute[0,index] + @first.execute[second_index..-1]
  end
end

class Interpreter
  def self.parse(input)
    @waiting_second_word = false
    words = []
    operations = []
    input.split.each_with_index do |value|
      if value =~ /^[^+-].*/ && !@waiting_second_word
        words << Word.new(value)
      else
        if symbol = operations.pop()
          first = words.size > 1 ? Word.new(words.map(&:execute).join(" ")) :
            words.pop
          second = Word.new(value)
          case symbol
          when /\A\+/
            words << Word.new(Plus.new(first, second).execute)
          when /\A\-/
            words << Word.new(Minus.new(first, second).execute)
          end
          @waiting_second_word = false
        else
          @waiting_second_word = true
          operations << value
        end
      end
    end
    words.pop.execute
  end
end

puts Interpreter.parse("NA + NA + NA + BATMAN")
#=> NANANABATMAN

puts Interpreter.parse("you know nothing Jon Snow - nothing")
#=> you know Jon Snow

puts Interpreter.parse("hello + world - llowo")
#=>herld

Back to top

Iterator

  • Iterator helps you to iterate through a complex object using an iterator method.[link]
class Parent
  attr_reader :first_name

  def initialize(first_name, gender)
    @first_name = first_name
    @gender = gender
  end
end

class Child < Parent
end

class Family
  def initialize(surname)
    @surname = surname
    @children = []
  end

  def add_father(first_name)
    @father = Parent.new first_name, "M"
  end

  def add_mother(first_name)
    @mother = Parent.new first_name, "F"
  end

  def add_child(first_name, gender)
    @children << Child.new(first_name, gender)
  end

  def each_member
    [@father, @mother, @children].flatten.each do |member|
      yield member
    end
  end
end

# Usage
family = Family.new "Jackson"
family.add_father("Robert")
family.add_mother("Susan")
family.add_child("Lucas", "M")
family.add_child("James", "M")
family.add_child("Rose", "F")
family.each_member{ |member| puts member.first_name }
# => Robert
#    Susan
#    Lucas
#    James
#    Rose

Back to top

Observer

  • Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.[link]
module Observable
  attr_reader :observers

  def initialize(attrs = {})
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def notify_observers
    @observers.each{ |observer| observer.update }
  end
end

class Tile
  include Observable

  def initialize(attrs = {})
    super
    @cursed = attrs.fetch(:cursed, false)
  end

  def cursed?
    @cursed
  end

  def activate_curse
    notify_observers
  end
end

class Hero
  attr_reader :health

  def initialize
    @cursed = false
    @health = 10
  end

  def damage(hit)
    @health -= hit
  end

  def cursed?
    @cursed
  end

  def discover(tile)
    if tile.cursed?
      @cursed = true
      tile.add_observer(self)
    end
  end

  def update
    damage(4)
  end
end

# Usage
hero = Hero.new
tile = Tile.new cursed: true
hero.discover(tile)
tile.activate_curse
puts hero.health
# => 6
puts hero.cursed?
# => true

Back to top

Proxy

  • Provide a surrogate or placeholder for another object to control access to it.[link]
require 'forwardable'

class Hero
  attr_accessor :keywords

  def initialize
    @keywords = []
  end
end

class ComputerProxy
  # Forwardable allows objects to run methods on behalf
  # of it's members, in this case the Computer object
  extend Forwardable

  # We delegate the ComputerProxy's use of
  # the Computer object's add method
  def_delegators :real_object, :add

  def initialize(hero)
    @hero = hero
  end

  def execute
    check_access
    real_object.execute
  end

  def check_access
    unless @hero.keywords.include?(:computer)
      raise "You have no access"
    end
  end

  def real_object
    @real_object ||= Computer.new
  end
end

class Computer
  def initialize
    @queue = []
  end

  def add(command)
    @queue << command
  end

  def execute
    "executing commands"
  end
end

# Usage
hero = Hero.new
proxy = ComputerProxy.new(hero)
proxy.add("some command")
proxy.execute
# => raise error
hero.keywords << :computer
proxy.execute
# => executing commands

Back to top

Singleton

  • Define a unique instance of an object.[link]
class HeroFactory
  @@instance = nil

  def self.instance
    @@instance ||= HeroFactory.send(:new)
  end

  def create_warrior
    Warrior.new
  end

  def create_mage
    Mage.new
  end

  private_class_method :new
end

# Usage
factory = HeroFactory.instance
another_factory = HeroFactory.instance
puts factory == another_factory
# => true
HeroFactory.new
# => Raise Exception

Back to top

State

  • This pattern tries to simplify complicated control flows changing an object's behavior dynamically.[link]
class Operation
  attr_reader :state
  def initialize
    @state = OperationOpenState.new
  end

  def trigger(state)
    @state = @state.next(state)
  end
end

class OperationOpenState
  def next(state)
    if valid?(state)
      OperationPendingPaymentState.new
    else
      raise IllegalStateJumpError
    end
  end

  def valid?(state)
    state == :pending_payment
  end
end

class OperationPendingPaymentState
  def next(state)
    OperationConfirmState.new if valid?(state)
  end

  def valid?(state)
    state == :confirm
  end
end
class IllegalStateJumpError < StandardError; end
class OperationConfirmState; end

#Usage
operation = Operation.new
puts operation.state.class
#=> OperationOpenState
operation.trigger :pending_payment
puts operation.state.class
#=> OperationPendingPaymentState
operation.trigger :confirm
puts operation.state.class
#=> OperationConfirmState

operation = Operation.new
operation.trigger :confirm
#=> raise IllegalStateJumpError

Back to top

Strategy

  • Define a family of algorithms, encapsulate each one, and make them interchangeable.[link]
class Hero
  attr_reader :damage, :health, :skills
  attr_accessor :printer

  def initialize(printer)
    @damage = 10
    @health = 5
    @printer = printer
    @skills = [:stealth, :driving, :intimidation]
  end

  def print_stats
    if block_given?
      yield(damage, health, skills)
    else
      printer.print(damage, health, skills)
    end
  end
end

class BattleStats
  def print(damage, health, skills)
    "Damage: #{damage}\nHealth: #{health}"
  end
end

class SkillStats
  def print(damage, health, skills)
    skills.inject(""){ |result, skill| result + skill.to_s.capitalize + "\n" }
  end
end

# Usage
Hero.new(BattleStats.new).print_stats
# => Damage: 10
#    Health: 5

Hero.new(SkillStats.new).print_stats
# => Stealth
#    Driving
#    Intimidation

Hero.new(any_printer).print_stats do |damage, health, skills|
  "Looks: I'm printing a customize message about my hero with damage #{damage} and number of skills: #{skills.size}"
end

Back to top

Template

  • Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template methods lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.[link]
class Hero
  attr_reader :damage, :abilities

  def initialize
    @damage = damage_rating
    @abilities = occupation_abilities
  end

  def greet
    greeting = ["Hello"]
    greeting << unique_greeting_line
    greeting
  end

  def unique_greeting_line
    raise "You must define unique_greeting_line"
  end

  def damage_rating
    10
  end

  def occupation_abilities
    []
  end

  def attack
    "Atack dealing #{damage} damage"
  end
end

class Warrior < Hero
  def damage_rating
    15
  end

  def occupation_abilities
    [:strike]
  end

  def unique_greeting_line
    "Warrior is ready to fight!"
  end
end

class Mage < Hero
  def damage_rating
    7
  end

  def occupation_abilities
    [:magic_spell]
  end

  def unique_greeting_line
    "Mage is ready to make powerful spells!"
  end
end

Back to top

About

Examples of Patterns in Ruby

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Ruby 100.0%