0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

HeadFirstデザインパターン」第4章
Rubyによるデザインパターン」第12章

Factory パターン

「HeadFirstデザインパターン」でのJavaコードは(だいたい)こんな感じ

後述するが、こいつらが出てきます

  • Simple Factory (実際にはデザインパターンではないが一般的によく使われるのでここに挙げる)
  • Static Factory 「↑をスタティックメソッドで実装してみた」
  • Factory Method
  • Abstract Factory

  • 疎結合のOO設計
  • インスタンス化を必ずしもpublicで実行すべきではない
  • インスタンス化が結合の問題を頻繁に引き起こす

実装に対してではなくインタフェースに対してプログラミングする

  • "new"を見たら具象と考える
  • newを使うと確かに実装である具象クラスをインスタンス化している
  • コードを具象クラスに結びつけると脆弱で柔軟ではなくなる可能性がある

一連の関連するクラスがあるとき、多くの場合は以下のようなコードを書かなければならなくなる

// いろいろなカモクラスがあり
// 実行時にどのクラスをインスタンス化するか決まる
// 実行時までどのクラスをインスタンス化するか判らない
Duck duck;

if (public) {
  duck = new MallardDuck();
} else if (hunting) {
  duck = new DecoyDuck();
} else if (inBathTub) {
  duck = new RubberDuck();
}

このようなコードを見ると、変更や拡張の際にはこのコードを見直し、検証する必要があることが判ります
多くの場合このようなコードが各所に分散し、保守と更新が困難に、バグを生みやすくなる

とは言っても、いつかはnewを使ってインスタンスを作る必要があるよね?

  • 技術的には、newには何も問題はない
  • 本当の問題は"変更"であり、変更によってnewの使用箇所がどのような影響を受けるかということ

  • インタフェースに対してプログラミングすると、将来システムに起こり得る多数の変更を分離できる
  • インタフェースに対してコードを記述すると、多態性により、そのコードはそのインタフェースを実装した新しいクラスに対しても動作する
  • 多数の具象クラスを使ったコードでは新しい具象クラスを追加する際にコードを変更しなければならない可能性がある
  • コードが「変更に対して閉じて」いないことになる
  • 変化する部分を特定、不変な部分と分離する

突然ですが、貴方はピザ屋を経営しています

public class PizzaStore {
  puclic Pizza orderPizza() {
    Pizza pizza = new Pizza();

    pizza.prepare(); // 下処理
    pizza.bake(); // 焼いて
    pizza.cut(); // カットして
    pizza.box(); // 箱に詰める

    return pizza;
  }
}

もちろん、数種類のピザがあります

public class PizzaStore {
  puclic Pizza orderPizza(String type) {
    Pizza pizza;

    if (type.equals("チーズ")) {
      pizza = new CheesePizza();
    } else if (type.equals("ギリシャ")) {
      pizza = new GreekPizza();
    } else if (type.equals("ペパロニ")) {
      pizza = new PrpperoniPizza();
    }

    pizza.prepare(); // 下処理
    pizza.bake(); // 焼いて
    pizza.cut(); // カットして
    pizza.box(); // 箱に詰める

    return pizza;
  }
}
  • ピザの種類に基づいて正しい具象クラスをインスタンス化する
  • それぞれのピザがPizzaインタフェースを実装する必要がある
  • Pizzaの各タイプはそれぞれの下処理の方法を知っている

競合他社が最近の流行りのピザをメニューに追加していることがわかりました

  • 我々もメニューの追加を行います
  • ギリシャピザがあまり売れていないのでメニューから外します
public class PizzaStore {
  puclic Pizza orderPizza(String type) {
    Pizza pizza;

    if (type.equals("チーズ")) {
      pizza = new CheesePizza();
-   } else if (type.equals("ギリシャ")) {
-     pizza = new GreekPizza();
    } else if (type.equals("ペパロニ")) {
      pizza = new PrpperoniPizza();
    } else if (type.equals("クラム")) {
      pizza = new ClamPizza();
    } else if (type.equals("野菜")) {
      pizza = new VeggiePizza();
    }

    pizza.prepare(); // 下処理
    pizza.bake(); // 焼いて
    pizza.cut(); // カットして
    pizza.box(); // 箱に詰める

    return pizza;
  }
}

これは、メニューの更新があった場合、毎回同じような変更を行うの?

変化する部分としない部分が判った、なら

  • オブジェクト作成部分(orderPizzaメソッド)を取り出し、ピザの作成だけを取り扱う別のオブジェクトに移動します
  • この別のオブジェクトには名前があります
  • ファクトリです

  • ファクトリはオブジェクト作成の詳細に対処する
  • SimplePizzaFactoryがあれば、orderPizza()メソッドはこのファクトリのクライアントになるだけです
  • orderPizza()メソッドでピザが必要になった時にはファクトリにピザの作成を頼みます
  • orderPizza()メソッドがギリシャピザやクラムピザについて知っている必要は、もうありません
  • orderPizza()メソッドはPizzaインタフェースを実装したピザを取得し、prepare(), bake()などのメソッドを呼び出せることだけを知っていればよい

Simple Factory

public class SimplePizzaFactory {
  public Pizza createPizza(String type) {
    Pizza pizza = null;

    if (type.equals("チーズ")) {
      pizza = new CheesePizza();
    } else if (type.equals("ペパロニ")) {
      pizza = new PrpperoniPizza();
    } else if (type.equals("クラム")) {
      pizza = new ClamPizza();
    } else if (type.equals("野菜")) {
      pizza = new VeggiePizza();
    }

    return pizza;
  }
}

public class PizzaStore {
  SimplePizzaFactory factory;

  public PizzaStore(SimplePizzaFactory factory) {
    this.factory = factory;
  }

  puclic Pizza orderPizza(String type) {
    Pizza pizza;

    pizza = factory.createPizza(type)

    pizza.prepare(); // 下処理
    pizza.bake(); // 焼いて
    pizza.cut(); // カットして
    pizza.box(); // 箱に詰める

    return pizza;
  }
  • Simple Factoryは、実際にはデザインパターンではない
  • むしろプログラミングのイディオム
  • しかし一般的に使われているので紹介する
  • このイディオムを「Factoryパターン」だと思っている人も多い

Static Factory

  • 簡単なファクトリをスタティックメソッドとして定義するのは一般的なテクニックであり
  • スタティックファクトリと呼ばれることがあよくある
  • なぜスタティックメソッドを使うの? => 作成メソッドを使う為にファクトリオブジェクトをインスタンス化する必要がない
  • (後述の)ファクトリをサブクラス化する、ような事が出来ず、作成メソッドの振る舞いをカスタマイズする事ができない、という事でもある

あなたのピザ屋がフランチャイズ化する

  • あなたのピザ屋は繁盛しています
  • フランチャイズ本部(あなた)としては、各フランチャイズ店にはこれまでと同じように実績のあるコードを使用してもらいたい
  • 各フランチャイズ店は、地域のピザ好きの好みによって異なる種類のピザ(ニューヨークスタイル、シカゴスタイル等)を提供したいはず

とりあえず1つの方法を見てみる

SimplePizzaFactoryを外し、
NYPizzaFacroty, ChicagoPizzaFactory といった異なるファクトリを作成すると
各PizzaStoreは適切なファクトリを使用するように構成できるはず

// こんな感じ?

NYPizzaFacroty nyFactory = new NYPizzaFacroty();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.order("野菜")

ShicagoPizzaFacroty chicagoFactory = new ChicagoPizzaFacroty();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.order("野菜")

ピザ屋のフレームワーク

  • ピザ作成処理をPizzaStoreに集中させ、フランチャイズ店に自由を与えて独自の地域スタイルを持たせる方法
  • その為にはcreatePizza()メソッドをPizzaStoreに戻しますが、今回はこのメソッドを抽象メソッドとし、それぞれの地域スタイル用のPizzaStoreサブクラスを作成していく
public abstract class PizzaStore {
  puclic Pizza orderPizza(String type) {
    Pizza pizza;

    pizza = createPizza(type)

    pizza.prepare(); // 下処理
    pizza.bake(); // 焼いて
    pizza.cut(); // カットして
    pizza.box(); // 箱に詰める

    return pizza;
  }

  abstract Pizza createPizza(String type);

↑何が変わった?

  • PizzaStoreは抽象クラス。必ず継承され、そのサブクラスを使用するという事
  • createPizza() はSimpleFactoryオブジェクトからではなく、PizzaStoreのメソッドを呼び出すようになった
  • createPizza() はPizzaStoreの抽象メソッド。サブクラスで必ず実装されるという事

各地域型のサブクラスを作る

public class NYPizzaStore extends PizzaStore {
  Pizza createPizza(String type) {
    if (type.equals("チーズ")) {
      pizza = new NYStyleCheesePizza();
    } else if (type.equals("ペパロニ")) {
      pizza = new NYStylePrpperoniPizza();
    } else if (type.equals("クラム")) {
      pizza = new NYStyleClamPizza();
    } else if (type.equals("野菜")) {
      pizza = new NYStyleVeggiePizza();
    }

    return pizza;
  }
}

  • スーパークラスの createPizza() メソッドを実装している
  • 各地域のPizzaStoreの違いはピザのスタイル(ニューヨークだとかシカゴだとか)
  • これらの違いをcreatePizzaメソッドに押し込め、このファクトリの役割を果たすメソッドに適切な種類のピザを作る全責務を担わせる
  • そのためにcreatePizzaの内容をPizzaStoreのサブクラスに実装する
  • それぞれ独自のピザを持つ、PizzaStoreの具象サブクラスを多数持つことになる
  • すべてのサブクラスはPizzaStoreフレームワークに収まり、洗練されたorderPizza()メソッドはそのまま利用できるということ
  • PizzaStore.orderPizza()はどのピザを作るのか知る事はない。分離されているという事
// やっとピザを作るよ!
public abstract class Pizza {
  String    name;
  String    dough;
  String    sauce;
  ArrayList toppings = new ArrayList();

  void prepare() {
    print(name + "を下処理");
    print("こねる");
    print("ソースを追加");
    print("トッピング");
    for (int i = 0, s = toppings.size; i < s; i++) {
      print(" " + toppings.get(i));
    }
  }

  void beke() {
    print("焼く");
  }

  void cut() {
    print("扇形に切り分ける");
  }

  void box() {
    print("しっかりした箱に入れる");
  }

  public String getName() {
    return name;
  }
}
public class NYStyleCheesePizza extends Pizza {
  public NYStyleCheesePizza() {
    name  = "ニューヨークスタイル ソース&チーズ";
    dough = "薄いクラスト生地";
    sauce = "マリナラソース";
    toppings.add("粉レッジャーノチーズ");
  }
}

public class ChicagoStyleCheesePizza extends Pizza {
  public ChicagoStyleCheesePizza() {
    name  = "シカゴスタイル ディープディッシュチーズ";
    dough = "極厚クラスト生地";
    sauce = "プラムトマトソース";
    toppings.add("刻んだモッツァレラチーズ");
    toppings.add("何かの粉");
  }

  void cut() {
    // シカゴスタイルではピザを四角に切るようにメソッドをオーバーライドする
    print("四角に切り分ける")
  }
}

おまたせ!ピザを作るよ!

PizzaStore nyStore = new NYPizzaStore();
Pizza pizza = nyPizzaStore.orderPizza("チーズ");
PizzaStore chicagoStore = new ChicagoPizzaStore();
Pizza pizza = chicagoStore.orderPizza("野菜");

Factory Methodパターン

  • 作成するオブジェクトをサブクラスに決定させる事によりオブジェクト作成をカプセル化する
  • スーパークラスのクライアントコードをサブクラスのオブジェクト作成コードから分離する
  • スーパークラスはファクトリメソッドを持つインタフェースを提供する
  • ファクトリメソッドと組み合わせたorderPizza()メソッドを提供することで、ファクトリメソッドがフレームワークを提供する

そして次のパターンへ

フランチャイズ店が低品質の食材を使用し、利益を増やしている事が判りました

食材の一貫性の確保

  • 各フランチャイズで使用する食材を取り決めたい
  • 各フランチャイズは様々な地域に立地している
  • つまりニューヨークでは新鮮なクラム貝が手に入るが、シカゴでは冷凍クラムで妥協しなければならない、ような事が起こる

食材ファクトリ

  • このファクトリは一連の食材の中の各食材を作成する役割を担う
  • 地域ごとの違いに対処し、生地、ソース、チーズなどを作成する
// すべての食材を作成するファクトリのインタフェース定義
// Ingredient : 食材
public interface PizzaIngredientFactory {
  public Dough     createDough();
  public Source    createSource();
  public Cheese    createCheese();
  public Veggies[] createVeggies();
  public Pepperoni createPepperoni();
  public Clams     createClam();
}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
  public Dough createDough() {
    return new ThinCrustDough();
  }

  public Sauce createSauce() {
    return new MarinaraSouce();
  }

  public Cheese createCheese() {
    return new ReggianoCheese();
  }

  public Veggies[] createVeggies() {
    Veggies veggies[] = {
      new Garlic(),
      new Onion(),
      new Mashroom(),
      new RedPepper()
    };
    return veggies;
  }

  public Pepperoni createPepperoni() {
    // 薄霧パペロニ、これはニューヨークでもシカゴでも手に入ります
    return new SlicedPepperoni();
  }

  public Clams createClam() {
    // ニューヨークは海岸沿いなので新鮮なクラムが手に入る
    // シカゴでは残念ながら冷凍物で我慢する事になるでしょう
    return new FreshClams();
  }
}
public abstract class Pizza {
  String    name;
  Dough     dough;
  Sauce     sauce;
  Veggies   veggies[];
  Cheese    cheese;
  Pepperoni pepperoni;
  Clams     clam;

  // prepare()メソッドが抽象メソッドになり、
  // 各ピザの実装にて、必要な材料が取得される
  abstract void prepare();

  void setName(String name) {
    this.name = name;
  }

  // ここより下は以前と全く同じコード
  void beke() {
    print("焼く");
  }

  void cut() {
    print("扇形に切り分ける");
  }

  void box() {
    print("しっかりした箱に入れる");
  }

  public String getName() {
    return name;
  }
}
public class CheesePizza extends Pizza {
  PizzaIngredientFactory ingredientFactory;

  public CheesePizza(PizzaIngredientFactory ingredientFactory) {
    this.ingredientFactory = ingredientFactory;
  }

  void prepare() {
    print(name + " を下処理");
    dough  = ingredientFactory.createDough();
    sauce  = ingredientFactory.createSauce();
    cheese = ingredientFactory.createCheese();
  }
}

public class ClamPizza extends Pizza {
  PizzaIngredientFactory ingredientFactory;

  public CheesePizza(PizzaIngredientFactory ingredientFactory) {
    this.ingredientFactory = ingredientFactory;
  }

  void prepare() {
    print(name + " を下処理");
    dough  = ingredientFactory.createDough();
    sauce  = ingredientFactory.createSauce();
    cheese = ingredientFactory.createCheese();
    clam   = ingredientFactory.createClam();
  }
}
// 変更なし
public abstract class PizzaStore {
  puclic Pizza orderPizza(String type) {
    Pizza pizza;

    pizza = createPizza(type)

    pizza.prepare(); // 下処理
    pizza.bake(); // 焼いて
    pizza.cut(); // カットして
    pizza.box(); // 箱に詰める

    return pizza;
  }

  abstract Pizza createPizza(String type);
public class NYPizzaStore extends PizzaStore {
  Pizza createPizza(String type) {
    Pizza pizza = null;
    // ここで、NYPizzaStore は NYPizzaIngredientFactory を使う事が決まっている
    PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();

    if (type.equals("チーズ")) {
      pizza = new CheesePizza(ingredientFactory);
      pizza.setName("ニューヨークスタイルチーズピザ")
    } else if (type.equals("ペパロニ")) {
      pizza = new PepperoniPizza(ingredientFactory);
      pizza.setName("ニューヨークスタイルペパロニピザ")
    } else if (type.equals("クラム")) {
      pizza = new ClamPizza(ingredientFactory);
      pizza.setName("ニューヨークスタイルクラムピザ")
    } else if (type.equals("野菜")) {
      pizza = new VeggiePizza(ingredientFactory);
      pizza.setName("ニューヨークスタイル野菜ピザ")
    }

    return pizza;
  }
}

おまたせ!ピザを作るよ!

PizzaStore nyStore = new NYPizzaStore();
Pizza pizza = nyPizzaStore.orderPizza("チーズ");

Abstract Factory

  • 日本語なら抽象ファクトリ
  • かなりのコードを変更した。我々は何をしたのか
  • 一連の製品 を作成する為の手段を提供する
  • 一連の製品 とは、今回の例でいうと生地、ソース、チーズ、肉、野菜など、ピザを作る材料です
  • これにより、異なるコンテキスト向けの製品を作成する様々なファクトリを実装できる
  • 具象クラスを指定することなく(関知することなく)一連の関連オブジェクトや依存オブジェクトを作成する
  • クライアントは具体的な製品の詳細から完全に分離される

  • Abstract Factory の各メソッドは Factory Method として実装される
  • Factory Method は継承を利用する。オブジェクトを作成するためのファクトリメソッドを実装したサブクラスに移譲します
  • Abstratct Methodはオブジェクトコンポジションを使用する。オブジェクト生成をファクトリインタフェースで公開されたメソッドで実装する

「Rubyによるデザインパターン」でのRubyコードは(だいたい)こんな感じ

  • 池の生息環境シミュレータ
  • いつものアヒルやカモがガーガー鳴いてるアレ

アヒルは食べて眠って鳴く

class Duck
  def initialize(name)
    @name = name
  end

  def eat
    p "アヒル #{@name} 食事中"
  end

  def speak
    p "アヒル #{@name} ガーガー鳴いている"
  end

  def sleep
    p "アヒル #{@name} 寝てる"
  end
end

アヒルには住む場所が必要なので 池(Pond)クラス を作る

class Pond
  def initialize(number_ducks)
    @ducks = []

    number_ducks.times do |i|
      duck = Duck.new("アヒル#{i}")
      @ducks << duck
    end

    def simulate_one_day
      @ducks.each { |duck| duck.speak }
      @ducks.each { |duck| duck.eat }
      @ducks.each { |duck| duck.sleep }
    end
  end
end

シミュレーション

pond = Pond.new(2)
pond.simulate_one_day
#=> "アヒル アヒル0 ガーガー鳴いている"
#=> "アヒル アヒル1 ガーガー鳴いている"
#=> "アヒル アヒル0 食事中"
#=> "アヒル アヒル1 食事中"
#=> "アヒル アヒル0 寝てる"
#=> "アヒル アヒル1 寝てる"

仕様変更

カエルを追加するそうです。同じインタフェースを持つカエルclassを実装するのは簡単です

class Frog
  def initialize(name)
    @name = name
  end

  def eat
    p "カエル #{@name} 食事中"
  end

  def speak
    p "カエル #{@name} ゲロゲロ鳴いている"
  end

  def sleep
    p "カエル #{@name} 寝ずにゲロゲロ鳴いている"
  end
end

しかし、Pondクラスには問題が

initializeメソッドにて、直接 Duck.new を使用している

def initialize(number_ducks)
  @ducks = []

  number_ducks.times do |i|
    duck = Duck.new("アヒル#{i}") # <= ココ
    @ducks << duck
  end
end

問題

  • 問題は、変わらないものから変わりうるものを分離する必要があること
  • 変わりうるものとは、池に住む生物の種 [アヒル、カエル]
  • その他のPondクラスの機能は変わらない
  • どうにかしてPondクラスから Duck.new を取り除くことができれば、Pondクラスはアヒルもカエルもサポートできるようになる
  • このジレンマが「クラスの選択」という本性の中心的な課題

  • 「クラスの選択」の問題を扱う1つの方法は、その問いをサブクラスに押し付けること
  • 汎用的な基底クラスを作る事から始める
  • 汎用的な、とは、この基底クラス自身が「クラスの選択」の決断をしないという意味
  • 基底クラスが新しいオブジェクトを必要とするときはサブクラスで定義されているメソッドを呼び出す
# 基底「池」クラス
class Pond
  def initialize(number_animals)
    @animals = []

    number_animals.times do |i|
      animal = new_animal("動物#{i}") # <= ココ
      @animals << animal
    end
  end

  def simulate_one_day
    @animals.each { |animal| animal.speak }
    @animals.each { |animal| animal.eat }
    @animals.each { |animal| animal.sleep }
  end
end

Pondクラスの、2つのサブクラスを作る

  • 1つはアヒルでいっぱいの池
  • 1つはカエルでいっぱいの池
class DuckPond < Pond
  def new_animal(name)
    Duck.new(name)
  end
end

class FrogPond < Pond
  def new_animal(name)
    Frog.new(name)
  end
end
FrogPond.new(2).simulate_one_day
#=> "カエル 動物0 ゲロゲロ鳴いている"
#=> "カエル 動物1 ゲロゲロ鳴いている"
#=> "カエル 動物0 食事中"
#=> "カエル 動物1 食事中"
#=> "カエル 動物0 寝ずにゲロゲロ鳴いている"
#=> "カエル 動物1 寝ずにゲロゲロ鳴いている"

Factory Methodパターン

このような「クラスの選択」の決定をサブクラスに押し付けるテクニックを、Gofでは Factory Methodパターンと呼んでいます

お、お前は・・Template Method・・?!

  • Factory Methodパターンは、実は全く新しいパターンではなく、 Template Methodパターンを新しいオブジェクトの作成の問題に適用しただけ
  • どちらのパターンも、そのアルゴリズムの汎用的な部分(simulate_one_dayメソッド)を汎用的な基底クラスに記述し、基底クラスで残された殻の部分をサブクラスが埋めます

パラメータ化された Factory Methodパターン

今度は 植物についてもこれまでの動物と同じようにシミュレーションするように依頼されました

# 藻クラス
class Algae
  def initialize(name)
    @name = name
  end

  def grow
    p "藻 #{@name} 日光を浴びて育つ"
  end
end

# スイレンクラス
class WaterLily
  def initialize(name)
    @name = name
  end

  def grow
    p "スイレン #{@name} は浮きながら日光を浴びて育つ"
  end
end

Pondクラスも植物に対応できるように修正します

class Pond
  def initialize(number_animals, number_plants) # <= ココ
    @animals = []

    number_animals.times do |i|
      animal = new_animal("動物#{i}")
      @animals << animal
    end

    @plants = [] # <= ココ

    number_plants.times do |i| # <= ココ
      plant = new_animal("植物#{i}")
      @plants << plant
    end
  end

  def simulate_one_day
    @plants.each  { |plant| plant.grow } # <= ココ
    @animals.each { |animal| animal.speak }
    @animals.each { |animal| animal.eat }
    @animals.each { |animal| animal.sleep }
  end
end

さらに、Pondのサブクラスも植物を作成できるように修正します

# アヒルとスイレンがある池クラス
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

どう考えてもダメな方向に向かってる

  • 動物、植物の数が増えると組み合わせが爆発するだろう
  • このように扱いにくい実装になったのは、作り出そうとしているオブジェクトの方によってメソッドを使い分けてしまった為
  • つまり、new_animal(), new_plant()メソッドがあるからです

別のもっときれいな方法

  • ファクトリメソッドを1つだけ作り、そのファクトリメソッドには作るべきオブジェクトの方を知らせるパラメータを受け取るようにする
  • 次の例は、引数で渡されたシンボルによって動物と植物のどちらも作り出すことができる
class Pond
  def initialize(number_animals, number_plants)
    @animals = []

    number_animals.times do |i|
      animal = new_organism(:animal, "動物#{i}") # <= ココ
      @animals << animal
    end

    @plants = []

    number_plants.times do |i|
      plant = new_organism(:plant, "植物#{i}") # <= ココ
      @plants << plant
    end
  end

  def simulate_one_day
    @plants.each  { |plant| plant.grow }
    @animals.each { |animal| animal.speak }
    @animals.each { |animal| animal.eat }
    @animals.each { |animal| animal.sleep }
  end
end

class DuckWaterLilyPond < Pond
  def new_organism(type, name)
    if type == :animal
      Duck.new(name)
    elsif type == :plant
      WaterLily.new(name)
    end
  end
end

パラメータ化されたファクトリメソッドを使うと、サブクラスが定義するファクトリメソッドが1つだけになるため、コードを小さく出来る

いや、Pondのサブクラスの組み合わせの数が爆発するのが問題なんじゃないの?

  • このPondクラスを使えば、以前のような複雑なことをしなくても良くなります
  • コンストラクタに植物と動物のクラスを渡せばいいのです
  • たくさんあったクラスを1つに押しやることが出来ました

(私見) Factory Methoodがすっかり姿を消してしまったんだけどいいのか?

class Pond
  def initialize(number_animals, animal_class, number_plants, plant_class)
    @animal_class = animal_class
    @plant_class  = plant_class

    @animals = []

    number_animals.times do |i|
      animal = new_organism(:animal, "動物#{i}")
      @animals << animal
    end

    @plants = []

    number_plants.times do |i|
      plant = new_organism(:plant, "植物#{i}")
      @plants << plant
    end
  end

  def simulate_one_day
    @plants.each  { |plant|  plant.grow }
    @animals.each { |animal| animal.speak }
    @animals.each { |animal| animal.eat }
    @animals.each { |animal| animal.sleep }
  end

  def new_organism(type, name)
    if type == :animal
      @animal_class.new(name)
    elsif type == :plant
      @plant_class.new(name)
    end
  end
end
Pond.new(2, Duck, 2, Algae).simulate_one_day
#=> "藻 植物0 日光を浴びて育つ"
#=> "藻 植物1 日光を浴びて育つ"
#=> "アヒル 動物0 ガーガー鳴いている"
#=> "アヒル 動物1 ガーガー鳴いている"
#=> "アヒル 動物0 食事中"
#=> "アヒル 動物1 食事中"
#=> "アヒル 動物0 寝てる"
#=> "アヒル 動物1 寝てる"

次の仕様変更

  • 池以外の生息地を扱えるようにします
  • とりあえずジャングル
class Tree
  def initialize(name)
    @name = name
  end

  def grow
    p "樹木 #{@name} が高く育つ"
  end
end

class Tiger
  def initialize(name)
    @name = name
  end

  def eat
    p "トラ #{@name} は何でも食べる"
  end

  def speak
    p "トラ #{@name} はガオーと吠える"
  end

  def sleep
    p "トラ #{@name} はいつでも寝る"
  end
end

Pond(池)クラスはもっとふさわしい名前に変更します Habitat(生息環境)にします

Habitat.new(1, Tiger, 2, Algae).simulate_one_day
#=> "藻 植物0 日光を浴びて育つ"
#=> "藻 植物1 日光を浴びて育つ"
#=> "トラ 動物0 はガオーと吠える"
#=> "トラ 動物0 は何でも食べる"
#=> "トラ 動物0 はいつでも寝る"

一貫したオブジェクトの生成

  • 新しいHabitatクラスの問題点の1つは、動植物の生物的にありえない組み合わせを作ることが出来てしまうことです
  • トラと藻が一緒に居るのはおかしいとは何も教えてはくれません
  • シミュレーションが詳細になってきたとして、いろいろな種類の置物が増え、魚とキノコを一緒に育てたくはありません
  • この問題は、その生息環境に住まわせる生物の指定方法を変えることで対応します
  • Habitatに個々の動植物クラスを渡す代わりに、つじつまの合う製品の組み合わせの作り方を知っているオブジェクトを渡すようにします
  • 例えば、池に合うようにカエルとスイレンを作り、ジャングルに合うようにトラと樹木を作るオブジェクトです
  • 矛盾のないオブジェクトの組み合わせを作るためのこのオブジェクトは、アブストラクトファクトリと呼ばれます

Abstract Factory パターン

ついに来た

class PondOrganismFactory
  def new_animal(name)
    Frog.new(name)
  end

  def new_plant(name)
    Algae.new(name)
  end
end

class JungleOrganismFactory
  def new_animal(name)
    Tiger.new(name)
  end

  def new_plant(name)
    Tree.new(name)
  end
end

Habitatクラスからアブストラクトファクトリを使用するようにする

class Habitat
  def initialize(number_animals, number_plants, organism_factory) # <= ココ
    @organism_factory = organism_factory

    @animals = []

    number_animals.times do |i|
      animal = @organism_factory.new_animal("動物#{i}")
      @animals << animal
    end

    @plants = []

    number_plants.times do |i|
      plant = @organism_factory.new_plant("植物#{i}")
      @plants << plant
    end
  end

  def simulate_one_day
    @plants.each  { |plant|  plant.grow }
    @animals.each { |animal| animal.speak }
    @animals.each { |animal| animal.eat }
    @animals.each { |animal| animal.sleep }
  end
end
Habitat.new(1, 2, JungleOrganismFactory.new).simulate_one_day
#=> "樹木 植物0 が高く育つ"
#=> "樹木 植物1 が高く育つ"
#=> "トラ 動物0 はガオーと吠える"
#=> "トラ 動物0 は何でも食べる"
#=> "トラ 動物0 はいつでも寝る"
  • ジャングルの住人と池の生物を混ぜ合わせるような混乱もなく、平穏に過ごすことが出来ます
  • (私見) なんのこっちゃ

こんなやり方も

class OrganismFacroty
  def initialize(animal_class, plant_class)
    @animal_class = animal_class
    @plant_class  = plant_class
  end

  def new_animal(name)
    @animal_class.new(name)
  end

  def new_plant(name)
    @plant_class.new(name)
  end
end
jungle_organism_factory = OrganismFactory.new(Tiger, Tree)
jungle = Habitat.new(1, 2, jungle_organism_factory)
jungle.simulate_one_day

pond_organism_factory = OrganismFactory.new(Frog, WaterLily)
pond = Habitat.new(1, 2, pond_organism_factory)
pond.simulate_one_day
  • ↑の説明
  • 個々のクラスを指定するのを避けるためにアブストラクトファクトリを作ったのではなかったか?
  • 上記の実装では、トラと藻が同居できる謎のOrganismFactoryが作れてしまうのではないか?
  • アブストラクトファクトリで大事なことは、どの製品の型が一緒になるのかという知識をカプセル化することなのです
  • (私見)どの製品とどの製品でグループを形成するか、という知識をカプセル化するもの
  • どのようにうやろうとも、一緒になるものの種類を知っているオブジェクト、に行き着く

Factoryパターンの使用上の注意

  • どのオブジェクト生成のテクニックにしても、失敗する最悪の方法は「使うべきではないところに使う」こと
  • すべてのオブジェクトをファクトリから生成する必要があるわけではない
  • 複数の関連したクラスがあり、その中から選ばなければならない場合のみに使用する

まとめ

  • どちらのFactoryパターンも「どのクラスを選ぶのか」という問いに応えるための技術
  • Factory Methodパターンは Template Methodパターンをオブジェクトの生成に応用したもの
  • Abstract Methodパターンは矛盾のないオブジェクトの組を作りたいときに使う
0
1
1

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?