何番煎じか判りませんがお勉強メモを残します
「HeadFirstデザインパターン」第3章
「Rubyによるデザインパターン」第11章
Decorator パターン
「HeadFirstデザインパターン」でのJavaコードは(だいたい)こんな感じ
- コーヒー屋の料金計算
- 飲み物の種類は ハウスブレンド、ダークロースト、カフェイン抜き、エスプレッソ
// 全ての飲み物の抽象クラス
public abstract class Beverage {
String description = "";
public String getDescription {
return description;
}
public abstract double cost();
}
public class Espresso extends Beverage {
public Espresso() {
description = "エスプレッソ";
}
public double cost() {
return 1.99;
}
}
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "ハウスブレンド";
}
public double cost() {
return 0.89;
}
}
- トッピングもあります スチームミルク、豆乳、モカ、ホイップクリーム 等です
- あ、それらを組み合わせる事も出来なければならないんでした
public class EspressoWithSteamedMilk extends Beverage {
public Espresso() {
description = "エスプレッソ スチームミルク";
}
public double cost() {
return 1.99 + 0.1;
}
}
public class EspressoWithSteamedMilkAndMocha extends Beverage {
public Espresso() {
description = "エスプレッソ スチームミルク モカ";
}
public double cost() {
return 1.99 + 0.1 + 0.2;
}
}
public class EspressoWithSteamedMilkAndSoy extends Beverage {
public Espresso() {
description = "エスプレッソ スチームミルク 豆乳";
}
public double cost() {
return 1.99 + 0.1 + 0.15;
}
}
public class EspressoWithSteamedMilkAndSoyAndWhip extends Beverage {
public Espresso() {
description = "エスプレッソ スチームミルク 豆乳 ホイップ";
}
public double cost() {
return 1.99 + 0.1 + 0.15 + 0.1;
}
}
ちょっと待って!これどう考えてもヤバイだろ!
- 違う、そうじゃない
- Beberageクラスにトッピング情報を持たせればいいじゃない
public abstract class Beverage {
String description = "";
Boolean milk = false;
Boolean soy = false;
Boolean mocha = false;
Boolean whip = false;
public String getDescription {
return description;
}
public setMilk() {
milk = true;
}
public hasMilk() {
milk;
}
public setSoy() {
soy = true;
}
public hasSoy() {
soy;
}
public double cost() {
float condimentCost = 0.0;
if (hasMilk()) {
condimentCost += 0.1;
}
if (hasWhip()) {
condimentCost += 0.1;
}
return condimentCost;
}
}
public class Espresso extends Beverage {
public Espresso() {
description = "エスプレッソ" + condimentDescriptions();
}
public double cost() {
1.99 + super.cost();
}
}
これも微妙じゃね?
-
違う、そうじゃない
-
ダブルモカとかどう表現するの?
-
あとで飲み物やトッピングが増えたり値段が変わったらどうするの?各所それぞれ変更しないといけないの?
-
継承を使って飲み物とトッピングの価格を表すのはうまく機能しない
-
クラス爆発・硬直した設計になる
-
ではどうすればいいの
-
Decoratorパターンでしょ
// 飲み物抽象クラス
public abstract class Beverage {
String description = "";
public String getDescription() {
return description;
}
public abstract double cost();
}
// トッピング抽象クラス
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
// 飲み物
public class Espresso extends Beverage {
public Espresso() {
description = "エスプレッソ";
}
public double cost() {
return 1.99;
}
}
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "ハウスブレンド ";
}
public double cost() {
return 0.89;
}
}
// トッピング
public class Mocha extends CondimentDecorator {
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + " モカ";
}
public double cost() {
return beverage.cost() + 0.2;
}
}
public class Whip extends CondimentDecorator {
Beverage beverage;
public Whip(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + " ホイップ";
}
public double cost() {
return beverage.cost() + 0.1;
}
}
Beverage beverage = new Espresso();
print(beverage.getDescription() + " $" + beverage.cost());
//=> エスプレッソ $1.99
Beverage beverage2 = new HouseBlend();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
print(beverage2.getDescription() + " $" + beverage2.cost());
//=> ハウスブレンド モカ モカ ホイップ $1.39
- Decoratorパターンはオブジェクトに付加的な責務を動的に(実行時に)付与、柔軟な機能拡張手段を提供、サブクラス化の代替手段となる
- CondimentDecorator(トッピング)クラスはBeverage(飲み物)クラスを継承している。継承は使うなって話じゃなかったの? => ここでは型を一致させる為に継承を使っている。振る舞いは一切取得していない
- インタフェースを使えばよかったのかも(私見)
- 今回の例で言うと、トッピングが追加されたり値段が変更された際も、その部分の修正だけでよい
「Rubyによるデザインパターン」でのRubyコードは(だいたい)こんな感じ
- オブジェクトの責務を変えたい、あるときは追加で必要になり、あるときは減らす、どうすればよいのか?
- Decoratorパターンを使うとレイヤ状に機能を積み重ねていく事ができ、それぞれの状況で必要なだけの機能を持つオブジェクトを作る事が出来る
ファイルにテキストを書き出す必要があるとします。そのシステムでは
- プレインテキストを出力したい
- 行番号を付与したい
- タイムスタンプを付与したい
時もある、そうじゃない時もある、それらを組み合わせたフォーマットで出力したいんだそうです
とりあえず、これらの要求に応える為に、適当なFile::IOを内部に持ったオブジェクトを作ってみます
class EnhancedWriter
def initialize(path=nil)
path ||= 'delete_me.txt'
@file = File.open(path, 'w')
@line_number = 1
end
def write_line(line)
@file.puts(line)
end
def timestamping_write_line(line)
write_line("#{Time.now}: #{line}")
end
def numbering_write_line(line)
write_line("#{@line_number}: #{line}")
@line_number += 1
end
def close
@file.close
end
end
e = EnhancedWriter.new
e.write_line('line_1')
e.write_line('line_2')
e.close
puts File.read('delete_me.txt')
# => line_1
# => line_2
e = EnhancedWriter.new
e.timestamping_write_line('line_1')
e.timestamping_write_line('line_2')
e.close
puts File.read('delete_me.txt')
#=> 2020-01-23 17:01:08 +0900: line_1
#=> 2020-01-23 17:01:08 +0900: line_2
e = EnhancedWriter.new
e.numbering_write_line('line_1')
e.numbering_write_line('line_2')
e.close
puts File.read('delete_me.txt')
#=> 1: line_1
#=> 2: line_2
-
フォーマットを組み合わせるにはどうする、フォーマットが増えたらどうなる?その組み合わせ数は爆発するだろう
-
クライアントは今から出力するテキストが行番号付きなのか、タイムスタンプ付きなのかを把握していなければならない、そのメソッドを間違いなく呼び出す必要がある
-
1つのクラスに全て突っ込んでいるこのクラスの問題は、全てを1つのクラスに詰め込んでいるまさにそのことにある
-
継承ベースのアプローチでは、あり得る全ての機能の組み合わせを設計時に考えなければならない
-
より良い解決策は、必要な機能の組み合わせを動的に、実行時に組み立てるようにする事
-
よろしくDecorator
class SimpleWriter
def initialize(path=nil)
path ||= 'delete_me.txt'
@file = File.open(path, 'w')
end
def write_line(line)
@file.puts(line)
end
def close
@file.close
end
end
class NumberingWriter
def initialize(real_writer)
@real_writer = real_writer
@line_number = 1
end
def write_line(line)
@real_writer.write_line("#{@line_number}: #{line}")
@line_number += 1
end
def close
@real_writer.close
end
end
class TimeStampWriter
def initialize(real_writer)
@real_writer = real_writer
end
def write_line(line)
@real_writer.write_line("#{Time.now}: #{line}")
end
def close
@real_writer.close
end
end
w = SimpleWriter.new
w.write_line('line_1')
w.write_line('line_2')
w.close
puts File.read('delete_me.txt')
#=> line_1
#=> line_2
w = SimpleWriter.new
w = NumberingWriter.new(w)
w.write_line('line_1')
w.write_line('line_2')
w.close
puts File.read('delete_me.txt')
#=> 1: line_1
#=> 2: line_2
w = SimpleWriter.new
w = TimeStampWriter.new(w)
w.write_line('line_1')
w.write_line('line_2')
w.close
puts File.read('delete_me.txt')
#=> 2020-01-23 17:01:08 +0900: line_1
#=> 2020-01-23 17:01:08 +0900: line_2
w = SimpleWriter.new
w = NumberingWriter.new(w)
w = TimeStampWriter.new(w)
w.write_line('line_1')
w.write_line('line_2')
w.close
puts File.read('delete_me.txt')
#=> 1: 2020-01-23 17:01:08 +0900: line_1
#=> 2: 2020-01-23 17:01:08 +0900: line_2
- 全てのデコレータオブジェクトにオリジナルと同じ基本的なインタフェースを持たせたので、デコレータに提供する"real_writer"が実際にSimpleWriterのインスタンスである必要はない
- つまり、好きな長さ、順番のデコレータチェーンを作れると言うこと
ちなみにRubyなら module と extend を使った方法もありえる
class SimpleWriter
# 変更なし
def initialize(path=nil)
path ||= 'delete_me.txt'
@file = File.open(path, 'w')
end
def write_line(line)
@file.puts(line)
end
def close
@file.close
end
end
module TimeStampWriter
def write_line(line)
super("#{Time.now}: #{line}")
end
end
module NumberingWriter
attr_reader :line_number
def write_line(line)
@line_number ||= 1
super("#{@line_number}: #{line}")
@line_number += 1
end
end
w = SimpleWriter.new
w.extend(NumberingWriter)
w.write_line('line_1')
w.write_line('line_2')
w.close
puts File.read('delete_me.txt')
#=> 1: line_1
#=> 2: line_2
w = SimpleWriter.new
w.extend(TimeStampWriter)
w.write_line('line_1')
w.write_line('line_2')
w.close
puts File.read('delete_me.txt')
#=> 2020-01-23 17:01:08 +0900: line_1
#=> 2020-01-23 17:01:08 +0900: line_2
w = SimpleWriter.new
w.extend(TimeStampWriter)
w.extend(NumberingWriter)
w.write_line('line_1')
w.write_line('line_2')
w.close
puts File.read('delete_me.txt')
#=> 2020-01-23 17:01:08 +0900: 1: line_1
#=> 2020-01-23 17:01:08 +0900: 2: line_2
- ああ、これはすごくいい感じがしますね(私見)
- このような動的なテクニックには1つ欠点があるかも知れません Rubyでは追加したモジュールを取り除く事ができない為です