Iterator Pattern - 反覆器模式
2016-11-25 22:34:29

前言

最近讀書會在讀深入淺出設計模式,趁這個機會複習一下設計模式,試著舉出簡單的例子並且用非模式與模式的方式來實作,比較它們的差異與優缺點。

範例問題

現在有兩個log記錄器,分別為 SystemLogger(系統log記錄器) 與 ApplicationLogger(應用程式log記錄器),如下所示:

class SystemLogger
  def initialize
    @sys_logs = []
  end

  def add(msg)
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%N')
    @sys_logs << [timestamp, msg]
  end

  def logs
    @sys_logs
  end
end

class ApplicationLogger
  def initialize
    @app_logs = {}
  end

  def add(msg)
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%N')
    @app_logs[timestamp] = msg
  end

  def logs
    @app_logs
  end
end

這兩個Logger做的事幾乎一樣,同樣都有add(加入新的log)與logs(列出目前儲存的log)兩個method,但最大的差別就在於logs回傳的格式不同。SystemLogger回傳的是一個Array裡面每一個項目是一個由 timestamp(時間標記)與msg(訊息) 組成的pair,例如:[[timestamp1, msg1], [timestamp2, msg2], ...];而ApplicationLogger則是利用一個hash來儲存log,以timestamp為key,msg為value,例如:{timestamp1 => msg1, timestamp2 => msg2, ...}

現在有一個LogCollector,負責收集這兩個Logger產生的log並將它們的log整合到一個array中,那要怎麼實作LogCollector呢?

沒有使用模式的實作

class LogCollector
  def initialize(loggers)
    @loggers = loggers
  end

  def collect
    all_logs = []
    @loggers.each do |logger|
      if logger.is_a?(SystemLogger)
        logger.logs.each do |log|
          all_logs << log.last
        end
      end
      if logger.is_a?(ApplicationLogger)
        logger.logs.each do |_timestamp, log|
          all_logs << log
        end
      end
    end
    all_logs
  end
end

sys_logger = SystemLogger.new
app_logger = ApplicationLogger.new

sys_logger.add('Segmentation fault.')
app_logger.add('This is a pen, this is an apple.')
sys_logger.add('The server is on fire.')
app_logger.add('Kait is not a cat.')

log_collector = LogCollector.new([sys_logger, app_logger])
p log_collector.collect

上面實作的缺點

  • LogCollector必須知道SystemLogger與ApplicationLogger如何存儲資料的細節,才可以從logs取得資料。一旦任何一個logger改了儲存的方式,LogCollector就必須跟著改。
  • 當有新的Logger加進來時,必須修改LogCollector裡collect這個method。

使用模式的實作

module LogIterator
  def next
    fail 'You should implement "next" method.'
  end

  def has_next?
    fail 'You should implement "has_next?" method.'
  end
end

class SysLogIterator
  include LogIterator

  def initialize(logs)
    @logs = logs.map(&:last)
    @counter = 0
  end

  def next
    log = @logs[@counter]
    @counter += 1
    log
  end

  def has_next?
    (@counter < @logs.size)
  end
end

class AppLogIterator
  include LogIterator

  def initialize(logs)
    @logs = logs.values
    @counter = 0
  end

  def next
    log = @logs[@counter]
    @counter += 1
    log
  end

  def has_next?
    (@counter < @logs.size)
  end
end

module IteratorLogger
  def iterator
    fail 'You should implement "iterator" method.'
  end
end

class SystemLogger
  include IteratorLogger

  def initialize
    @sys_logs = []
  end

  def add(msg)
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%N')
    @sys_logs << [timestamp, msg]
  end

  def iterator
    SysLogIterator.new(@sys_logs)
  end
end

class ApplicationLogger
  include IteratorLogger

  def initialize
    @app_logs = {}
  end

  def add(msg)
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%N')
    @app_logs[timestamp] = msg
  end

  def iterator
    AppLogIterator.new(@app_logs)
  end
end

class LogCollector
  def initialize(loggers)
    @loggers = loggers
  end

  def collect
    all_logs = []
    @loggers.each do |logger|
      iterator = logger.iterator
      while iterator.has_next?
        all_logs << iterator.next
      end
    end
    all_logs
  end
end

sys_logger = SystemLogger.new
app_logger = ApplicationLogger.new

sys_logger.add('Segmentation fault.')
app_logger.add('This is a pen, this is an apple.')
sys_logger.add('The server is on fire.')
app_logger.add('Kait is not a cat.')

log_collector = LogCollector.new([sys_logger, app_logger])
p log_collector.collect

如何實作

  • LogIterator定義了如何從取得資料的方法,其中has_next?是表示現在對應的位子是否有值,而next則是取出目前位子的資料,並移動到下一個。
  • 每個Logger會有一個對應的LogIterator,來定義怎麼取得各自的log,例如 SystemLogger 會有一個 SysLogIterator。
  • 每個Logger必須實作IteratorLogger,提供iterator這個method讓LogCollector可以得到對應的LogIterator。
  • LogCollector透過LogIterator來取得各個Logger的資料。

上面實作的優點

  • LogCollector只知道如何使用LogIterator來取資料,所以與Logger可以完整的切割,不用去知道Logger是怎麼儲存log的。
  • 要增加新的Logger只要實作IteratorLogger就可以支援LogCollector。

使用 ruby Enumerable 的實作

在ruby中,我們比較熟悉存取一連串資料的方式是用each來做,而要讓class有each的功能就必須include Enumerable這個module,下面是一個例子:

class SysLogIterator
  include Enumerable

  def initialize(logs)
    @logs = logs.map(&:last)
  end

  def each(&block)
    @logs.each(&block)
  end
end

class AppLogIterator
  include Enumerable

  def initialize(logs)
    @logs = logs.values
  end

  def each(&block)
    @logs.each(&block)
  end
end

module IteratorLogger
  def iterator
    fail 'You should implement "iterator" method.'
  end
end

class SystemLogger
  include IteratorLogger

  def initialize
    @sys_logs = []
  end

  def add(msg)
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%N')
    @sys_logs << [timestamp, msg]
  end

  def iterator
    SysLogIterator.new(@sys_logs)
  end
end

class ApplicationLogger
  include IteratorLogger

  def initialize
    @app_logs = {}
  end

  def add(msg)
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%N')
    @app_logs[timestamp] = msg
  end

  def iterator
    AppLogIterator.new(@app_logs)
  end
end

class LogCollector
  def initialize(loggers)
    @loggers = loggers
  end

  def collect
    all_logs = []
    @loggers.each do |logger|
      logger.iterator.each do |log|
        all_logs << log
      end
    end
    all_logs
  end
end

sys_logger = SystemLogger.new
app_logger = ApplicationLogger.new

sys_logger.add('Segmentation fault.')
app_logger.add('This is a pen, this is an apple.')
sys_logger.add('The server is on fire.')
app_logger.add('Kait is not a cat.')

log_collector = LogCollector.new([sys_logger, app_logger])
p log_collector.collect

其中要注意的是,一旦include Enumerable,就必須實作each這個method,以SysLogIterator為例,實作each的方式其實就是將each導向@logs的each。因為都是each,所以可以直接將傳給each的block用參數的方式傳給裡面的each:

def each(&block)
  @logs.each(&block)
end

上面的寫法可能不容易了解,其實上面做的事情就跟下面的程式做的是一樣的,也就是將資料一個個取出並當做參數傳入block中:

def each(&block)
  @logs.each do |log|
    block.call(log)
  end
end

樣式名稱

Iterator Pattern - 反覆器(疊代器)模式

目的

當我們需要從一連串的資料集合(Aggregate)中將資料一個個取出來使用時,利用 Iterator(LogIterator) 抽象化存取資料的界面,讓 client(LogCollector) 端只需要操作 Iterator 而不用去了解底層是如何儲存資料的。另一方面,Iterator 也統一個存取資料的界面,只要有實作並提供 Iterator 的 Aggregate(SystemLogger, ApplicationLogger) 就都可以讓 client 端取得資料。

使用時機

  • 需要從一連串的資料集合(Aggregate)中將資料一個個取出來使用,而底層可能有多種儲存資料的方式。