WEB系エンジニアの勉強日記

Baby steps to Giant strides!

Command pattern - Design Pattern in Ruby

「何かを実行するもの」をオブジェクトとして扱う方法。

これによる恩恵として、実行の記録の保管が容易になり、undo, redoなどの実装もしやすくなる。

よく使用されるケースとしては、GUIのパーツで、パーツのデザインや配置などの処理と、何かしらのアクションに応じた処理を切り離して管理することができる。

compositeパターンの考え方を用いてCompositeCommandを実装すると、複数のコマンドを単一のコマンドと同じように扱うことができる。

簡易実装

ボードにピースを配置するという簡単なプログラムで、ピースの追加、移動等の操作をCommandとして実装した。

class Board
  attr_accessor :pieces
  def initialize (size)
    @pieces = Array.new size
    @commands = []
    @command_index = 0
  end

  def execute_command(command)
    puts command.describe
    command.execute
    display_state
    @commands[@command_index] = command
    @command_index += 1
  end

  def undo
    puts 'Undo:'
    command = @commands[@command_index - 1]
    puts "\t" + command.describe
    command.unexecute
    display_state
    @command_index -= 1
  end

  def display_state
    state = '|'
    @pieces.each do |ele|
      if ele
        state << ele.name
      else
        state << '  '
      end
      state << '|'
    end
    puts state
  end
end

class Piece
  attr_accessor :name
  def initialize(name)
    @name = name
  end
end

class CompositeCommand
  def initialize
    @commands = []
  end

  def add_command (command)
    @commands << command
  end

  def execute
    @commands.each {|c| c.execute}
  end

  def unexecute
    @commands.reverse.each {|c| c.unexecute}
  end

  def describe
    @commands.map{|c| c.describe}.join "\n"
  end
end

class AddPieceCommand
  def initialize (piece, index, board)
    @piece = piece
    @index = index
    @board = board
    @previousPiece
  end

  def execute
    @previousPiece = @board.pieces[@index]
    @board.pieces[@index] = @piece
  end

  def unexecute
    @board.pieces[@index] = @previousPiece
  end

  def describe
    "Add Piece: #{@piece.name} to #{@index}"
  end
end

board = Board.new 8

initial_command = CompositeCommand.new
initial_command.add_command AddPieceCommand.new Piece.new('A'), 2, board
initial_command.add_command AddPieceCommand.new Piece.new('B'), 4, board

board.execute_command initial_command
# Add Piece: A to 2
# Add Piece: B to 4
# |  |  |A|  |B|  |  |  |

board.undo
# Undo:
# Add Piece: A to 2
# Add Piece: B to 4
# |  |  |  |  |  |  |  |  |

ActionController#respond_to - code reading

requestのMIME TYPEに応じたレスポンスを定義する機能

定義は

rails/mime_responds.rb at 48f140cf7459c963a54637c897448b959dbbfd26 · rails/rails · GitHub

ActionController:: MimeResponds#respond_to

def respond_to(*mimes)
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?

      collector = Collector.new(mimes, request.variant)
      yield collector if block_given?

      if format = collector.negotiate_format(request)
        _process_format(format)
        _set_rendered_content_type format
        response = collector.response
        response.call if response
      else
        raise ActionController::UnknownFormat
      end
    end

Collectorを生成して、blockが与えられている場合は、collector を yield させる。

なので、よくある下記のようなコードのformatの中身はCollectorのインスタンス

respond_to do |format|
      if @kihu.save
        format.html { redirect_to @kihu, notice: 'Kihu was successfully created.' }
        format.json { render :show, status: :created, location: @kihu }
      else
        format.html { render :new }
        format.json { render json: @kihu.errors, status: :unprocessable_entity }
      end
    end

与えられたブロックの処理を通した後に、negotiate_formatで返信すべきMIMEタイプを取得し、

collector.responseでレスポンスを生成するProcを取得し、実行している。

ActionController:: MimeResponds::Collector

いわゆるformatの実態で、respond_toで渡されるブロックで定義されるMIMEタイプごとの処理を保持する。

format.html を呼び出した場合、includeされているAbstractController::Collectorで定義されているmethod_missingを通して、下記の処理が呼び出され、mimeタイプの名前のメソッドが作成され、実行される。

# AbstractController::Collector. generate_method_for_mimeを実行し、呼び出している。

def method_missing(symbol, &block)
      unless mime_constant = Mime[symbol]
        raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
          "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
          "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
          "be sure to nest your variant response within a format response: " \
          "format.html { |html| html.tablet { ... } }"
      end

      if Mime::SET.include?(mime_constant)
        AbstractController::Collector.generate_method_for_mime(mime_constant)
        send(symbol, &block)
      else
        super
      end
    end

rails/collector.rb at 565094a8b5cdfa158fef6ae75252fd98a4ba8fe4 · rails/rails · GitHub

def self.generate_method_for_mime(mime)
      sym = mime.is_a?(Symbol) ? mime : mime.to_sym
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{sym}(*args, &block)
          custom(Mime[:#{sym}], *args, &block)
        end
      RUBY
    end

customを実行するメソッドを定義している。

custom内では、@responses[mime_type]にレスポンス処理をするProcを保管。

blockが与えられていない場合、VariantCollectorを生成して保管しているが、この辺りはよくわからないので後回し。

def custom(mime_type, &block)
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
        @responses[mime_type] ||= if block_given?
          block
        else
          VariantCollector.new(@variant)
        end
      end

上記で保持されたProcが、#responseで返され、#respond_to内で呼び出される。

Iterator pattern - Design Pattern in Ruby

複数の要素を持つオブジェクトが、それぞれの要素に順繰りにアクセスする方法を提供するパターン。

  • External iterator

    has_next, next_item のような順繰りに必要なメソッドを提供することで、外部からの要素へのアクセスを可能にする。

    loop処理などを自分で書く必要があるが、その分柔軟な記述が可能

  • Internal iterator

    要素を引数にもったコードブロックを渡して、内部で各要素付きで実行してもらう。

    簡潔に処理したいコードを書くことができる。

実装

eachを実装して、Enumerableをincludeするミニマムのコードを書いてみた。


class Player
  attr_reader :time
  def initialize (name, time)
    @name = name
    @time = time
  end

  def <=> (other)
    @time <=> other.time
  end
end

class PlayerGroup
  include Enumerable
  def initialize
    @players = []
  end

  def add_player (player)
    @players << player
  end

  def each (&block)
    @players.each {|p| yield p}
  end
end

pg = PlayerGroup.new
pg.add_player Player.new 'kiryuu', 10.0
pg.add_player Player.new 'bolt', 9.58
pg.add_player Player.new 'gatorin', 9.81

p pg.sort

Composite pattern - Design Pattern in Ruby Chapter 6

共通のオペレーションを行う要素と、その要素の集合体を同じように扱うことで、複雑な構造をシンプルに扱う。

用語

  • Component 共通のインターフェース

  • Leaf ミニマムの要素

  • Composite 要素の集合体

注意点

compositeとleaf,異なる属性のものを全く同じように扱うのは無理。

目的に沿って、うまく設計する必要がある。

実践

ViewCompnentをサイズを共通オペレーションとして持つような想定で実装.

class ViewComponent
  attr_accessor :name, :position
  def initialize(name, position)
    @name = name
    @position = position
  end
  def get_size
    {
      width: 0,
      height: 0
    }
  end
end

class Element < ViewComponent
  def get_size
    {
      width: 50,
      height: 100
    }
  end
end

class ViewContainer < ViewComponent
  def initialize(name, position)
    super(name, position)
    @children = []
  end

  def add_child(child)
    @children << child
  end

  def get_size
    width = 0
    height = 0
    @children.each do |child|
      w = child.position[:x] + child.get_size[:width]
      h = child.position[:y] + child.get_size[:height]
      width = w if w > width
      height = h if h > height
    end
    {
      width: width,
      height: height
    }
  end
end

box = ViewContainer.new 'Box', {x: 10, y: 10}
box.add_child Element.new 'ele1', {x:20, y: 20}
p box.get_size
# {:width=>70, :height=>120}

box.add_child Element.new 'ele2', {x:100, y: 200}
p box.get_size
#{:width=>150, :height=>300}

Observer Pattern - Design Pattern i Ruby Chapter 5

注意点

  • 更新頻度を考える。 考えなしにすべての変更時にobserverオブジェクトに通知すると、かなりの数の冗長な通知が飛び交ってしまう可能性がある。 どういったタミングで通知を行うかの戦略は考えておくべき。

  • 変更の一貫性 上記の高新頻度にも似ているが、細かい情報の変更を全て通知していると、整合性が取れなくなるケースが考えられる。 どういった変更のまとまりで通知するかを考えておく必要がある。

バリエーション

ruby標準のobserver

module Observable (Ruby 1.8.7)

updateメソッドを実装したobserverオブジェクトを定義することで簡単に実装できる。


コードベースのobserver

observerオブジェクトにupdateメソッドを実装したクラスのオブジェクトではなく、Procオブジェクトを使うことで、より簡単にObserverオブジェクトを登録できる。


updateメソッドの引数

一番簡単な実装はシンプルにselfを渡す実装だけど、この場合はobserverがobservableオブジェクトの内容から変更箇所を特定するような処理が必要になったりする。

こういったことが問題になるようなケースでは、変更箇所ごとに細かいupdate_***系のメソッドを用意するといい。

ただこの場合だと標準のobserverモジュールが使えないため、自分でmoduleを定義する必要がある。

実践

NewsFeedの通知を受け取るような想定で実装

require 'observer'

class NewsFeed
  include Observable
  def initialize
    @news = []
    @next_news_id = 1
  end

  def add_news(title)
    @news << {
      id: @next_news_id,
      title: title
    }
    @next_news_id += 1
    changed
  end

  def publish
    notify_observers self
  end

  def getNewsAfter(id)
    @news.select {|news| news[:id] > id}
  end
end

class NewsListener
  def initialize
    @last_received_news_id = 0
  end

  def update(news_feed)
    unread = news_feed.getNewsAfter @last_received_news_id
    @last_received_news_id = unread.last[:id]
    p unread
  end
end

news_feed = NewsFeed.new
news_feed.add_observer NewsListener.new

news_feed.add_news 'news1'
news_feed.publish

news_feed.add_news 'news2'
news_feed.add_news 'news3'
news_feed.publish

Strategy Pattern - Design Pattern in Ruby Chapter 5

www.amazon.co.jp

まとめ

Templateパターンと同じ問題を解決するが、compositionとdelegateの形式で行う分、疎結合で柔軟な設計になる。


似たようなことを行うオブジェクト(family objects)をまとめ同じインターフェースを定義することで、使う側のオブジェクト(context object)から、簡単に入れ替えができるようになる。


rubyの場合、シンプルなStrategy patternはProcオブジェクトベースで実現できる。


Strategyパターンを用いるときに注意することは、contextとstrategyのインターフェース。


1つ目のstrategyオブジェクトを複雑にタイトに連携したインターフェースにしてしまうと、それ以降のstrategyオブジェクトをうまく扱えなくなってしまう。

ProcベースのStrategy Patternの実施

Array#bubble_sort!のオーダー順をProcベースのStrategy Patternで実装してみた。

class Array

  def bubble_sort! (&ordering)

    (self.size - 1).times do     
      for i in 0...(self.size - 1)
        if ordering.call(self[i], self[i + 1]) > 0
          tmp = self[i + 1]
          self[i + 1] = self[i]
          self[i] = tmp
        end
      end
    end
  end
end

array = [1, 3, 10, 9]
array.bubble_sort! {|a, b| a <=> b}
# [1, 3, 9, 10]

array.bubble_sort! {|a, b| b <=> a}
# [10, 9, 3, 1]

extends core Date - Rails code reading

activesupport/lib/active_support/core_ext/date/conversions.rb

to_sで指定できるフォーマットを拡張している。

一番上に対応するフォーマットが定義されている。

class Date
  DATE_FORMATS = {
    :short        => '%d %b',
    :long         => '%B %d, %Y',
    :db           => '%Y-%m-%d',
    :number       => '%Y%m%d',
    :long_ordinal => lambda { |date|
      day_format = ActiveSupport::Inflector.ordinalize(date.day)
      date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
    },
    :rfc822       => '%d %b %Y',
    :iso8601      => lambda { |date| date.iso8601 }
  }

formatが文字列かlamdaかをrespond_to?(:call) で判別している。

def to_formatted_s(format = :default)
    if formatter = DATE_FORMATS[format]
      if formatter.respond_to?(:call)
        formatter.call(self).to_s
      else
        strftime(formatter)
      end
    else
      to_default_s
    end
  end
  alias_method :to_default_s, :to_s
  alias_method :to_s, :to_formatted_s

activesupport/lib/active_support/core_ext/date/calculations.rb

Date#+が日の加算処理、Date#>>が月の加算処理

# Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
  # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
  def advance(options)
    options = options.dup
    d = self
    d = d >> options.delete(:years) * 12 if options[:years]
    d = d >> options.delete(:months)     if options[:months]
    d = d +  options.delete(:weeks) * 7  if options[:weeks]
    d = d +  options.delete(:days)       if options[:days]
    d
  end