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
まとめ
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