概要
Factory Methodパターンで紹介したインスタンスの生成を、抽象化して複数パターン用意するためのデザインパターン。
サンプルプログラム
階層構造を持ったリンク集のHTMLを出力するためのサンプルプログラム。以下のテストのように使用する。
import { Factory } from 'abstractFactory/factory/factory';
import { Link } from 'abstractFactory/factory/link';
import { Page } from 'abstractFactory/factory/page';
import { Tray } from 'abstractFactory/factory/tray';
describe('Abstract Factory', () => {
test('ListFactory', () => {
const listFactory: Factory = Factory.getFactory('ListFactory');
const asahi: Link = listFactory.createLink(
'朝日新聞',
'https://www.asahi.com/',
);
const yomiuri: Link = listFactory.createLink(
'読売新聞',
'https://www.yomiuri.co.jp/',
);
const yahoo: Link = listFactory.createLink(
'Yahoo!',
'https://www.yahoo.co.jp/',
);
const google: Link = listFactory.createLink(
'Google',
'https://www.google.com/',
);
const newsTray: Tray = listFactory.createTray('新聞');
newsTray.add(asahi);
newsTray.add(yomiuri);
const searchTray: Tray = listFactory.createTray('サーチエンジン');
searchTray.add(yahoo);
searchTray.add(google);
const page: Page = listFactory.createPage('LinkPage', '結城浩');
page.add(newsTray);
page.add(searchTray);
const html = page.makeHTML();
expect(html).toEqual(`<html><head><title>LinkPage</title></head>
<body>
<h1>LinkPage</h1>
<ul>
<li>
新聞
<ul>
<li><a href=\"https://www.asahi.com/\">朝日新聞</a></li>
<li><a href=\"https://www.yomiuri.co.jp/\">読売新聞</a></li>
</ul>
</li>
<li>
サーチエンジン
<ul>
<li><a href=\"https://www.yahoo.co.jp/\">Yahoo!</a></li>
<li><a href=\"https://www.google.com/\">Google</a></li>
</ul>
</li>
</ul>
<hr><address>結城浩</address>
</body></html>`);
});
test('TableFactory', () => {
const tableFactory: Factory = Factory.getFactory('TableFactory');
const asahi: Link = tableFactory.createLink(
'朝日新聞',
'https://www.asahi.com/',
);
const yomiuri: Link = tableFactory.createLink(
'読売新聞',
'https://www.yomiuri.co.jp/',
);
const yahoo: Link = tableFactory.createLink(
'Yahoo!',
'https://www.yahoo.co.jp/',
);
const google: Link = tableFactory.createLink(
'Google',
'https://www.google.com/',
);
const newsTray: Tray = tableFactory.createTray('新聞');
newsTray.add(asahi);
newsTray.add(yomiuri);
const searchTray: Tray = tableFactory.createTray('サーチエンジン');
searchTray.add(yahoo);
searchTray.add(google);
const page: Page = tableFactory.createPage('LinkPage', '結城浩');
page.add(newsTray);
page.add(searchTray);
const html = page.makeHTML();
expect(html).toEqual(`<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=\"https://www.asahi.com/\">朝日新聞</a></td>
<td><a href=\"https://www.yomiuri.co.jp/\">読売新聞</a></td>
</tr></table>
</td></tr>
<tr><td>
table width=\"100%\" border=\"1\"><tr>
<td bgcolor=\"#cccccc\" align=\"center\" colspan=\"2\"><b>サーチエンジン</b></td>
</tr>
<tr>
<td><a href=\"https://www.yahoo.co.jp/\">Yahoo!</a></td>
<td><a href=\"https://www.google.com/\">Google</a></td>
</tr></table>
</td></tr>
</table>
<hr><address>結城浩</address>
</body></html>`);
});
});Factory.getFactory('FactoryName')でFactoryを指定※- ファクトリのインスタンス
createLinkでURLとテキストを持つリンクを作る - ファクトリのインスタンス
createTrayでリンクを分類するトレイを作る - ファクトリのインスタンス
createPageで、トレイをまとめるページを作る page.makeHTML()でhtmlを出力する
※このサンプルプログラムでは、ulタグとliタグによる階層構造のファクトリと、tableタグによる階層構造のファクトリから選べる。
ファクトリ
階層構造を持つhtmlを出力するためのファクトリは、どのような階層構造であろうと必要になるリンク・トレイ・ページを作るためのメソッドを持っているため、その抽象クラスを用意する必要がある。
import { Link } from 'abstractFactory/factory/link';
import { Tray } from 'abstractFactory/factory/tray';
import { Page } from 'abstractFactory/factory/page';
type FactoryType = 'ListFactory' | 'TableFactory';
export abstract class Factory {
public static getFactory(className: FactoryType): Factory {
switch (className) {
case 'ListFactory':
return new ListFactory();
case 'TableFactory':
return new TableFactory();
}
}
public abstract createLink(caption: string, url: string): Link;
public abstract createTray(caption: string): Tray;
public abstract createPage(title: string, author: string): Page;
}
import { ListLink } from 'abstractFactory/list/listLink';
import { ListPage } from 'abstractFactory/list/listPage';
import { ListTray } from 'abstractFactory/list/listTray';
class ListFactory extends Factory {
public createLink(caption: string, url: string): Link {
return new ListLink(caption, url);
}
public createTray(caption: string): Tray {
return new ListTray(caption);
}
public createPage(title: string, author: string): Page {
return new ListPage(title, author);
}
}
import { TableLink } from 'abstractFactory/table/tableLink';
import { TablePage } from 'abstractFactory/table/tablePage';
import { TableTray } from 'abstractFactory/table/tableTray';
class TableFactory extends Factory {
public createLink(caption: string, url: string): Link {
return new TableLink(caption, url);
}
public createTray(caption: string): Tray {
return new TableTray(caption);
}
public createPage(title: string, author: string): Page {
return new TablePage(title, author);
}
}Factoryがファクトリの抽象クラスで、それをul・liなのか、tableなのかで具象にしたものがListFactoryとTableFactory。
FactoryはstaticメソッドgetFactoryを持っており、渡された引数に応じて具体的なファクトリのインスタンスを返す。
ファクトリはLink、Tray、Pageといった、抽象化された共通のインターフェースのメソッドを持っている。
共通のメソッドのインターフェース
リンク・トレイ・ページの抽象クラス郡。
export abstract class Item {
protected caption: string;
public constructor(caption: string) {
this.caption = caption;
}
public abstract makeHTML(): string;
}htmlを組み立てるための部品の最小単位。
import { Item } from 'abstractFactory/factory/item';
export abstract class Link extends Item {
protected url: string;
public constructor(caption: string, url: string) {
super(caption);
this.url = url;
}
}リンク。Itemを継承している。
import { Item } from 'abstractFactory/factory/item';
export abstract class Tray extends Item {
protected tray: Item[] = [];
public constructor(caption: string) {
super(caption);
}
public add(item: Item): void {
this.tray.push(item);
}
}トレイ。Itemの配列を持っており、addでItemを配列に追加できる。
import { Item } from 'abstractFactory/factory/item';
export abstract class Page {
protected title: string;
protected author: string;
protected content: Item[] = [];
public constructor(title: string, author: string) {
this.title = title;
this.author = author;
}
public add(item: Item): void {
this.content.push(item);
}
public abstract makeHTML(): string;
}ページ。ページのタイトルと著者、ページのコンテンツを持っている。ページのコンテンツはaddで追加できる。
具体的なファクトリの実装
ListFactory
ul・liによる階層構造を持ったhtmlを出力するファクトリ。クラスのコードは上記### ファクトリのセクションに載せたもの。ListFactoryのcreateXxxが返すインスタンスは、Link、Tray、Pageを以下のように実装している。
import { Link } from 'abstractFactory/factory/link';
export class ListLink extends Link {
public constructor(caption: string, url: string) {
super(caption, url);
}
public makeHTML(): string {
return `<li><a href=\"${this.url}\">${this.caption}</a></li>`;
}
}import { Item } from 'abstractFactory/factory/item';
import { Tray } from 'abstractFactory/factory/tray';
export class TableTray extends Tray {
public constructor(caption: string) {
super(caption);
}
public makeHTML(): string {
const html: string[] = [];
html.push('<td>');
html.push('table width="100%" border="1"><tr>');
html.push(
`<td bgcolor=\"#cccccc\" align=\"center\" colspan=\"${this.tray.length}\"><b>${this.caption}</b></td>`,
);
html.push('</tr>');
html.push('<tr>');
this.tray.map((item: Item) => {
html.push(item.makeHTML());
});
html.push('</tr></table>');
html.push('</td>');
return html.join('\n');
}import { Item } from 'abstractFactory/factory/item';
import { Page } from 'abstractFactory/factory/page';
export class TablePage extends Page {
public constructor(title: string, author: string) {
super(title, author);
}
public makeHTML(): string {
const html: string[] = [];
html.push(`<html><head><title>${this.title}</title></head>`);
html.push('<body>');
html.push(`<h1>${this.title}</h1>`);
html.push('<table width="80%" border="3"');
this.content.map((item: Item) => {
html.push(`<tr>${item.makeHTML()}</tr>`);
});
html.push('</table>');
html.push(`<hr><address>${this.author}</address>`);
html.push('</body></html>');
return html.join('\n');
}
}TableFactory
TableFactoryのcreateXxxが返すインスタンスは、Link、Tray、Pageを以下のように実装している。
import { Link } from 'abstractFactory/factory/link';
export class TableLink extends Link {
public constructor(caption: string, url: string) {
super(caption, url);
}
public makeHTML(): string {
return `<td><a href=\"${this.url}\">${this.caption}</a></td>`;
}
}import { Item } from 'abstractFactory/factory/item';
import { Tray } from 'abstractFactory/factory/tray';
export class TableTray extends Tray {
public constructor(caption: string) {
super(caption);
}
public makeHTML(): string {
const html: string[] = [];
html.push('<td>');
html.push('table width="100%" border="1"><tr>');
html.push(
`<td bgcolor=\"#cccccc\" align=\"center\" colspan=\"${this.tray.length}\"><b>${this.caption}</b></td>`,
);
html.push('</tr>');
html.push('<tr>');
this.tray.map((item: Item) => {
html.push(item.makeHTML());
});
html.push('</tr></table>');
html.push('</td>');
return html.join('\n');
}
}import { Item } from 'abstractFactory/factory/item';
import { Page } from 'abstractFactory/factory/page';
export class TablePage extends Page {
public constructor(title: string, author: string) {
super(title, author);
}
public makeHTML(): string {
const html: string[] = [];
html.push(`<html><head><title>${this.title}</title></head>`);
html.push('<body>');
html.push(`<h1>${this.title}</h1>`);
html.push('<table width="80%" border="3"');
this.content.map((item: Item) => {
html.push(`<tr>${item.makeHTML()}</tr>`);
});
html.push('</table>');
html.push(`<hr><address>${this.author}</address>`);
html.push('</body></html>');
return html.join('\n');
}
}感想・考察
- 登場する役が多くて理解するのがけっこう大変だった。
-
JavaのコードそのままTypeScriptに変換できないものがいくつかあったので工夫した。
- 本だと
Factoryでは受け取ったクラス名の文字列classNameをjava.lang.Class.forName('className').newInstance()してインスタンスにしていた。 - 本だと
FactoryクラスとListFactory・TableFactoryは別ファイルにしていたが、TypeScriptだとできなかったので一緒のファイルにした。 - 上記の
forNameが無い問題のせいで、ListFactoryなどを参照せねばならない。 FactoryがListFactoryを参照→ListFactoryがFactoryを参照(継承のため)の循環参照が発生してしまう。- というかJavaだとなんでこれできるのか。
- 本だと
