Abstract Factoryパターンの例
多くのデザインパターンと同様、Abstract FactoryパターンとBuilderパターンは、クラス構造という点で見ると非常によく似ています。どちらも複合的な一群のオブジェクトを構成するために用いられる生成パターンです。しかし、Builderパターンが複合構造の各要素を段階的に構成することに重点を置くのに対し、Abstract Factoryは構成されたオブジェクトを直ちに返すように設計されています。
この2つのパターンの実装において、クライアントコードはそれぞれ異なる役割を果たします。一般に、Builderパターンを使う場合、クライアントはビルダーに渡すコンポーネントの種類を知っています。一方、Abstract Factoryを使う場合、クライアントはインターフェイスの実装を要求し、返されるオブジェクト構造の実装がどのような種類なのかを特に関知しません。
Abstract FactoryとBuilderの違いを際立たせるため、以前の記事「デザインパターンの使い方: Builder」で紹介した例を再び使うことにします。図書館の所蔵目録は、書籍や映画などのさまざまな資料をまとめたコレクションです(リスト1を参照)。図書館では、いろいろなレポートがちょくちょくプリントされます。その中でも比較的大きなものに所蔵資料レポートがあります。これは館内の全資料を種類別にソートして一覧にしたものです。これらのレポートはWeb上に表示されることもあれば、テキストベースの簡単なデスクトップユーザーインターフェイスに出力されることもあります(おかげで、ちょっとしたテキストもこうして簡単に再利用できるわけです! )。
// Material.java: public class Material { private String classification; private String title; private String director; private String year; public Material(String classification, String title, String director, String year) { this.classification = classification; this.title = title; this.director = director; this.year = year; } public String getAuthor() { return director; } public String getClassification() { return classification; } public String getTitle() { return title; } public String getYear() { return year; } } // Book.java: public class Book extends Material { public Book(String classification, String title, String author, String year) { super(classification, title, author, year); } // ... } // Movie.java: public class Movie extends Material { public Movie(String classification, String title, String director, String year) { super(classification, title, director, year); } public String getDirector() { return getAuthor(); } // ... }
Catalogクラスは資料を収容する簡単なコンテナです(リスト2)。
import java.util.*; public class Catalog implements Iterable<Material> { private List<Material> materials = new ArrayList<Material>(); public void add(Material material) { materials.add(material); } public List<Material> materials() { return materials; } @Override public Iterator<Material> iterator() { return materials.iterator(); } }
レポート作成サブシステムが達成すべき目標は、クライアントがどのプラットフォーム(Webまたはデスクトップ)を対象としているかという情報と資料の一覧を受け取り、プラットフォームに応じて適切なレポートを作成することです。クライアントはenum
(列挙型)を使用してプラットフォームを指定します(リスト3)。今回の実装では、Abstract Factoryパターンを使います。Builderパターンの記事を見てわかるように、レポートを構成する手順は簡単です。見出しを出力し、ソートされた資料の一覧を反復的に走査して1行に1資料を出力し、最後に要約セクションを出力します。
public enum Platform { WEB, DESKTOP }
Abstract Factoryパターンを適用してレポートを作成してもBuilderパターンのときと同じ結果が得られるはずなので、2つのレポートを比較する簡単な統合テストプログラムを作成しました。このプログラム(リスト4)を見ると、クライアントがAbstract FactoryとBuilderの両方でレポートを生成する様子がよくわかります。
import static org.junit.Assert.*; import org.junit.*; import builder.*; import domain.*; public class FactoryTest { private Catalog catalog; @Before public void initialize() { catalog = createCatalog(); } @Test public void compareWebReports() { CatalogReportFactory factory = CatalogReportFactory.create(catalog, Platform.WEB); CatalogReportDirector director = new CatalogReportDirector(catalog); assertEquals(director.generate(new HtmlCatalogBuilder()), factory.generate()); } @Test public void compareDesktopReports() { CatalogReportFactory factory = CatalogReportFactory.create(catalog, Platform.DESKTOP); CatalogReportDirector director = new CatalogReportDirector(catalog); assertEquals(director.generate(new PrintCatalogBuilder()), factory.generate()); } private Catalog createCatalog() { Catalog catalog = new Catalog(); catalog.add(new Book("QA234.543.34", "Java Puzzlers", "Bloch, Joshua", "2006")); catalog.add(new Book("QA234.543.33", "Agile Java", "Langr, Jeff", "2005")); catalog.add(new Movie("ZZ234", "Fight Club", "Fincher, David", "1999")); catalog.add(new Book("QA234.543.35", "Clean Code", "Martin, Robert C.", "2008")); catalog.add(new Movie("ZZ2888", "A Clockwork Orange", "Kubrick, Stanley", "1971")); catalog.add(new Book("QA234.543.399", "Working Effectively With Legacy Code", "Feathers, Michael", "2004")); catalog.add(new Book("QA234.543.452", "Secrets of Consulting", "Weinberg, Gerald", "1995")); catalog.add(new Movie("ZZ234", "Kill Bill: Vol. 1", "Tarantino, Quentin", "2003")); return catalog; } }
CatalogReportFactoryで定義されている静的メソッドcreate
自体が、ファクトリメソッド(つまり「工場」メソッド)となります。このメソッドの仕事はクライアントの要求にぴったり合ったファクトリ(工場)を返すことです。クライアントの要求を適切なファクトリにつなげるこの部分こそ、Abstract Factoryパターンの心臓部です。リスト5に、CatalogReportFactoryの実装を示します。
import java.util.*; import domain.*; public abstract class CatalogReportFactory { protected final Catalog catalog; public static CatalogReportFactory create(Catalog catalog, Platform platform) { if (platform == Platform.WEB) return new HtmlCatalogReportFactory(catalog); return new PrintCatalogReportFactory(catalog); } public CatalogReportFactory(Catalog catalog) { this.catalog = catalog; } public String generate() { Report report = new Report(); createReportHeader().appendTo(report); for (Material material: sortedCatalog()) createDetailRecord(material).appendTo(report); createSummary().appendTo(report); return report.toString(); } private List<Material> sortedCatalog() { return new MaterialSorter(catalog.materials()).sort(); } abstract protected ReportComponent createSummary(); abstract protected ReportComponent createDetailRecord(Material material); abstract protected ReportComponent createReportHeader(); }