デザインパターンFactory Pattern
今回は4章ファクトリーパターンをC#で勉強していきます
Factoryパターンは大きく分けて3つにわけることができます。どれも基本的にやっていることは同じで、一つのクラスの中で制作元と制作先を一緒にするのではなく、分けることで汎用性を大幅に増やすことができます。制作元と制作先の間にクラスやインターフェイスを噛ませることで抽象度を上げ、依存性の向上にもつながります。
パターンのシンプルな導入としてSimple Factory Pattern,次にFactory Pattern,そしてAbstract Factory Patternと派生させることができます。この本ではピザ屋が例として挙げられているので、それを使って説明していきます。今回はSimple Factory PatternとFactory Patternのみを扱います。Abstract Factory Patternは簡単にだけ理解できたことを書いています。
0.Simple Pizza
まずはシンプルなチーズピザを作るピザ屋さんを作ります。ピザ屋さんはピザを準備、焼く、切る、箱に入れる作業を行います。
class SimplePizzaStore
{
public SimplePizza orderPizza()
{
SimplePizza pizza = new SimplePizza();
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
次に、ピザを実装します。ピザには名前、生地、ソース、トッピングを与えることができます。
public class SimplePizza
{
private string name;
private string dough;
private string sauce;
private List<string> toppings = new List<string>();
public SimplePizza()
{
name = "Simple Pizza";
this.dough = "Simple Dough";
this.sauce = "Simple Tomato Sauce";
toppings.Add("Cheese");
}
public void prepare()
{
Console.WriteLine("Preparing " + name);
Console.WriteLine("Tossing dough...");
Console.WriteLine("Adding sauce... ");
Console.WriteLine("Adding toppings: ");
for (int i = 0; i < toppings.Count; i++)
{
Console.WriteLine(" " + toppings[i]);
}
}
public void bake()
{
Console.WriteLine("Bake for 25 minutes at 350");
}
public virtual void cut()
{
Console.WriteLine("Cutting the pizza into diagonal slices");
}
public void box()
{
Console.WriteLine("Place pizza in official PizzaStore box");
}
public string getName()
{
return name;
}
}
static void Main(string[] args)
{
SimplePizzaStore simplePizzaStore = new SimplePizzaStore();
SimplePizza simplePizza = simplePizzaStore.orderPizza();
Console.WriteLine("Ethan ordered a " + simplePizza.getName() + ".\n");
}
華氏350度(摂氏175くらい)で25分焼いてシンプルなピザを作りました。
シンプルなピザしか作れないのでは客が怒ってしまうので、ほかのピザも作れるように店主は頑張ります。
チーズのほかにペパロニと野菜が乗ったピザも作れるようになりました。チーズと言えばチーズピザが、ペパロニと言えばペパロニピザが出てきます。
public Pizza orderPizza(string type)
{
Pizza pizza;
if(type == "cheese")
{
pizza = new CheesePizza();
}
else if(type == "pepperoni")
{
pizza = new PepperoniPizza();
}
else if(type == "veggie")
{
pizza = new VeggiePizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
このまま種類を増やして同じメソッド内に書いていってもよいですが、このままだと種類が増えると管理が大変になりそうです。
このピザの種類の分岐部分だけ取り出して別に書いてみる方法はどうでしょうか?
1.Simple Factory Pattern
今まではピザ屋でレジとキッチンは分かれていませんでしたが、この方法だと分けることができそうです。
レジはキッチンにピザの種類を伝え、キッチンからそのピザを受け取ります。キッチンはレジから受け取った注文に沿ったピザを作りレジに渡します。
レジの人は注文に沿ったピザが出てくればよいのでたとえキッチンがよそからピザを買って持ってきていたとしても気が付くことはありません。レジからは具体的なピザが見えないという意味できれいな分業になりました。
public class PizzaStore
{
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory)
{
this.factory = factory;
}
public Pizza orderPizza(string type)
{
Pizza pizza;
pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
public class SimplePizzaFactory
{
public Pizza createPizza(string type)
{
Pizza pizza = null;
if (type == "cheese")
{
pizza = new CheesePizza();
}
else if (type == "pepperoni")
{
pizza = new PepperoniPizza();
}
else if (type == "veggie")
{
pizza = new VeggiePizza();
}
return pizza;
}
}
UML図で表すとこのようになります。ピザ屋からはピザは直接見えません、そして具体的なピザたちは抽象化されたピザから作ることができます。
実はこのピザ屋の店主には弟子がいて、弟子が「違うスタイルのピザを売る」と言って新しい店を立ち上げようとしています。弟子も師匠には恩義を感じているのでフランチャイズという形で二号店を開きます。
2.店舗を増やす
店舗を二つにし、分けずに一つのクラスで処理をしようと思うとこのようになってしまいます。
class DependentPizzaStore
{
public Pizza createPizza(string style,string type)
{
Pizza pizza = null;
if(style == "NY")
{
if (type == "cheese")
{
pizza = new NYCheesePizza();
}
else if (type == "pepperoni")
{
pizza = new NYPepperoniPizza();
}
else if (type == "veggie")
{
pizza = new NYVeggiePizza();
}
}
else if(style == "Chicago")
{
if (type == "cheese")
{
pizza = new ChicagoCheesePizza();
}
else if (type == "pepperoni")
{
pizza = new ChicagoPepperoniPizza();
}
else if (type == "veggie")
{
pizza = new ChicagoVeggiePizza();
}
}
else
{
Console.WriteLine("Such store does not exist");
return null;
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
このままだと前回同様の問題に当たってしまいそうです。
3.Factory Pattern
ピザを抽象化して具体的な異なるピザを作ることができるなら、ピザ店そのものを抽象化してスタイルの異なるピザ屋も作れるはずです。
店主と弟子は、ニューヨークの薄いピザとシカゴディープディッシュの両方で店を開く模様です。
NYスタイルとシカゴスタイルのピザです。
どちらも食べたことがありますが私は薄いピザの方が好みです。
抽象的なピザ屋クラスを作ります。ここでは具体的にどのようなピザを作るのかについての記述はありません。
public abstract class PizzaStore
{
public Pizza orderPizza(string type)
{
Pizza pizza;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
protected abstract Pizza createPizza(string type);
}
NY式ピザ屋です。ここでは具体的にタイプの違うピザが出てきます。
public class NYPizzaStore: PizzaStore
{
protected override Pizza createPizza(string type)
{
if (type == "cheese")
{
return new NYStyleCheesePizza();
}
else if (type == "veggie")
{
return new NYStyleVeggiePizza();
}
else if (type == "pepperoni")
{
return new NYStylePepperoniPizza();
}
else return null;
}
}
シカゴ式も同様に違うタイプのピザを出すことができます。
public class ChicagoPizzaStore : PizzaStore
{
protected override Pizza createPizza(string type)
{
if (type == "cheese")
{
return new ChicagoStyleCheesePizza();
}
else if (type == "veggie")
{
return new ChicagoStyleVeggiePizza();
}
else if (type == "pepperoni")
{
return new ChicagoStylePepperoniPizza();
}
else return null;
}
}
まだピザがないのでここからピザを具体的に作っていきます。
ここでも抽象的なものから作っていきます。
public class Pizza
{
public string Name { get; set; }
public string Dough { get; set; }
public string Sauce { get; set; }
public List<string> Toppings { get; set; } = new List<string>();
public void prepare()
{
Console.WriteLine("Preparing " + Name);
Console.WriteLine("Tossing dough...");
Console.WriteLine("Adding sauce... ");
Console.WriteLine("Adding toppings: ");
for(int i = 0;i < Toppings.Count; i++)
{
Console.WriteLine(" " + Toppings[i]);
}
}
public void bake()
{
Console.WriteLine("Bake for 25 minutes at 350");
}
public virtual void cut()
{
Console.WriteLine("Cutting the pizza into diagonal slices");
}
public void box()
{
Console.WriteLine("Place pizza in official PizzaStore box");
}
public string getName()
{
return Name;
}
}
NY式チーズピザです。ほかのピザも似たように作れるのでここでは記述しません。
public class NYStyleCheesePizza : Pizza
{
public NYStyleCheesePizza()
{
Name = "NY Style Sause and Cheese Pizza";
Dough = "Thin Crust Dough";
Sauce = "Marinara Sauce";
Toppings.Add("Grated Reggiano Cheese");
}
}
シカゴ式ペパロニピザもNY式と似たように書いていきます。ピザの切り方が斜めではなく四角に切るようなので、cutメソッドを上書きしていきます。面倒だから同じ切り方でよいという場合は何も書く必要はありません。
public class ChicagoStylePepperoniPizza : Pizza
{
public ChicagoStylePepperoniPizza()
{
Name = "Chicago Style Deep Dish Cheese Pizza";
Dough = "Extra Thick Crust Dough";
Sauce = "Plum Tomato Sauce";
Toppings.Add("Shredded Mozzarella Cheese");
}
public override void cut()
{
Console.WriteLine("Cutting the pizza into square slices");
}
}
最後にピザ工場を作り、ピザづくりを実行します。
class Program
{
static void Main(string[] args)
{
PizzaStore nyStore = new NYPizzaStore();
Pizza cheesepizza = nyStore.orderPizza("cheese");
Console.WriteLine("Ethan ordered a " + cheesepizza.getName() + "\n");
Pizza veggiepizza = nyStore.orderPizza("veggie");
Console.WriteLine("Ethan ordered a " + veggiepizza.getName() + "\n");
PizzaStore chicagoStore = new ChicagoPizzaStore();
Pizza chicagoCheesePizza = chicagoStore.orderPizza("cheese");
Console.WriteLine("Ethan ordered a " + chicagoCheesePizza.getName() + "\n");
}
}
NY式チーズピザ、NY式野菜ピザ、Chicago式チーズピザが出てきました。
NY式ピザ屋はNY式ピザを、Chicago式ピザ屋はChicago式ピザを作るよう分業ができるようになりました。
ファクトリーパターンは下の図のように一般化することができます。
現在のNYピザの状況を上の図に当てはめてみましょう。ピザを作るクリエイターはピザ屋で、具体的なNYピザを作るのはNYピザ屋です。
NYピザはピザクラスをベースに、NYピザ屋はピザ屋クラスをベースにそれぞれ作ることができます。(plant umlでクラス一つ一つを重ねることができたりしないのでしょうか…?)
このようにファクトリーパターンを使うと将来ピザ屋の種類を増やすことも減らすことも簡単にできるようになります。
この章ではSOLID原則のDに当たる「依存性逆転の原則」にも触れています。ピザ屋を基準にNYチーズピザや、Chicagoペパロニピザ等具体的なピザを作ってしまった場合、ピザ屋が上、その下に具体的なピザたちがあるという上下関係のある構造になります。これはピザ屋が具体的なピザたちに依存している一方的な関係です。中間に抽象的なピザクラス(またはインターフェイス)を挟むことによってピザ屋が抽象的なピザに依存、具体的なピザも抽象的なピザに依存することになるので、上から見ても下から見ても依存性が変わらないような状況を作ることができます。
一方的に依存するものよりもこの方が何かと都合がよいみたいですが、SOLID原則は何となくの理解しかできていないのでこの説明を鵜呑みにしないでください。あくまでここまでで理解できたことを文章にしています。
Abstract Factory Pattern
簡単にしか触れませんが、今までの抽象化を生地、ソース、チーズの種類にも当てはめることができます。
いままでは各ピザ内で毎回どのような素材が使われるかを定義してきましたが、チーズという抽象クラスを作り、そこからチェダーチーズやモッツァレラチーズを作ることでチーズもFactory Patternのように抽象化することができます。チーズを仕入れるお得意様が倒産してしまった場合や品切れを起こした場合にチーズを抽象化しておくことですぐに入れ替えができるようになります。