- Factory メソッド
- Abstract Factory メソッド
- ビルダ(Builder) パターン
- シングルトンパターン
- アダプタ
- コンポジットパターン
- デコレータ
- プロキシ
- コマンド
- インタプリタ
- イテレータ
- オブザーバ
- ストラテジパターン
- テンプレートメソッド
- 参考
Factory メソッド
インスタンス生成をサブクラスに任せるデザインパターン。インスタンスの生成部分を切り離すことで、結合度を下げて追加、変更、保守を容易にする。
例えば、次の楽器クラスがあるとする。
今後も楽器の種類が増えることが考えら、楽器インスタンスを生成するインタフェースが統一されていると、このクラスを利用するユーザは使いやすいし、保守もしやすくなる。
Factoryメソッドを次のように導入する。インスタンス化する際には、派生クラスで initialize
メソッドを呼び出し、スーパクラスの initialize
メソッドでは派生クラスの new
メソッドを呼び出す。
そうすることで、Factoryクラスの各インスタンスに応じて生成するクラスを切り替えられる。
実行するときには、次のようにする
# サックスのファクトリーメソッド factory = SaxophoneFactory.new(3) saxophones = factory.ship_out # トランペットのファクトリーメソッド factory = TrumpetFactory.new(2) trumpets = factory.ship_out
Abstract Factory メソッド
異なる組み合わせ を持った複数のオブジェクトをひと塊に生成するために用いる。
Abstract Factory
メソッドの旨味は次の2点。
- 関連し合うオブジェクトの集まりを生成できる
- 整合性が必要なオブジェクト群を集めて生成できる
これだけだとよくわからんので、次のようなクラスがあるとする。
上記クラスを用いて開発することになったとする。その時の制約として、次のものがあるとする。
上記を守るように実装する際に、各クラスをそれぞれインスタンス化して、お互いが関係するように実装することもできるが、中々面倒。
そこで、AbstractFactory
メソッドで、複数のオブジェクトをひと塊にして生成できるようにする。メソッドのイメージとしては次のような感じ。
インスタンス生成時には、スーパクラスの initialise
メソッドを呼び出し、スーパクラスの initialize
メソッドの中でインスタンス化を行う。そして、インスタンス化で呼び出される new
メソッドでは、派生クラスの中で定義したものを用いる。
そうすることで、組み合わせを守ったオブジェクトを生成できる。
実行するときには、次のようなコードになる。
# カエルと藻のAbstractFactory factory = FrogAndAlgaeFactory.new(4,1) animals = factory.get_animals animals.each { |animal| animal.eat } plants = factory.get_plants plants.each { |plant| plant.grow } # アヒルとスイレンのAbstractFactory factory = DuckAndWaterLilyFactory.new(3,2) animals = factory.get_animals animals.each { |animal| animal.eat } plants = factory.get_plants plants.each { |plant| plant.grow }
ビルダ(Builder) パターン
ビルダパターンは次の場面で使われる。
- オブジェクト生成に大量のコードが必要(生成するインスタンスのプロパティが多い)
- オブジェクトの生成が難しい
- オブジェクト生成時に必要なチェックを行う
上記のために、ビルダパターンでは、『Director:作業過程を決定』と『Builder:作業インタフェースを持つ』を組み合わせて、柔軟にオブジェクトを生成するデザインパターンとなっている。
ビルダパターンには3つの構成要素がある。
役割名 | 内容 |
---|---|
Director(ディレクタ) | ビルダで提供されているインタフェースのみを使用して処理を行う |
Builder(ビルダ) | 各メソッドのインタフェースを定める |
ConcreateBuilder(具体ビルダ) | Builderが定めたインタフェースの実装 |
上だけだとよくわからないので、ここでもサンプルを考える。
加工水(塩水と砂糖水)を作る処理を考える。まずは実際の工程を考えてみると、必要な要素と作業に分割できそう。
項目 | 内容 |
---|---|
要素 | 水、素材(砂糖 or 塩) |
作業 | 水を加える、素材を加える |
加工水を作るのに必要なモノと作業は揃った。あとはこれを実施する作業者がいれば加工水の生成が可能になる。つまり、オブジェクトが作れるようになったということ。
上記の例を、ビルダパターンに当てはめると、『要素:具体ビルダ』、『作業:ビルダ』、『作業者:ディレクタ』という関係になる。
要するに、オブジェクトの生成に必要な作業は、ビルダにインタフェースとして用意し、ディレクタで何をするかを決定していく。
そして、出来上がるオブジェクトは具体ビルダによって生成されるオブジェクトであり、それ自体の構成は、ディレクタが決定した作業(ビルダのインタフェースの組み合わせ)により決まる。
こうしておくことのメリットは、 ディレクタはインタフェース(作業)だけ知っていればよく、Builderではインタフェースを担保していれば良いので、すげ替えなどが容易 に行える。
クラス図に置き換えてみる。
ディレクタはビルダーのインタフェースを参照し、具体ビルダではビルダのインタフェースを実装する。
ソースコードで示すと次のような感じになる。
- 具体ビルダ
# 塩水クラス (ConcreteBuilder:ビルダーの実装部分) class SaltWater attr_accessor :water, :salt def initialize(water, salt) @water = water @salt = salt end # 素材(塩)を加える def add_material(salt_amount) @salt += salt_amount end end # 砂糖水クラス (ConcreteBuilder:ビルダーの実装部分) class SugarWater attr_accessor :water, :sugar def initialize(water, sugar) @water = water @sugar = sugar end # 素材(砂糖)を加える def add_material(sugar_amount) @sugar += sugar_amount end end
- ビルダ
# 加工した水を生成するためのインターフェイス(Builder) class WaterWithMaterialBuilder def initialize(class_name) @water_with_material = class_name.new(0,0) end # 素材を入れる def add_material(material_amount) @water_with_material.add_material(material_amount) end # 水を加える def add_water(water_amount) @water_with_material.water += water_amount end # 加工水の状態を返す def result @water_with_material end end
- ディレクタ
# 加工水の作成過程を取り決める class Director def initialize(builder) @builder = builder end def cook @builder.add_water(150) @builder.add_material(90) @builder.add_water(300) @builder.add_material(35) end end
実際にオブジェクトを生成するときには、次のようにする。
# 砂糖水を作る builder = WaterWithMaterialBuilder.new(SugarWater) director = Director.new(builder) director.cook # 塩水を作る builder = WaterWithMaterialBuilder.new(SaltWater) director = Director.new(builder) director.cook
シングルトンパターン
シングルトンパターンは、あるクラスのインスタンスが一つしかない状態を保証する方法。
例えば、次のようなリソース管理で用いるのが適切と考えられている。
- ログの書き込み処理を行うメソッドでのファイルアクセス
- システム内の共通のキャッシュテーブルを参照する場合
- etc.
シングルトンパターンに則ったクラスの条件は次の3点。
- publicのコンストラクタを持たず、privateのコンストラクタを持つ
- private変数として、自身のインスタンスを持つ
- public関数として、自身のインスタンスを返す
geter
メソッドを持つ
Ruby でシングルトンパターンを実装する時には、 クラスに Singleton モジュールを Mix-in して利用する。
sigleton
モジュールを Mix-in すると、new
メソッドは プライベートになる。クラス図にすると次のような感じ。
上記のクラスを実装してみる。
# SingletonはMix-inしたクラスの同一のインスタンスを返す require 'singleton' # シングルトン class SingletonObject # instanceメソッドが定義され、newメソッドがprivateに include Singleton attr_accessor :counter def initialize @counter = 0 end end
上のクラスを使ってみる。
obj1 = SingletonObject.instance obj1.counter += 1 puts(obj1.counter) #=> 1 obj2 = SingletonObject.instance # 同一のインスタンスが返る obj2.counter += 1 puts(obj2.counter) # => 2 ⇦ 前回の結果が引き継がれている
また、シングルトンの条件として、『パブリックのコンストラクタを持たない』があるので、試しに new
メソッドでインスタンス生成してみるとエラーになる。
obj3 = SingletonObject.new # private method `new' called for SingletonObject:Class (NoMethodError)
アダプタ
アダプタは、現実世界の変換コネクタのようなもの。直接繋がらないコネクタと差し込み口は、変換コネクタを仲介として結びつける。つまり、クラスの型を整える『ラッパー』のようなもの。
アダプタが利用されるシーンは次のようなもの。
- 関連性・互換性のないオブジェクト同士を結びつけたい
- 他のコンポーネントへの変更ができるようにしたい
アダプタの構成要素は次の4種類。
項目 | 役割 |
---|---|
利用者 | ターゲットのメソッドを呼び出す |
ターゲット | インタフェースを規定 |
アダプタ | アダプティのインタフェースを変換して、ターゲット向けのインタフェースを提供 |
アダプティ | 実際に動作する既存クラス |
ここも例の如く、クラス図っぽく書いてみる。
Client から OldPrinter のメソッドを用いるようにしたい時には、アダプタを用意してそこから呼び出したいメソッドを呼び出す。
まずは、ターゲットを実装。Ruby に Interface はないが、下記のようにオブジェクトを渡して、当該オブジェクトのメソッドを呼び出すという形で Interface を再現。
# 利用者(Client)へのインターフェイス (Target) class Printer def initialize(obj) @obj = obj end def print_weak @obj.print_weak end def print_strong @obj.print_strong end end
次に、アダプティを実装。
# Targetにはないインターフェイスを持つ (Adaptee) class OldPrinter def initialize(string) @string = string.dup end # カッコに囲って文字列を表示する def show_with_paren puts "(#{@string})" end # アスタリスクで囲って文字列を表示する def show_with_aster puts "*#{@string}*" end end
最後に、アダプタを実装。アダプタでは、インタフェースのメソッド内に呼び出したいメソッドを実装する。
# Targetが利用できるインターフェイスに変換 (Adapter) class Adapter def initialize(string) @old_printer = OldPrinter.new(string) end def print_weak @old_printer.show_with_paren end def print_strong @old_printer.show_with_aster end end
利用者では、Printer
クラスのインスタンス生成時に、アダプタを用いてオブジェクト生成して渡す。
アダプタのメソッド内では、アダプティのメソッドが呼ばれるようになっているため、ターゲットのインタフェースで実行するとアダプティのメソッドが呼ばれる。
# 利用者(Client) p = Printer.new(Adapter.new("Hello")) # アダプタを通してアダプティのメソッドが呼ばれる p.print_weaek # => (Hello) p.print_strong # => *Hello*
コンポジットパターン
コンポジットパターンは、『全体と中身』を同一のものとして捉えることで、再起的な構造をクラスで表現するデザインパターンのこと。
コンポジットは3つの要素からなる。
要素名 | 役割 |
---|---|
コンポーネント | 全てのオブジェクトの基底となるクラス |
リーフ | プロセスの単純な構成要素で再帰しない |
コンポジット | コンポーネントの一つで、サブコンポーネントを構成 |
具体的な使い方としては、ディレクトリとフォルダを同様のコンポーネントとして扱うことで、削除処理などを再起的に行えるようにするなどがある。
他にも、ファイルシステムなどの木構造を伴う再起的なデータ構造を表現できたり、階層構造で表現されるオブジェクトの扱いが楽になる。
例として、ファイルシステムを考えてみる。まず、クラス図で全体像を眺める。ディレクトリは、再帰できる(自身の中にファイルとディレクトリをもてる)ので、集約でそれを表現する。
上記のコードを書く。
まずは、コンポーネントから。コンポーネントで共通メソッドを規定する。
# FileEntry, DirEntryクラスの共通メソッドを規定 class Entry def get_name end def ls_entry(prefix) end def remove end end
次に、FileEntry
クラス。
# Leaf class FileEntry < Entry def initialize(name) @name = name end def get_name @name end def ls_entry(prefix) puts(prefix + "/" + get_name) end def remove puts @name + "を削除しました" end end
最後に、DirEntry
クラス。ディレクトリ内には、ファイルが複数あることもあるため、メンバ変数に配列がある。
# Composite class DirEntry < Entry def initialize(name) @name = name @directory = Array.new end def get_name @name end def add(entry) @directory.push(entry) end def ls_entry(prefix) puts(prefix + "/" + get_name) @directory.each do |e| e.ls_entry(prefix + "/" + @name) end end def remove @directory.each do |i| i.remove end puts @name + "を削除しました" end end
上記のコードを使ってみる。
root = DirEntry.new("root") tmp = DirEntry.new("tmp") # tmp ディレクトリ生成 tmp.add(FileEntry.new("conf")) # tmp ディレクトリ内にファイル追加① tmp.add(FileEntry.new("data")) # tmp ディレクトリ内にファイル追加② root.add(tmp) # root ディレクトリに tmp ディレクトリを追加 root.ls_entry("") # => /root # => /root/tmp # => /root/tmp/conf # => /root/tmp/data root.remove # => confを削除しました # => dataを削除しました # => tmpを削除しました # => rootを削除しました
デコレータ
デコレータは、既存オブジェクトに簡単に機能を追加するためのパターン。デコレータパターンを適応することで、レイヤ状に機能を積み重ねて、必要な機能を持つオブジェクトを作れる。
レイヤ状に機能を重ねるので、既存オブジェクトの中身を変更なく機能を追加(デコレート)できる。機能の積み重ねの組み合わせの仕方で、様々な機能を実現できる。
継承に似ているが、継承よりも変更の影響を受けにくい。
デコレータは2つの要素からなる。
要素名 | 役割 |
---|---|
具体コンポーネント | ベースとなる処理を持つオブジェクト |
デコレータ | 追加する機能を持つクラス |
『ファイルへの出力機能』を持つクラスで、デコレータの実装を学ぶ。まずは、クラス図を書いてみる。
上記の通り、基となる ConcreteComponent
の機能を、デコレータで表現する。
SimpleWriter
クラスを次ようなものとする。
class SimpleWriter def initialize(path) @file = File.open(path, "w") end def write_line(line) @file.print(line) @file.print("\n") end def pos @file.pos end def rewind @file.rewind end def close @file.close end end
上記のコードをデコレータで表現する。
class WriterDecorator def initialize(real_writer) @real_writer = real_writer end def write_line(line) @real_writer.write_line(line) end def pos @real_writer.pos end def rewind @real_writer.rewind end def close @real_writer.close end end
次に修飾を考える。
業務番号出力機能を実現する。
class NumberingWriter < WriterDecorator def initialize(real_writer) super(real_writer) @line_number = 1 end def write_line(line) @real_writer.write_line("#{@line_number} : #{line}") end end
タイムスタンプ出力機能を実装する
class TimestampingWriter < WriterDecorator def write_line(line) @real_writer.write_line("#{Time.new} : #{line}") end end
上記を使って、コーディングしてみる。
f = NumberingWriter.new(SimpleWriter.new("file1.txt")) f.write_line("Hello out there") f.close f = TimestampingWriter.new(SimpleWriter.new("file2.txt")) f.write_line("Hello out there") f.close #=> 2012-12-09 12:55:38 +0900 : Hello out there f = TimestampingWriter.new(NumberingWriter.new(SimpleWriter.new("file3.txt"))) f.write_line("Hello out there") f.close # => 2012-12-09 12:55:38 +0900 : Hello out there
上記のように、既存クラス(ConcreteComponent)の実装を変更することなく、機能を自由に組み合わせることが可能。
プロキシ
プロキシパターンは、1つオブジェクトに複数の関心事がある場合に、それを分離したい時に使う。
例えば、オブジェクトの本質的な目的とは異なる非機能用件(セキュリティ要件とか)を切り離して実装できる。
プロキシは2つの要素からなる。
要素名 | 役割 |
---|---|
対象オブジェクト(subject) | 本物のオブジェクト |
代理サブジェクト(proxy) | 特定の「関心事」を担当、それ以外を対象サブジェクトに渡す |
イメージ的には、代理人が本人でなくてもできることを処理し、代理人ができない本人が処理するといった、いわゆる『代理人』のように振る舞う。
代理オブジェクトは、対象オブジェクトと同じインターフェースを持ち、利用時は代理オブジェクトを通して対象となるオブジェクトを操作する
サンプルとして、銀行の窓口業務を担当するクラスとユーザ認証を担当するクラスで、『関心事の分離』をするプロキシパターンを学ぶ。
イメージ的には、ユーザ認証担当は、代理人として自身にできること(ユーザ認証)は自分で処理して、できないこと(入金/出金)は対象の窓口担当に回すって感じ。
まずは、クラス図を書いてみる。
上記のように、クライアントは代理オブジェクトにリクエストを出すことで、銀行内のオブジェクトを操作できる。代理オブジェクトはできることは自分で行い、できないことは対象オブジェクトのメソッドを呼び出し対象オブジェクトに処理させる。
コードは次のような感じ。
# 銀行の入出金業務を行う(対象オブジェクト/subject) class BankAccount attr_reader :balance def initialize(balance) @balance = balance end # 出金 def deposit(amount) @balance += amount end # 入金 def withdraw(amount) @balance -= amount end end
次に代理人オブジェクト。
ポイントは、 initialize
で対象オブジェクトを渡している点。
# etcはRubyの標準ライブラリで、/etc に存在するデータベースから情報を得る # この場合は、ログインユーザー名を取得するために使う require "etc" # ユーザーログインを担当する防御Proxy class BankAccountProxy def initialize(real_object, owner_name) @real_object = real_object @owner_name = owner_name end def balance check_access @real_object.balance end def deposit(amount) check_access @real_object.deposit(amount) end def withdraw(amount) check_access @real_object.withdraw(amount) end def check_access if(Etc.getlogin != @owner_name) raise "Illegal access: #{@owner_name} cannot access account." end end end
BankAccountProxy
にとって、 check_access
以外は自身の主目的でないので、対象オブジェクトのメソッドを呼び出す設計になっている。
で、使う時には、次のように書く。
# ログインユーザーの場合 account = BankAccount.new(100) proxy = BankAccountProxy.new(account, "goruchan") puts proxy.deposit(50) # => 150 <- proxyを通してオブジェウトを操作 puts proxy.withdraw(10) # => 140 # ログインユーザーではない場合 account = BankAccount.new(100) proxy = BankAccountProxy.new(account, "no_login_user") puts proxy.deposit(50) #`check_access': Illegal access: no_login_user cannot access account. (RuntimeError)
proxy
で new
するときに、対象オブジェクトと所有者を引数として渡し、組としている。
コマンド
コマンドは、あるオブジェクトに対してコマンドを送ることで、そのオブジェクトのメソッドを呼び出すこと。
例えば、Linuxコマンドのように、ユーザはファイルシステムの実装を知らなくても、ファイルの追加・削除を実行できるのも、コマンドパターンの一つ。
コマンドパターンは、2つの要素からなる。
要素名 | 役割 |
---|---|
コマンド | コマンドのインタフェース |
具体コマンド | コマンドの具体的な処理 |
ファイルの作成・削除・コピーができるモデルで、コマンドパターンを考える。
まずはクラス図で書いてみる。
基本的には、継承先で基底クラスのメソッドをオーバライドする。
コマンドのコードは次のような感じ。
class Command attr_reader :description def initialize(description) @description = description end def execute end def undo_execute end end
具体コマンドを実装していく。
require "fileutils" class CreateFile < Command def initialize(path, contents) super("Create file : #{path}") @path = path @contents = contents end def execute f = File.open(@path, "w") f.write(@contents) f.close end def undo_execute File.delete(@path) end end class DeleteFile < Command def initialize(path) super("Delete file : #{path}") @path = path end def execute if File.exists?(@path) @content = File.read(@path) end File.delete(@path) end def undo_execute f = File.open(@path, "w") f.write(@contents) f.close end end class CopyFile < Command def initialize(source, target) super("Copy file : #{source} to #{target}") @source = source @target = target end def execute FileUtils.copy(@source, @target) end def undo_execute File.delete(@target) if(@contents) f = File.open(@target, "w") f.write(@contents) f.close end end end class CompositeCommand < Command def initialize @commands = [] end def add_command(cmd) @commands << cmd end def execute @commands.each { |cmd| cmd.execute } end def undo_execute @commands.reverse.each { |cmd| cmd.undo_execute } end def description description = "" @commands.each { |cmd| description += cmd.description + "\n"} description end end
クラスの準備はこれで完了。あとはこいつを使ってみる。
command_list = CompositeCommand.new command_list.add_command(CreateFile.new("file1.txt", "hello world\n")) command_list.add_command(CopyFile.new("file1.txt", "file2.txt")) command_list.add_command(DeleteFile.new("file1.txt")) command_list.execute puts(command_list.description) # => Create file : file1.txt # => Copy file : file1.txt to file2.txt # => Delete file : file1.txt # 処理を取り消すコマンド command_list.undo_execute # => file1のみになっている
インタプリタ
インタプリタは、ひとつひとつの問題はシンプルだが、組み合わさって複雑になるような場合に効果を発揮する。
インタプリタでは、各問題を処理し、その結果を手順に基づいて処理を実行していく。
上だけだと何言ってるかわからないけれど、例えば次のような機能を実装したいとする。
- ファイルネームが〇〇、かつ、サイズが△△以上のファイル一覧を表示せよ
- ファイルネームが〇〇、または、サイズが△△以上のファイル一覧を表示せよ
- ファイルネームが〇〇以外、かつ、サイズが△△以上のファイル一覧を表示せよ
上記の場合、具体的な機能(ファイルネーム検索、サイズ確認)と、それを組み合わせる処理(And,Or,Not)があることで、少し複雑になっていて、まさにインタプリタの効果を発揮するパターンといえる。
インタプリタパターンは、4つの要素からなる。
要素名 | 役割 |
---|---|
抽象表現(AbstractExpression) | 共通のインタフェースを定義 |
終端(TerminalExpression) | 終端を表現するクラス(具体的な機能) |
終端以外(NonterminalExpression) | 非終端を表現するクラス(組み合わせ) |
状況、文脈(Context) | 構文の解析を手助けする |
ファイル検索機能でインタプリタを学んでいく。
とりあえず、クラス図で書いてみる。
まずは、すべてのファイル検索のベースとなる単純なクラスを用意する。
class Expression def |(other) Or.new(self, other) end def &(other) And.new(self, other) end end
次に、具体的な機能のクラスを用意
- すべてのファイル名を取得する
All
クラス
require "find" class All < Expression def evaluate(dir) results= [] Find.find(dir) do |p| next unless File.file?(p) results << p end results end end
- 与えられたパターンとマッチするすべてのファイル名を返す
FileName
クラス
class FileName < Expression def initialize(pattern) @pattern = pattern end def evaluate(dir) results= [] Find.find(dir) do |p| next unless File.file?(p) name = File.basename(p) results << p if File.fnmatch(@pattern, name) end results end
- 指定したファイルサイズより大きいファイルを返す
Bigger
クラス
class Bigger < Expression def initialize(size) @size = size end def evaluate(dir) results = [] Find.find(dir) do |p| next unless File.file?(p) results << p if( File.size(p) > @size) end results end end
- 書込可能なファイルを返す
Writable
クラス
class Writable < Expression def evaluate(dir) results = [] Find.find(dir) do |p| next unless File.file?(p) results << p if( File.writable?(p) ) end results end end
そして、組み合わせ部分のクラスを用意
- 書き込みができないファイル検索用の
Not
クラス
class Not < Expression def initialize(expression) @expression = expression end def evaluate(dir) All.new.evaluate(dir) - @expression.evaluate(dir) end end
- ファイル検索式結合用の
Or
,And
クラス
class Or < Expression def initialize(expression1, expression2) @expression1 = expression1 @expression2 = expression2 end def evaluate(dir) result1 = @expression1.evaluate(dir) result2 = @expression2.evaluate(dir) (result1 + result2).sort.uniq end end class And < Expression def initialize(expression1, expression2) @expression1 = expression1 @expression2 = expression2 end def evaluate(dir) result1 = @expression1.evaluate(dir) result2 = @expression2.evaluate(dir) (result1 & result2) end end
上のクラスを用いて、ファイル検索をすると、次のような出力になる。
# 具体的な機能(名前検索)を組み合わせ(And)で結合 complex_expression1 = And.new(FileName.new('*.mp3'), FileName.new('big*')) puts complex_expression1.evaluate('13_test_data') #=> 13_test_data/big.mp3 #=> 13_test_data/big2.mp3 # 具体的な機能(サイズ検索)だけ complex_expression2 = Bigger.new(1024) puts complex_expression2.evaluate('13_test_data') #=> 13_test_data/big.mp3 #=> 13_test_data/big2.mp3 #=> 13_test_data/subdir/other.mp3 # 具体的な機能(名前検索)で条件に当てはまるオブジェクト生成し、Andで抽出。 complex_expression3 = FileName.new('*.mp3') & FileName.new('big*') puts complex_expression3.evaluate('13_test_data') #=> 13_test_data/big.mp3 #=> 13_test_data/big2.mp3 # 具体的な機能(全抽出)だけ complex_expression4 = All.new puts complex_expression4.evaluate('13_test_data') #=> 13_test_data/big.mp3 #=> 13_test_data/big2.mp3 #=> 13_test_data/small.mp3 #=> 13_test_data/small1.txt #=> 13_test_data/small2.txt #=> 13_test_data/subdir/other.mp3 #=> 13_test_data/subdir/small.jpg
イテレータ
イテレータは次のような場合に用いる。
- 要素の集まったオブジェクト(配列等)にアクセス
- 集合の要素に順番にアクセスする必要がある
つまり、イテレータパターンでは、要素の集まりを保有するオブジェクトの各要素に、順番にアクセスする方法を提供するパターンといえる。
イテレータパターンには、内部イテレータと外部イテレータがある。
イテレータ名 | 概要 |
---|---|
内部イテレータ | コードブロックベースのイテレータ。 each メソッドがこれに相当する |
外部イテレータ | コードブロック外のクラスとして用意したイテレータ。 |
ブログ内の記事にアクセスする方法で、外部イテレータについて学んでいく。
とりあえず、クラス図で書いてみる。
外部イテレータは、ブログ(集約オブジェクト)の要素(記事)にアクセスするためのパターン。
まずは、要素の記事のクラスを書く。
class Article def initialize(title) @title = title end attr_reader :title end
次に、要素のまとまりであるブログクラスを書く。
class Blog def initialize @articles = [] end def get_article_at(index) @articles[index] end def add_article(article) @articles << article end def length @articles.length end def iterator BlogIterator.new(self) end end
最後に、このまとまりを操作するイテレータを用意する。
class BlogIterator def initialize(blog) @blog = blog @index = 0 end def has_next? @index < @blog.length end def next_article article = self.has_next? ? @blog.get_article_at(@index) : nil @index = @index + 1 article end end
上記の動作を確認する。
blog = Blog.new blog.add_article(Article.new("デザインパターン1")) blog.add_article(Article.new("デザインパターン2")) blog.add_article(Article.new("デザインパターン3")) iterator = blog.iterator while iterator.has_next? article = iterator.next_article puts article.title end #=> デザインパターン1 #=> デザインパターン2 #=> デザインパターン3
オブザーバ
オブザーバは、次の条件を満たす場合に用いる。
- オブジェクトの状態が変化する可能性がある
- 変化したことを他のオブジェクトに通知する必要がある
例えば、新幹線の予約者が予約を解除したいときに、予約キャンセルを鉄道会社に連絡するみたいな状況。
上の例の通り、オブザーバは、あるオブジェクトの状態が変化した際に、オブジェクト自身が『観察者』に『通知』する仕組み。これにより、観察者は、常に観察しなければならない状態から解放される。
オブザーバパターンは、3つのオブジェクトからなる。
オブジェクト名 | 役割 |
---|---|
サブジェクト(subject) | 変化する側のオブジェクト(変化を報告する側) |
オブザーバ(Observer) | 状態の変化を関連するオブジェクトに通知するインタフェース |
具象オブザーバ(ConcreteObserver) | 状態の変化に関連して具体的な処理を行う(報告を受ける側) |
従業員の小切手、税金請求書発行の処理で、オブザーバについて学んでいく。
とりあえず、クラス図で書いてみる。
まずは、サブジェクトのコードを書く。
require 'observer' class Employee include Observable attr_reader :name, :title, :salary def initialize(name, title, salary) @name = name @title = title @salary = salary add_observer(Payroll.new) add_observer(TaxMan.new) end def salary=(new_salary) @salary = new_salary changed notify_observers(self) end end
ruby には、observable
と呼ばれるオブザーブとしての機能を持つ標準モジュールがあるため、組み込む。
changed
メソッドで更新フラグを立て、add_observer
で追加したオブザーブに対して、notify_observers
メソッドで更新通知を出す。
次に、小切手発行と、税金請求書発行を行うクラスのコードを書く。
class Payroll def update(changed_employee) puts "彼の給料は#{changed_employee.salary}になりました!#{changed_employee.title}のために新しい小切手を切ります。" end end class TaxMan def update(changed_employee) puts "#{changed_employee.name}に新しい税金の請求書を送ります" end end
これを実行する。
john = Employee.new('John', 'Senior Vice President', 5000) john.salary = 6000 #=> 彼の給料は6000になりました!Senior Vice Presidentのために新しい小切手を切ります。 #=> Johnに新しい税金の請求書を送ります john.salary = 7000 #=> 彼の給料は7000になりました!Senior Vice Presidentのために新しい小切手を切ります。 #=> Johnに新しい税金の請求書を送ります
salary
を更新したタイミングで、具象オブザーバが呼ばれていることが確認できる。
ストラテジパターン
ストラテジパターンは、メソッドの中である一部分の処理を切り替えたい時に使えるパターン。状況に応じて、アルゴリズムを変えなければならないような時に使える。例えば、ゲームの難易度設定に応じて、戦略アルゴリズムが変わるなど。
ストラテジパターンは、3つのオブジェクトからなる。
オブジェクト名 | 役割 |
---|---|
コンテキスト(Context) | ストラテジの利用者 |
抽象戦略(Strategy) | 同じ目的をもった一連のオブジェクトを抽象化したもの |
具象戦略(ConcreteStrategy) | 具体的なアルゴリズム |
ストラテジのアイディアは、コンテキスト*1が委譲*2によってアルゴリズムを交換できるようにすること。
レポート生成を、HTML形式とプレーンテキスト形式で切り替えられるようなプログラムを題材に、ストラテジについて学んでいく。
とりあえず、クラス図で書いてみる。
まずは、両形式のインタフェースとなる、抽象戦略のコードを書く。
class Formatter def output_report(title, text) raise 'Called abstract method !!' end end
次に、具象戦略のコードを書く。
class HTMLFormatter < Formatter def output_report(report) puts '' report.text.each { |line| puts "#{line}" } puts '' end end class PlaneTextFormatter < Formatter def output_report(report) puts "***** #{report.title} *****" report.text.each { |line| puts(line) } end end
最後に、レポートのコードを書く。
class Report attr_reader :title, :text attr_accessor :formatter def initialize(formatter) @title = 'report title' @text = %w(text1 text2 text3) @formatter = formatter end def output_report @formatter.output_report(self) end end
initialize
メソッドで @formatter
をインスタンス生成時に渡した具象戦略を設定して、インスタンスのレポート出力は、具象戦略の output_report
に自分自身(Report)を渡して処理させている。
結果を確認する。
report = Report.new(HTMLFormatter.new) report.output_report # => # => text1 # => text2 # => text3 # => # formatterの出力形式を切り替える(戦略変更) report.formatter = PlaneTextFormatter.new report.output_report # => ***** report title ***** # => text1 # => text2 # => text3
テンプレートメソッド
テンプレートメソッドは、2つのコードのやりたいこと(アルゴリズム)がほぼ同じで、部分的に変更したい時に有効なパターン。
スーパークラスで処理の枠組みを固め、サブクラスで具体的内容を実装する。
スーパークラスでは、アルゴリズムの流れの中で利用される抽象的なメソッドと、抽象的なメソッドを用いて処理のアルゴリズムを定義するテンプレートメソッドを定義する。
テンプレートメソッドを使うことのメリットは次のもの。
- スーパークラスに『変わらない基本的なアルゴリズム』を置ける
- スーパークラスは『高レベル*3の処理』の制御に集中できる
- サブクラス側に『変化するロジック』を置ける
- サブクラスは『詳細を埋める*4こと』に集中できる
テンプレートメソッドは、2つのオブジェクトからなる。
オブジェクト名 | 役割 |
---|---|
スーパークラス | 変わらない基本的なアルゴリズム |
サブクラス | 具体的な処理 |
レポート生成を、HTML形式とプレーンテキスト形式で切り替えられるようなプログラムを題材に、テンプレートメソッドについて学んでいく。
とりあえず、クラス図で書いてみる。
まずは、スーパークラスで高レベルの処理を書く。
# レポートを出力する class Report def initialize @title = "html report title" @text = ["report line 1", "report line 2", "report line 3"] end # レポートの出力手順を規定 def output_report output_start output_body output_end end # レポートの先頭に出力 def output_start end # レポートの本文の管理 def output_body @text.each do |line| output_line(line) end end # 本文内のLINE出力部分 def output_line(line) raise 'Called abstract method !!' end # レポートの末尾に出力 def output_end end end
次に、サブクラスで詳細な処理を記述する。
# HTML形式でのレポート出力を行う class HTMLReport < Report def output_start puts '' end def output_line(line) puts "#{line}" end def output_end puts '' end end # PlaneText形式(`*****`で囲う)でレポートを出力 class PlaneTextReport < Report def output_start puts "**** #{@title} ****" end def output_line(line) puts line end end
使用する際には、サブクラスをインスタンス化するだけ。
html_report = HTMLReport.new html_report.output_report # => # => report line 1 # => report line 2 # => report line 3 plane_text_report = PlaneTextReport.new plane_text_report.output_report # => **** html report title **** # => report line 1 # => report line 2 # => report line 3