LoginSignup
1
0

More than 5 years have passed since last update.

「HeadFirstデザインパターン」と「Rubyによるデザインパターン」を読んで Decorator パターン

Posted at

何番煎じか判りませんがお勉強メモを残します

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では追加したモジュールを取り除く事ができない為です
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0