Factory
Rubyによるデザインパターンの勉強です。残りわずかになってきました。
提供は、
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
本日はFactoryパターンを勉強します。
前回のSingletonと同様、Factoryパターンも有名なデザインパターンです。
GoFのデザインパターンでは、
Factory Methodパターン、Abstract Factoryパターンに分類しています。
どちらもオブジェクトの生成を抽象化するデザインパターンになります。
ソースコードを解説する前に、本に載っているGoFのデザインパターンの原則を引用しておきます。
- デザインパターンの原則
- 変わるものを変わらないものから分離する
- インタフェースに対してプログラムし、実装に対して行わない
- 継承より集約
- 委譲、委譲、委譲
Factoryパターンは原則の一つ目がポイントになります。
- 変わるものを変わらないものから分離する
それでは、本に載っているとおり、
「池でたたずんでいるアヒルの一日?」をイメージしたプログラムをFactoryパターンを見ていきます。
まずはアヒルを表すDuckクラスとDuckが存在する池となるPondクラスを作ってみます。
class Duck def initialize(name) @name = name end def speak puts("Duck #{@name} speak.") end end class Pond def initialize(number_ducks) @ducks = [] number_ducks.times do |i| duck = Duck.new("Duck#{i}") @ducks << duck end end def simulate_one_day @ducks.each { |duck| duck.speak} end end
Duckクラスには鳴く(speak)メソッドが定義されています。
Pondクラスはコンストラクタで、池で泳ぐアヒルの数を受け取ります。
また、ある日のシミュレートを行うsimulate_one_dayメソッドも定義しています。
それでは上記のクラスを実行する呼び出しもとプログラムを作成します。
Pond.new(3).simulate_one_day
3匹のアヒルが池で泳ぐ様子をシミュレートします。
実行結果は以下のようになります。
Duck Duck0 speak. Duck Duck1 speak. Duck Duck2 speak.
ここまではなんてことのないプログラムですね。
ここから、Factoryパターンの勉強になります。
ここで、仕様変更?が発生し、
さきほどのアヒル(Duck)クラスを、カエル(Frog)クラスに変更する必要があるとします。
カエル(Frog)クラスは以下のようになります。
class Frog def initialize(name) @name = name end def speak puts("Frog #{@name} speak.") end end
カエル(Frog)クラスは、さきほどのアヒル(Duck)クラスと同じspeakメソッドを実装しています。
つまり、同じインタフェースで操作できるとも言えます。(Rubyの場合、ダックタイピングで操作できます)
問題は、Pondクラスをどのように変更するのか?というところです。
もう一度Pondクラスを見てみます。
class Pond def initialize(number_ducks) @ducks = [] number_ducks.times do |i| duck = Duck.new("Duck#{i}") @ducks << duck end end def simulate_one_day @ducks.each { |duck| duck.speak} end end
ここでDuckクラスに依存する部分に着目すると、
duck = Duck.new("Duck#{i}")
の部分がDuckクラスに依存しているとわかります。
#インスタンス変数の名前や、引数の名前がduckを含んでいる点は別途修正します。
ここで、冒頭に述べたデザインパターンの原則である
- 変わるものを変わらないものから分離する
がポイントになります。
さきほどのDuckオブジェクトを生成している1行をメソッドとして切り出し、サブクラスに実装を任せることにします。
#あわせて変数名も修正します。
class Pond def initialize(number_animals) @animals = [] number_animals.times do |i| animal = new_animal("Animal#{i}") @animals << animal end end def simulate_one_day @animals.each { |animal| animal.speak} end end class DuckPond < Pond def new_animal(name) Duck.new(name) end end class FrogPond < Pond def new_animal(name) Frog.new(name) end end
PondクラスではDuck、Frogオブジェクトを生成する処理を抽象化しています。
実際のオブジェクト生成する処理はサブクラスであるDuckPondクラス、FrogPondクラスに任せています。
Gofは、クラスの選択をサブクラスに任せることをFactory Methodパターンと呼んでいます。
Factory Methodパターンをクラス図で表現すると以下のようになります。
Creator
-
- Productオブジェクトを生成するクラス。実際にオブジェクトを生成するかはサブクラスに任せる。Pondクラスが該当する。
ConcreteCreator1、2
-
- 特定のProductオブジェクトを生成するクラス。DuckPondクラス、FrogPondクラスが該当する。
Product
Product1、2
-
- Creatorクラスによって生成される具象クラス。Duckクラス、Frogクラスが該当する。
ここからAbstract Factoryパターンまで、もうちょっとだけ続きます。
つぎに、動物(Duck、Frog)以外にも植物を追加します。
ここでは、藻(Algae)、スイレン(WaterLily)クラスを作成します。
class Algae def initialize(name) @name = name end def grow puts "Algae #{@name} grow." end end class WaterLily def initialize(name) @name = name end def grow puts "WaterLily #{@name} grow." end end
2つのクラスには植物の成長を示すgrowメソッドが定義されています。
これらのクラスのオブジェクトも生成できるようにPondクラスを変更します。
class Pond def initialize(number_animals, number_plants) @animals = [] number_animals.times do |i| animal = new_animal("Animal#{i}") @animals << animal end @plants = [] number_plants.times do |i| plant = new_plant("Plant#{i}") @plants << plant end end def simulate_one_day @plants.each { |plant| plant.grow} @animals.each { |animal| animal.speak} end end
コンストラクタには、Algaeオブジェクト、WaterLilyオブジェクトを返すnew_plantメソッドを追加しています。
同様にsimulate_one_dayメソッドでも@plants変数にアクセスしています。
実際にAlgaeオブジェクトや、WaterLilyオブジェクトを作成するクラスは次のようになります。
#今回はDuckとWaterLily、FrogとAlgaeの組み合わせのみ有効とします。
class DuckWaterLilyPond < Pond def new_animal(name) Duck.new(name) end def new_plant(name) WaterLily.new(name) end end class FrogAlgaePond < Pond def new_animal(name) Frog.new(name) end def new_plant(name) Algae.new(name) end end
ここまでくると、Abstract Factoryパターンと呼んでも良いと思います。
Abstract Factoryパターンは、矛盾のないオブジェクトの組み合わせを実現します。
このプログラムでは、DuckとWaterLily、FrogとAlgaeの組み合わせのみを許可しています。
最後にAbstarct Factoryパターンのクラス図です。
- AbstractFactory
- 複数のオブジェクト生成処理の組み合わせを定義するクラス(インタフェース)。Pondクラスが該当する。
- ConcreteFactoryA、B
- 特定のオブジェクトの組み合わせを実装するクラス。DuckWaterLilyPond、FrogAlgaePondクラスが該当する。
- ProductA1、A2、B1、B2
- CocreteFactoryA、Bによって生成されるクラス。DuckクラスWaterLilyクラスなどが該当する。
本では、ここから先もいろんな変更を加えていくんだけど、
長くなるのでFactoryの話題はこのへんで終わります。
最後にクラスオブジェクトを渡すFactoryについてもコードだけメモ。
class Pond def initialize(number_animals, animal_class, number_plants, plant_class) @animals = [] number_animals.times do |i| animal = animal_class.new("Animal#{i}") @animals << animal end @plants = [] number_plants.times do |i| plant = plant_class.new("Plant#{i}") @plants << plant end end def simulate_one_day @plants.each { |plant| plant.grow} @animals.each { |animal| animal.speak} end end
呼び出しもとプログラムは以下のとおり。
Pond.new(3, Duck, 2, Algae).simulate_one_day
Decorator
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
本日はSingletonパターンを勉強します。
デザインパターンと言えばSingletonかっていうくらい有名なパターンです。
Singletonとは「オブジェクトを1つしか作らない」ことを指します。
そういえば、僕が研修のとき、
初めて教えてもらったデザインパターンはSingletonパターンだったような気がする。
今回も本に載っているとおり、
Logger(ログを出力する)クラスを作ってSingletonパターンを実装してみます。
まずはログを出力するSimpleLoggerクラスを作成します。
SimpleLoggerクラスにはerror、warning、infoの3つのメソッドが存在します。
それぞれのメソッドはログの出力レベル(@level)によって出力有無を切り替えます。
class SimpleLogger attr_accessor :level ERROR = 1 WARNING = 2 INFO = 3 def initialize @log = File.open("log.txt", "w") @level = WARNING end def error(msg) @log.puts(msg) @log.flush end def warning(msg) @log.puts(msg) if @level >= WARNING @log.flush end def info(msg) @log.puts(msg) if @level >= INFO @log.flush end end
上記のSimpleLoggerクラスはまだSingletonになっていません。
なので通常とおりオブジェクトを生成して利用します。
呼び出しもとプログラムは以下のとおりです。
logger = SimpleLogger.new logger.level = SimpleLogger::INFO logger.info("info1") logger.warning("warning2") logger.error("error3")
ログファイル(log.txt)には以下のように出力されます。
info1 warning2 error3
上記のコードだと、newメソッドを呼び出すたびに新しいオブジェクトを生成することができます。
(つまり、Singletonではない、ということになります)
それでは、SimpleLoggerクラスを修正してSingletonにしてみます。
まずはSingletonとなるオブジェクトを保持するクラス変数を追加します。
次に、クラス変数に追加したオブジェクトを返すクラスメソッドを追加します。
class SimpleLogger attr_accessor :level ERROR = 1 WARNING = 2 INFO = 3 def initialize @log = File.open("log.txt", "w") @level = WARNING end def error(msg) @log.puts(msg) @log.flush end def warning(msg) @log.puts(msg) if @level >= WARNING @log.flush end def info(msg) @log.puts(msg) if @level >= INFO @log.flush end @@instance = SimpleLogger.new def self.instance @@instance end end
追加したのはコードの最後の部分です。
@@instance = SimpleLogger.new def self.instance @@instance end
この変更により、呼び出しもとでは、newメソッドの呼び出しからinstanceメソッドの呼び出しに変更することでSingletonなオブジェクトを取得することができます。
logger = SimpleLogger.instance logger.level = SimpleLogger::INFO logger.info("info1") logger.warning("warning2") logger.error("error3")
もう少し修正してみます。
今のコードでは相変わらずnewメソッドを呼び出すことができてしまいます。
そこで、newメソッドの可視性をprivateに変更します。
class SimpleLogger attr_accessor :level ERROR = 1 WARNING = 2 INFO = 3 def initialize @log = File.open("log.txt", "w") @level = WARNING end def error(msg) @log.puts(msg) @log.flush end def warning(msg) @log.puts(msg) if @level >= WARNING @log.flush end def info(msg) @log.puts(msg) if @level >= INFO @log.flush end @@instance = SimpleLogger.new def self.instance @@instance end private_class_method :new end
newメソッドをprivateにするために最後の1行を追加しました。
#Rubyには、とってつけたようなメソッドが用意されてるのがスゴいと思う。ちなみにインスタンスメソッドをprivateにする場合は、privateメソッドを利用するみたい。
private_class_method :new
これで直接newメソッドを呼び出すことを禁止できました。
以上がクラスベースでSingletonパターンを実現する流れです。
ここからはRubyらしくSingletonパターンを実現するにはどうするか、ってお話です。
というか、、
Singletonパターンを実現するためにRubyにはSingletonモジュールが用意されています。
#なんでもアリな気がしてきました。。
Singletonモジュールを使って、さきほどのSimpleLoggerクラスを修正してみます。
requireと、includeを追加するだけです。
require "singleton" class SimpleLogger include Singleton attr_accessor :level ERROR = 1 WARNING = 2 INFO = 3 def initialize @log = File.open("log.txt", "w") @level = WARNING end def error(msg) @log.puts(msg) @log.flush end def warning(msg) @log.puts(msg) if @level >= WARNING @log.flush end def info(msg) @log.puts(msg) if @level >= INFO @log.flush end end
Singletonモジュールをincludeすると、instanceメソッドが定義され、newメソッドがprivateに設定されます。
以上、おしまい。
Decorator
Rubyによるデザインパ(ry
提供は、
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
本日はDecoratorパターンを勉強します。
Decoratorパターンは、既存のオブジェクトに対しての機能追加を実現するパターンです。
decorateは、装飾する、という意味になります。
オブジェクトに機能という名の装飾を施していくパターンかな、と。
上の最後の1行、スベってる感があるな。今日は不調かも。
JavaのDecoratorパターンといえば、
BuffererdReaderといったIOのAPIが思い浮かびます。
BuffererdReaderの場合は、ファイル読み込みというベース機能に、バッファリング機能を装飾してると考えることができます。
今回は本に載っているとおり、
ファイル出力機能をDecoratorパターンで実装してみます。
ただし、ファイルを出力する際に、必要に応じて以下の機能を追加できるようにします。
- 行番号の表示
- タイムスタンプの表示
まずはファイル出力を行うSimpleWriteクラスを作成します。
名前のとおり、シンプルにファイル出力のみを行うクラスです。
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
データを出力するwrite_lineメソッド、ファイル出力ポジションをコントロールするposメソッド、rewindメソッド、ファイル出力を閉じるcloseメソッドを実装しています。
SimpleWriteクラスを呼び出すプログラムは以下のようになります。
f = SimpleWriter.new("file1.txt") f.write_line("Hello world.") f.close
結果として、file1.txtファイルにHello world.が出力されます。
上記のSimpleWriterクラスに、行番号出力機能を装飾するNumberingWriterクラスを作成します。
NumberingWriterクラスは以下のとおりです。
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
NumberingWriterクラスは、
コンストラクタの引数で受け取ったオブジェクトをインスタンス変数に保持します。
write_lineメソッドでは、行番号出力を施したあと、
コンストラクタで受け取ったオブジェクトに処理を委譲します。
つづいて、タイムスタンプ出力機能を装飾するTimestampoingWriterクラスです。
class TimestampingWriter < WriterDecorator def write_line(line) @real_writer.write_line("#{Time.new} : #{line}") end end
また、NumberingWriterクラス、TimestampingWriterクラスはともにWriteDecoratorクラスを継承しています。
WriteDecoratorクラスは、NumberWriterクラス、TimestampingWriterクラスの共通機能を切り出したクラスになります。
話が前後しますが、このクラスでインスタンス変数real_writerを定義しているのが、Decoratorパターンのポイントになります。
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
では、呼び出しもとプログラムを作成してみます。
f = NumberingWriter.new(SimpleWriter.new("file1.txt")) f.write_line("Hello world.") f.close
ファイルに出力された結果は以下のとおりです。行番号が付加されます。
1 : Hello world.
同様にタイムスタンプの出力は、
f = TimestampingWriter.new(SimpleWriter.new("file1.txt")) f.write_line("Hello world.") f.close
ファイルに出力された結果は以下のとおりです。タイムスタンプが付加されます。
Fri Jul 03 17:57:24 +0900 2009 : Hello world.
で、行番号とタイムスタンプの両方の出力を追加してみます。
f = TimestampingWriter.new(NumberingWriter.new(SimpleWriter.new("file1.txt"))) f.write_line("Hello world.") f.close
ファイルに出力された結果は以下のとおりです。行番号とタイムスタンプが付加されます。
1 : Fri Jul 03 17:58:56 +0900 2009 : Hello world.
以上がDecoratorパターンのサンプルになります。
Decoratorパターンのクラス図を見てみます。
- Component
- ConcreteComponent
- ベースとなる処理をもつクラス。SimpleWriterクラスが該当する。
- Decorator
- ベースとなる処理に機能を装飾するクラス。NumberingWriterクラス、TimestampingWriterクラスが該当する。
ここから、RubyらしくDecoratorパターンをイジってみます。
NumberingWriterクラス、TimestampingWriterクラスの基底クラスであるWriterDecoratorクラスはForwardableモジュールを使用するとシンプルになります。
require "forwardable" class WriterDecorator extend Forwardable def_delegators :@real_writer, :write_line, :pos, :rewind, :close def initialize(real_writer) @real_writer = real_writer end end
もともと、WriterDocoratorクラスは、real_writerオブジェクトへ処理を委譲していました。
処理の委譲は、forwardableモジュールのdef_delegatorsメソッドで行うことができます。
#ということを初めて知りました。勉強して良かった。
method_missingメソッドでも似たようなことがでますが、forwardableの方がわかりやすいです。
#method_missingは委譲するメソッドの数が多くなった場合に便利と本には書いています。このへんは上手く使い分けると良いですね。
まだまだあります。
次は実行時にオブジェクトを拡張する方法を見てみます。
f = SimpleWriter.new("file1.txt") class << f alias old_write_line write_line def write_line(line) old_write_line("#{Time.new} : #{line}") end end
面白いのはaliasメソッドで、既存のwrite_lineメソッドにエイリアス(old_write_line)を付けているところですね。
このあと、write_lineメソッドは再定義されて上書きされてしまうのですが、
old_write_lineメソッドは上書きされる前のメソッドを参照したままになります。
#これはスゴい。Rubyのスゴさがまた一つわかった気がする。。
あとは、モジュールを使って機能を追加する方法も紹介しています。
module NumberingWriter attr_reader :line_number def write_line(line) @line_number = 1 unless @line_number super("#{@line_number} : #{line}") @line_number += 1 end end module TimestampingWriter def write_line(line) super("#{Time.new} : #{line}") end end
NumberingWriter、TimestampingWriterをモジュールとして定義すると、
呼び出しもとプログラムは以下のようになります。
f = SimpleWriter.new("file1.txt") f.extend(NumberingWriter) f.extend(TimestampingWriter) f.write_line("Hello world3.") f.close
実行時にモジュールを追加する場合はextendメソッドを使います。
#このへんの感覚がRubyは難しいな。まだ慣れない。
委譲、じゃなかった、
以上、Decoratorパターンのお話でした。
Proxy
Rubyによるデザインパターン生活。続けます。
本日はProxyパターンを取り上げます。
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
Proxyパターンでは、呼び出し対象となるオブジェクトに対して、
同じインタフェースを持つ代理オブジェクトを用意します。
対象となるオブジェクトのメソッドをそのまま呼び出すのではなく、
代理オブジェクトを通じて、対象となるオブジェクトのメソッドを操作します。
Proxyパターンを用いることで、いわゆる「関心事の分離」を実現することができます。
関心事の分離とは、クラス(オブジェクト)の持つ本来の責務とは質の異なる要件(例えばセキュリティ要件やトランザクション管理など)を本来のクラスから切り離して実装することです。
#なんだか、ちょっとゴリ押しでまとめた感があるけど。。
この本では、Proxyパターンの活用方法を以下の3つに分類しています。
- 防御Proxy
- リモートProxy
- 仮想Proxy
まずは防御Proxyから見ていきます。
この章のサンプルでは銀行処理クラス(BankAccount)クラスが登場します。
class BankAccount attr_reader :balance def initialize(balance) @balance = balance end def deposit(amount) @balance += amount end def withdraw(amount) @balance -= amount end end
BankAccountクラスは残高照会処理(balance)と、入金処理(deposit)、引落処理(withdrow)を持っています。
言い換えると、BankAccountクラスは、銀行処理の基盤となる振る舞い(メソッド)は実装していますが、
利用権限を確認するためのユーザの認証処理といったセキュリティに関する要件は実装していません。
ここで、BankAccountクラスにユーザ認証処理を付加してみようと思います。
BankAccountクラスをそのまま修正してもよいのですが、その分、BankAccountクラスは複雑になってしまいます。
これはBankAccountクラスの本来の責務である銀行処理に加えて、ユーザ認証といった関心事の異なる要件を実装してしまうために発生する問題です。
そこでProxyパターンを用いると既存のBankAccountクラスに変更を加えずにユーザ認証機能を実現することができます。
BankAccountの代理となるBankAccountProxyクラスを作成します。
require "etc" 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
BankAccounrProxyクラスは、インスタンス変数にBankAccountオブジェクトを保持します。
また、BankAccountProxyクラスには、depositやwithdrowといったメソッドを定義しています。
depositメソッドやwithdrowメソッドは、
内部でcheck_accessメソッドを呼び出しユーザ認証を行っています。*1
ユーザ認証をパスした場合のみ、BankAccountオブジェクトに処理を委譲することになります。
プログラムの呼び出しもとは次のようになります。
account = BankAccount.new(100) proxy = BankAccountProxy.new(account, "murayama") puts proxy.deposit(50) puts proxy.withdraw(10)
ユーザ認証をパスした場合の実行結果は以下のとおりです。
150 140
一方で、ユーザ認証に失敗した場合はRuntimeErrorが発生します。
以上が、防御Proxyのお話になります。
ユーザ認証を担当するBankAccountProxyクラスを作成することで、
セキュリティ要件を代理クラスに任せることができました。
つづいて、仮想Proxyのお話です。
仕組みはさきほどの防御Proxyとよく似ていますが、その利用目的が異なります。
仮想Proxyでは、複雑なオブジェクトの生成を遅延させることを目的とします。
VirtualAccountProxyは入金処理(deposit)、引落処理(withdrow)、残高照会処理(balance)が呼び出されるまで、
処理の実体となるBankAccountオブジェクトの生成を遅延します。
class VirtualAccountProxy def initialize(starting_balance) @starting_balance = starting_balance end def balance subject.balance end def deposit(amount) subject.deposit(amount) end def withdraw(amount) subject.withdraw(amount) end def subject @subject || (@subject = BankAccount.new(@starting_balance)) end end
このVirtualAccountProxyは、subjectメソッドを呼び出すことでBankAccountオブジェクトを生成します。
subjectメソッドはdepositメソッドやwithdrowメソッドが呼び出されたときに実行します。
言い換えると、depositメソッドや、withdrowメソッドが呼び出されるまで、
処理の本体となるBankAccountオブジェクトは生成されないことになります。
以上がProxyパターンのお話です。
#リモートProxyの話はおいときます。
せっかくなのでクラス図も載せておきます。
と、ここまでが一般的なProxyパターンのお話です。
ここからは、さきほどの防御Proxy(BankAccountProxy)を、
Rubyらしいコードに変換してみます。
少し話が変わりますが、Rubyには未定義のメソッド呼び出しが発生した場合に、
method_missingという名前のメソッドが呼び出されます。*2
このmethod_missingメソッドを利用することで、
さきほどのBankAccountProxyクラスを次のように実装することができます。
require "etc" class BankAccountProxy def initialize(real_object, owner_name) @real_object = real_object @owner_name = owner_name end def method_missing(name, *args) check_access @real_object.send(name, *args) end def check_access if(Etc.getlogin != @owner_name) raise "Illegal access: #{@owner_name} cannot access account." end end end
depositメソッドや、withdrowメソッドを定義する必要がなくなったのでシンプルになりました。
method_missingメソッドの内部でsendメソッドを使用することで、本体となるオブジェクトのメソッドを呼び出しています。
また、method_missingメソッドの第2匹数*argsは不定個の引数を配列に格納します。
以上、Proxyパターンの勉強でした。おしまい。
Adapter
Rubyによるデザインパターン生活。若干飽きてきたけど続けます。
本日はAdapterパターンを取り上げます。
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
Adapterパターンは、インターフェースの不一致を埋めるためのパターンです。
ここでいうインターフェースの不一致というのは、
メソッドのネーミング、シグネチャの不一致のことを意味しています。
例えば、次のようなプログラムがあるとします。
画面に文字列を出力するだけのシンプルなものです。
#今回は本に載ってるプログラムをもろパクリではなく、ちょっとアレンジしています。
class Renderer def execute(text_object) text_object.print end end class TextObject def print puts "TextObject print!" end end
このプログラムは、Rendererクラスのexecuteメソッドの中で、
引数に受け取ったオブジェクトのprintメソッドを呼び出します。
引数のオブジェクトには、TextObjectを渡すことができます。
renderer = Renderer.new text_object = TextObject.new renderer.execute(text_object)
実行結果は以下のとおりです。
TextObject print!
次に、Rendererクラスのexecuteメソッドの引数に与えるオブジェクトを、
さきほどのTextObjectからLegacyTextObjectに変更します。
LegacyTextObjectクラスは以下のとおりです。
class LegacyTextObject def show_message puts "LegacyTextObject show_message!" end end
ここで問題が発生します。
LegacyTextObjectクラスにはprintメソッドが定義されていません。
その代わりに類似したメソッドであるshow_messageが定義されています。
このままの状態で、さきほどと同じようにプログラムを実行するとエラーが発生してしまいます。
renderer = Renderer.new lto = LegacyTextObject.new renderer.execute(lto)
実行結果(NoMethodError)
NoMethodError: private method ‘print’ called for #<LegacyTextObject:0x1ce6c>
エラーの原因はLegacyTextObjectクラスにprintメソッドが定義されていないためです。
一番簡単な解決法はLegacyTextObjectクラスのshow_messageメソッドを、
printメソッドに名前を変更することですが、
LegacyTextObjectクラスが既に別のプログラムから利用されている場合は、変更箇所が増えてしまいます。
上記のような問題の解決策となるのがAdapterパターンです。
Adapterパターンでは、インタフェースの不一致を吸収するためのAdapterクラスを作成します。
class LegacyTextObjectAdapter def initialize(legacty_text_object) @lto = legacty_text_object end def print @lto.show_message end end
実行時には、LegacyTextObjectAdapterを使ってLegacyTextObjectをラップします。
renderer = Renderer.new lto = LegacyTextObject.new renderer.execute(LegacyTextObjectAdapter.new(lto))
すると上手く動作します。(実行結果です)
LegacyTextObject show_message!
Adapterパターンのクラス図は、以下のようになります。
各クラスの役割は以下のとおりです。
- Client
- Targetのメソッドを呼び出すクラス。
- Target
- 実行対象となるクラス(インタフェース)。
- Adapter
- Targetインタフェースと、Adapteeクラスの差分を埋めるクラス。
- Adaptee
- 実際に動作するクラス。
先ほどのプログラムの場合、以下のような構成になります。
- Client
- Rendererクラス。
- Target
- Adapter
- LegacyTextObjectAdapterクラス。
- Adaptee
- LegacyTextObjectクラス。
以上がAdapterパターンの説明です。
なんですが、この本の面白いところはここからです。
Rubyは動的型付け言語です。
Javaのようにコンパイル時に変数の型をチェックするような仕組みはありません。
そのため、Adapterパターンにおいても、
Ruby特有の、動的型付け言語特有の解決策が存在します。
例えば、実行時にクラスを修正することで、メソッドを追加することができます。
LegacyTextObjectがロード済みの場合、以下のようにクラス自身を再定義することができます。
class LegacyTextObject def print show_message end end
これにより実行時にprintメソッドが追加されます。
printメソッドの内部では、show_messageメソッドを呼んでいるので、
結果的にshow_messageメソッドが呼び出されることになります。
上記の方法はクラス定義自体に変更が発生します。
LegacyTextObjectクラスのすべてのオブジェクトはprintメソッドを保持することになります。
クラスに直接メソッドを追加するのではなく、
特定のオブジェクトにだけメソッドを追加することもできます。
lto = LegacyTextObject.new class << lto def print show_message end end
この方法ではlto変数に格納されているオブジェクトのみがprintメソッドを保持することになります。
Rubyではオブジェクトに固有なメソッドのことを特異メソッド(Singletonメソッド)と呼びます。
特異メソッドは、以下のように実装することもできます。
lto = LegacyTextObject.new def lto.print show_message end
以上で説明はおしまいです。
Adapterパターンは理解しやすいパターンだと思います。
ただ、僕の場合はこれまでJavaの考え方(静的型付け)で理解していました。
そのため、Rubyのような動的型付けの世界ではいろんな代替手段があるんだなー、と、
改めて感じました。
プログラミングってほんとうにおもしろいですね、みたいな。
*1:ダックタイピングというやつ
Command
Rubyによるデザインパターン生活。継続中。
本日はCommandパターンを取り上げます。
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
CommandパターンのCommand(コマンド)とは命令の意味です。
この本には、
コマンドはある特定の何かをするための命令です。
と定義されています。
あ、今回からクラス図をアップすることにしました。
それではCommandパターンのクラス図です。
Commandパターンはとてもシンプルな構成になっています。
上記のクラス図では、抽象的なCommandクラス(インタフェース)を実装する、
ConcreteCommand1、ConcreteCommand2クラスを示しています。
Commandインタフェースにはexecuteメソッドが定義されています。
このexecuteメソッドが「ある特定の何かをするための命令」になります。
今回のサンプルに登場するクラスです。
- Command
- Commandクラス。名前そのままでCommandパターンのCommandを担当します。
- CreateFile
- Commandのサブクラス。ファイルを作成するクラス。
- DeleteFile
- Commandのサブクラス。ファイルを削除するクラス。
- CopyFile
- Commandのサブクラス。ファイルをコピーするクラス。
※CreateFile、DeleteFile、CopyFileクラスは、ConcreteCommand1(2)クラスのように振る舞います。
- CompositCommand
- 複数のコマンドを集約するクラス。自身もCommandのサブクラスであり、保持する複数のコマンドをまとめて実行します。
つづいて、ソースコードです。
require "fileutils" class Command attr_reader :description def initialize(description) @description = description end def execute end end 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 end class DeleteFile < Command def initialize(path) super("Delete file : #{path}") @path = path end def execute File.delete(@path) 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 end class CompositCommand < Command def initialize @commands = [] end def add_command(cmd) @commands << cmd end def execute @commands.each { |cmd| cmd.execute } end def description description = "" @commands.each { |cmd| description += cmd.description + "\n"} description end end cmds = CompositCommand.new cmds.add_command(CreateFile.new("file1.txt", "hello world\n")) cmds.add_command(CopyFile.new("file1.txt", "file2.txt")) cmds.add_command(DeleteFile.new("file1.txt")) cmds.execute
Commandクラスとそのサブクラスを定義したあと、CompositCommandオブジェクトを生成し、
CreateFile、CopyFile、DeleteFileオブジェクトを追加しています。
その後、追加したCommandオブジェクトを追加した順にまとめて実行しています。
実行結果は、カレントディレクトリに作成したfile1.txtがコピーされてfile2.txtとなります。
また、コピー元となったfile1.txtは削除されます。
結果としてfile2.txtのみが残ります。
また、さきほどのプログラムの最後で、
puts cmds.description
と実行すると、画面に集約したコマンドの一覧を出力します。
Create file : file1.txt Copy file : file1.txt to file2.txt Delete file : file1.txt
ここまでの流れをみると、
一つひとつのCommandの処理はexecuteメソッドを実装するだけで単純なものです。
「executeメソッドを実装した」から「Commandパターンだ」と思っていてはいけません。
本に載っている大事な部分を引用します。
Commandパターンのポイントは、何を行うかの決定と、それの実行を分離することです。
このパターンを使う場合、「これを行え」と命令する代わりに、「これを行う方法を記録しろ」と命令し、その後「記録したことを行え」と命令します。
Commandパターンは命令と実行の分離と考えると良いと思います。
また、Commandパターンの応用編として、
Undo(やりなおし)の実装も紹介されています。
さきほどのプログラムの各Commandクラスにunexecuteメソッドを追加してみます。
require "fileutils" class Command attr_reader :description def initialize(description) @description = description end def execute end end 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 unexecute File.delete(@path) end end class DeleteFile < Command def initialize(path) super("Delete file : #{path}") @path = path end def execute if(File.exists?(@path)) @contents = File.read(@path) end File.delete(@path) end def unexecute 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 if(File.exists?(@target)) @contents = File.read(@target) end FileUtils.copy(@source, @target) end def unexecute File.delete(@target) if(@contents) f = File.open(@target, "w") f.write(@contents) f.close end end end class CompositCommand < Command def initialize @commands = [] end def add_command(cmd) @commands << cmd end def execute @commands.each { |cmd| cmd.execute } end def unexecute @commands.reverse.each { |cmd| cmd.unexecute } end def description description = "" @commands.each { |cmd| description += cmd.description + "\n"} description end end cmds = CompositCommand.new cmds.add_command(CreateFile.new("file1.txt", "hello world10\n")) cmds.add_command(CopyFile.new("file1.txt", "file2.txt")) cmds.add_command(DeleteFile.new("file1.txt")) cmds.execute cmds.unexecute
最後にcmds.unexecuteメソッドを呼び出すことで、
コピー処理の一連の流れを取り消すことができます。
Iterator
Rubyによるデザインパターン生活、何日目とかもういいや。
本日はIteratorパターンを取り上げます。
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
Iteratorパターンは、
「集約オブジェクトがもとにある内部表現を公開せずに、その要素に順にアクセスする方法を提供する」とGoFは定義しています。
Javaでよく出てくるIteratorインタフェースがまさにそれです。
Iteratorインタフェースを利用することでListオブジェクトなど内部を意識せず、順番に要素へアクセスすることができます。
RubyにおけるIteratorパターンでは、外部イテレータと内部イテレータが存在します。
外部イテレータってのは、JavaのIteratorインタフェースのようなクラスを自分で作ってしまう方法で、
内部イテレータってのは、Rubyでよく見るeach{|element| #do something}のようなメソッドを意味します。
*1
で、まずは外部イテレータを試してみます。
配列オブジェクト用のイテレータ(ArrayIterator)クラス。
class ArrayIterator def initialize(array) @array = array @index = 0 end def has_next? @index < @array.length end def item @array[@index] end def next_item value = @array[@index] @index += 1 value end end
使い方はこんなかんじ。
array = ["red", "green", "blue"] it = ArrayIterator.new(array) while it.has_next? puts "item: #{it.next_item}" end
そんで実行結果。
item: red item: green item: blue
ちなみに名前はArrayIteratorだけど、配列オブジェクトにしか使えないわけではなくて、文字列オブジェクトの操作にも使えます。
it = ArrayIterator.new("test") while it.has_next? puts "item:#{it.next_item.chr}" end
文字列操作の場合は文字コードが返されるので、chrメソッドで文字データに変換しています。
実行結果は以下のとおり。
item:t item:e item:s item:t
続いて、内部イテレータの話です。
Arrayオブジェクトに定義されているeachメソッドのようなものを独自で実装すると以下のようになります。
def for_each_element(array) i = 0 while i < array.length yield(array[i]) i += 1 end end
引数で受け取ったArrayオブジェクトに対して、yieldメソッドでコードブロックを順に実行するかんじです。
呼び出し側のプログラムは以下のようになります。
a = [10, 20, 30] for_each_element(a){ |element| puts element }
実行結果です。
10 20 30
外部イテレータより、内部イテレータの方がRubyっぽいというか便利なように思います。
ただし、2つのArrayオブジェクトをマージするような場合は、
外部イテレータを使う方が楽なようです。
def merge(array1, array2) merged = [] it1 = ArrayIterator.new(array1) it2 = ArrayIterator.new(array2) while it1.has_next? && it2.has_next? if it1.item < it2.item merged << it1.next_item else merged << it2.next_item end end while it1.has_next? merged << it1.next_item end while it2.has_next? merged << it2.next_item end merged end a = [10, 20, 30] b = [11, 12, 21, 22] merge(a, b).each { |e| puts e }
マージの実行結果です。
10 11 12 20 21 22 30
Iteratorパターンのお話はこんなところなんだけど、
この章では、Enumerableモジュールの使い方を紹介しています。
Enumerableモジュールをインクルードすると、集約オブジェクトに対して便利なメソッドを取り込むことができます。
たとえば、all?とかany?とかinclude?とか。
Enumerableモジュールをインクルードするには、以下の点を考慮します。
class Account attr_accessor :name, :balance def initialize(name, balance) @name = name @balance = balance end def <=>(other) @balance <=> other.balance end end class Portfolio include Enumerable def initialize @accounts = [] end def each(&block) @accounts.each(&block) end def add_account(account) @accounts << account end end account1 = Account.new("account1", 1000) account2 = Account.new("account2", 5000) account3 = Account.new("account3", 3000) account4 = Account.new("account4", 4000) portfolio = Portfolio.new portfolio.add_account(account1) portfolio.add_account(account2) portfolio.add_account(account3) portfolio.add_account(account4) puts portfolio.any? { |account| account.balance > 2000 } puts portfolio.all? { |account| account.balance > 2000 }
実行すると、trueが表示されます。
true false
あと、この章の最後のところで、ObjectSpaceモジュールも紹介されています。
ObjectSpaceは、Rubyインタープリタの中に存在する完全なオブジェクト空間へのアクセスを提供します。
初めて知ったけど、これけっこう面白い。どこで使えばよいのかはわからないけど。
さっきのプログラムのつづきに以下のコードを追加すると、
ObjectSpace.each_object(Account){ |object| puts object.name }
こんなふうにメモリ上に存在するインスタンスを取得することができます。
account4 account3 account2 account1
以上。
*1:こういう話は言われてみたら、そうだなーって思うけど、言われなかったら意識しないままだと思う。