概要
@hyuki 先生著の『Javaで学ぶデザインパターン入門』(2004年、SB Creative)の1章ずつをベースに、サンプルコードをC#で置き換えながら勉強していく記事です。
※著者の @hyuki 先生には適切に書籍への参照を入れれば問題ない旨ご確認いただいています。
本題
Abstract Factoryパターン
第8回はAbstract Factoryパターンです。Abstract Factoryパターンは「部品の具体的な実装には注目せず、インターフェース(API)に注目する。そして、そのインターフェース(API)だけを使って部品を組み立てる」ようなデザインパターンです。
サンプルコード
早速具体的な事例を見てみましょう。『Javaで学ぶデザインパターン入門』(2004年、SB Creative)に掲載されているコードをC#で(大体)書き換えます。
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Reflection;
namespace AbstractFactoryPattern
{
using Factory;
class Program
{
// Client
// AbstractFactoryとAbstractProductのインターフェース(API)だけを使って仕事を行う
static void Main(string[] args)
{
if(args.Length != 1)
{
Console.WriteLine("Usage: C# Main class.name.of.ConcreteFactory");
Console.WriteLine("Example 1: C# Main ListFactory.ListFactory");
Console.WriteLine("Example 2: C# Main TableFactory.TableFactory");
Environment.Exit(0);
}
Factory factory = Factory.GetFactory(args[0]);
Link asahi = factory.CreateLink("朝日新聞", "http://www.asashi.com/");
Link yomiuri = factory.CreateLink("読売新聞", "http://www.yomiuri.co.jp/");
Link usYahoo = factory.CreateLink("Yahoo!", "http://www.yahoo.com/");
Link jpYahoo = factory.CreateLink("Yahoo!Japan", "http://www.yahoo.co.jp/");
Link excite = factory.CreateLink("Excite", "http://www.excite.co.jp/");
Link google = factory.CreateLink("Google", "http://www.google.com/");
Tray traynews = factory.CreateTray("新聞");
traynews.Add(asahi);
traynews.Add(yomiuri);
Tray trayyahoo = factory.CreateTray("Yahoo!");
trayyahoo.Add(usYahoo);
trayyahoo.Add(jpYahoo);
Tray traysearch = factory.CreateTray("サーチエンジン");
traysearch.Add(trayyahoo);
traysearch.Add(excite);
traysearch.Add(google);
Page page = factory.CreatePage("LinkPage", "結城 浩");
page.Add(traynews);
page.Add(traysearch);
page.Output();
// 実行が一瞬で終わって確認できないので、キーの入力を待ちます
Console.ReadLine();
}
}
}
namespace Factory
{
// AbstractProduct
// ・AbstractFactoryによって作り出される抽象的な部品や製品のインターフェース(API)を定める
public abstract class Item
{
protected string caption;
public Item(string caption)
{
this.caption = caption;
}
public abstract string MakeHTML();
}
// AbstractProduct
public abstract class Link : Item
{
protected string url;
public Link(string caption, string url) : base(caption)
{
this.url = url;
}
}
// AbstractProduct
public abstract class Tray : Item
{
protected List<Item> tray = new List<Item>();
public Tray(string caption) : base(caption) { }
public void Add(Item item)
{
tray.Add(item);
}
}
// AbstractProduct
public abstract class Page
{
protected string title;
protected string author;
protected List<Item> content = new List<Item>();
public Page(string title, string author)
{
this.title = title;
this.author = author;
}
public void Add(Item item)
{
this.content.Add(item);
}
public void Output()
{
try
{
string filename = title + ".html";
using (StreamWriter writer = new StreamWriter(filename, false, Encoding.UTF8))
{
writer.Write(this.MakeHTML());
}
Console.WriteLine($"{filename}を作成しました。");
}
catch(IOException e)
{
Console.Error.WriteLine(e);
}
}
public abstract string MakeHTML();
}
// AbstractFactory
// ・AbstractProductのインスタンスを作り出すためのインターフェース(API)を定める
public abstract class Factory
{
public static Factory GetFactory(string classname)
{
Factory factory = null;
try
{
Assembly assembly = Assembly.GetExecutingAssembly();
factory = (Factory)assembly.CreateInstance(
classname,
false,
BindingFlags.CreateInstance,
null,
null,
null,
null
);
}
catch(TypeLoadException)
{
Console.Error.WriteLine($"クラス{classname}が見つかりません。");
}
catch(Exception e)
{
Console.Error.WriteLine(e.StackTrace);
}
return factory;
}
public abstract Link CreateLink(string caption, string url);
public abstract Tray CreateTray(string caption);
public abstract Page CreatePage(string title, string author);
}
}
namespace ListFactory
{
using Factory;
// ConcreteFactory
public class ListFactory : Factory
{
public override Link CreateLink(string caption, string url)
{
return new ListLink(caption, url);
}
public override Tray CreateTray(string caption)
{
return new ListTray(caption);
}
public override Page CreatePage(string title, string author)
{
return new ListPage(title, author);
}
}
// ConcreteProduct
public class ListLink : Link
{
public ListLink(string caption, string url) : base(caption, url) { }
public override string MakeHTML()
{
return $" <li><a href=\"{url}\">{caption}</a></li>\n";
}
}
// ConcreteProduct
public class ListTray : Tray
{
public ListTray(string caption) : base(caption) { }
public override string MakeHTML()
{
StringBuilder sb = new StringBuilder();
sb.Append("<li>\n");
sb.Append($"{caption}\n");
sb.Append("<ul>\n");
IEnumerator<Item> e = tray.GetEnumerator();
while (e.MoveNext())
{
sb.Append(e.Current.MakeHTML());
}
sb.Append("</ul>\n");
sb.Append("</li>\n");
return sb.ToString();
}
}
// ConcreteProduct
public class ListPage : Page
{
public ListPage(string title, string author) : base(title, author) { }
public override string MakeHTML()
{
StringBuilder sb = new StringBuilder();
sb.Append($"<html><head><title>{title}</title></head>\n");
sb.Append("<body>\n");
sb.Append($"<h1>{title}</h1>");
sb.Append("<ul>\n");
IEnumerator<Item> e = content.GetEnumerator();
while (e.MoveNext())
{
sb.Append(e.Current.MakeHTML());
}
sb.Append("</ul>\n");
sb.Append($"<hr><address>{author}</address>");
sb.Append("</body></html>\n");
return sb.ToString();
}
}
}
namespace TableFactory
{
using Factory;
// Concrete Factory
public class TableFactory : Factory
{
public override Link CreateLink(string caption, string url)
{
return new TableLink(caption, url);
}
public override Tray CreateTray(string caption)
{
return new TableTray(caption);
}
public override Page CreatePage(string title, string author)
{
return new TablePage(title, author);
}
}
// ConcreteProduct
public class TableLink : Link
{
public TableLink(string caption, string url) : base(caption, url) { }
public override string MakeHTML()
{
return $"<td><a href=\"{url}\">{caption}</a></td>\n";
}
}
// ConcreteProduct
public class TableTray : Tray
{
public TableTray(string caption) : base(caption) { }
public override string MakeHTML()
{
StringBuilder sb = new StringBuilder();
sb.Append("<td>");
sb.Append("<table width=\"100%\" border=\"1\"><tr>");
sb.Append($"<td bgcolor=\"#cccccc\" align=\"center\" colspan=\"{tray.Count}\"<b>{caption}</b></td>");
sb.Append("</tr>\n");
sb.Append("<tr>\n");
IEnumerator<Item> e = tray.GetEnumerator();
while (e.MoveNext())
{
sb.Append(e.Current.MakeHTML());
}
sb.Append("<tr></table>");
sb.Append("</tr>");
return sb.ToString();
}
}
// ConcreteProduct
public class TablePage : Page
{
public TablePage(string title, string author) : base(title, author) { }
public override string MakeHTML()
{
StringBuilder sb = new StringBuilder();
sb.Append($"<html><head><title>{title}</title></head>\n");
sb.Append("<body>\n");
sb.Append($"<h1>{title}</h1>\n");
sb.Append("<table width=\"80%\" border=\"3\">\n");
IEnumerator<Item> e = content.GetEnumerator();
while (e.MoveNext())
{
sb.Append($"<tr>{e.Current.MakeHTML()}</tr>");
}
sb.Append("</table>\n");
sb.Append($"<hr><address>{author}</address>");
sb.Append("</body></html>\n");
return sb.ToString();
}
}
}
実行結果
・ListFactory
<html><head><title>LinkPage</title></head>
<body>
<h1>LinkPage</h1><ul>
<li>
新聞
<ul>
<li><a href="http://www.asashi.com/">朝日新聞</a></li>
<li><a href="http://www.yomiuri.co.jp/">読売新聞</a></li>
</ul>
</li>
<li>
サーチエンジン
<ul>
<li>
Yahoo!
<ul>
<li><a href="http://www.yahoo.com/">Yahoo!</a></li>
<li><a href="http://www.yahoo.co.jp/">Yahoo!Japan</a></li>
</ul>
</li>
<li><a href="http://www.excite.co.jp/">Excite</a></li>
<li><a href="http://www.google.com/">Google</a></li>
</ul>
</li>
</ul>
<hr><address>結城 浩</address></body></html>
・TableFactory
<html><head><title>LinkPage</title></head>
<body>
<h1>LinkPage</h1>
<table width="80%" border="3">
<tr><td><table width="100%" border="1"><tr><td bgcolor="#cccccc" align="center" colspan="2"<b>新聞</b></td></tr>
<tr>
<td><a href="http://www.asashi.com/">朝日新聞</a></td>
<td><a href="http://www.yomiuri.co.jp/">読売新聞</a></td>
<tr></table></tr></tr><tr><td><table width="100%" border="1"><tr><td bgcolor="#cccccc" align="center" colspan="3"<b>サーチエンジン</b></td></tr>
<tr>
<td><table width="100%" border="1"><tr><td bgcolor="#cccccc" align="center" colspan="2"<b>Yahoo!</b></td></tr>
<tr>
<td><a href="http://www.yahoo.com/">Yahoo!</a></td>
<td><a href="http://www.yahoo.co.jp/">Yahoo!Japan</a></td>
<tr></table></tr><td><a href="http://www.excite.co.jp/">Excite</a></td>
<td><a href="http://www.google.com/">Google</a></td>
<tr></table></tr></tr></table>
<hr><address>結城 浩</address></body></html>
効能
- 組み合わせが要求される部品オブジェクト群を管理できる
- 部品オブジェクトの実装を隠ぺいできる
- Concrete Factoryの追加が楽(Abstract FactoryとAbstract Productに手順が定められている、別のConcrete Factoryに影響を与えない)
使用上の注意
- abstract purouctを足すのは大変(全てのConcrete Factoryを修正する必要がある)
関連しているパターン
- Builderパターン
- Factory Methodパターン
- Compositeパターン
- Singletonパターン
感想、疑問、メモ
- Builderパターンとの違いについての記事がたくさんありました。こんな感じでしょうか...
- Builder: 使用者は部品を自由に組み合わせてオブジェクトを初期化する
- Abstract Factory: 部品の組み合わせ方が決まった製品を得たい。使用者は個々の部品については知らない。
- Abstruct Productのプロパティを
protected
からprivate
にして、アクセサを準備するとAbstruct ProductとConcrete Productの結合度が下がる - オブジェクト指向における「抽象」: 具体的にどのように実装されているかについては感がえず、インターフェース(API)に注目している状態
- この章JavaのサンプルコードではJavaの標準クラスのIteratorパターン実装のIteratorクラスを使っていました。C#では
IEnumerator
を使うことになりますが、IteratorパターンのhasNext
はありません。かわりにMoveNext()
があり、次の要素の存在確認と次の要素への移動を行ってくれます。①Iteratorを勉強したときに感じた、要素1つすっ飛ばしてしまいそうとか解決できる形になっていて便利に感じました。 - .NET: is there a “HasNext” method for an IEnumerator?
- そして
IEnumerable<T>
を継承し、GetEnumerator()
をoverrideすればforeachやLINQが使えるみたいなインターフェース実装は再利用性だったり、利用者から把握しやすかったりしっかり設計されてるんだなぁと改めて思いました。 - 似たような名前・構成のパターンと混じるようになってきたので、できるだけ類似品との違いを書いていきたいです。
JavaとC#
C#で学ぶデザインパターン入門
①Iterator
②Adapter
③Template Method
④Factory Method
⑤Singleton
⑥Prototype
⑦Builder
⑧AbstractFactory
⑨Bridge
⑩Strategy
⑪Composite Pattern
⑫Decorator Pattern