『Java言語で学ぶデザインパターン入門』でBuilderパターンを学びました。
Builderパターンの構造自体は理解できたものの、コードを追っていく中で次のような疑問が出てきました。
- DirectorはなぜTextBuilderとHTMLBuilderを使い分けられるのか
- Builder型で受け取っているのに、なぜTextBuilder側のメソッドが実行されるのか
- TextBuilder内で使われている
append()はどこから来ているのか - 子クラスだけに独自メソッドを追加してもよいのか
- Builderは抽象クラスではなくインターフェースでもよいのではないか
Builderパターンを追っていくことで、単なるデザインパターンだけではなく、継承やポリモーフィズム、抽象クラスとインターフェースについても理解が深まりました。
この記事では、学習した内容を整理します。
Builderパターンの基本構造
書籍の例では、おおまかに次のような構造になっています。
Director
│
利用
▼
Builder
▲ ▲
継承 継承
│ │
TextBuilder HTMLBuilderそれぞれの役割は次のように考えられます。
Builder:文章を組み立てるための共通操作を定義するTextBuilder:テキスト形式で文章を組み立てるHTMLBuilder:HTML形式で文章を組み立てるDirector:どの順番で文章を組み立てるかを定義するMain:どのBuilderを使用するかを決める
たとえばBuilderには次のようなメソッドがあります。
public abstract class Builder {
public abstract void makeTitle(String title);
public abstract void makeString(String str);
public abstract void makeItems(String[] items);
public abstract void close();
}
そして、TextBuilderとHTMLBuilderがこれらをそれぞれ実装します。
Directorは「作り方」だけを知っている
DirectorはBuilderを受け取ります。
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public void construct() {
builder.makeTitle("Greeting");
builder.makeString("一般的な挨拶");
builder.makeItems(
new String[] {
"おはようございます。",
"こんにちは。"
}
);
builder.close();
}
}
ここで重要なのは、Directorが知っているのはBuilderだけということです。
Directorは、
TextBuilder
や、
HTMLBuilder
を直接扱っていません。
つまりDirectorの役割は、
タイトルを作る
↓
文章を作る
↓
項目を作る
↓
完成させる
という「組み立て手順」を決めることです。
実際にテキストを作るのかHTMLを作るのかは、Directorの責任ではありません。
Mainで具体的なBuilderを渡す
Mainでは、実際に使用するBuilderのインスタンスを生成します。
たとえばTextBuilderを使用する場合です。
TextBuilder textBuilder = new TextBuilder();
Director director = new Director(textBuilder);
director.construct();
まず、
new TextBuilder()
によってTextBuilderのインスタンスが生成されます。
そして、そのインスタンスをDirectorに渡します。
Directorのコンストラクタは、
public Director(Builder builder)
となっています。
TextBuilderはBuilderを継承しているため、Builder型として受け取ることができます。
Builder
▲
│
TextBuilder
つまり、
new Director(textBuilder);
が成立します。
Builder型なのにTextBuilderのメソッドが実行される理由
ここが最初に大きく疑問に感じた部分でした。
Director内部では、
Builder builder;
として保持しています。
そのため、
builder.makeTitle("Greeting");
を見ると、BuilderクラスのmakeTitle()を実行しているように見えます。
しかし、実際にはTextBuilderのmakeTitle()が実行されます。
理由は、変数の型と実際のインスタンスの型が異なるからです。
Builder builder = new TextBuilder();
この場合、
変数としての型
Builder
実際のインスタンス
TextBuilder
という状態です。
イメージすると次のようになります。
Builder型の変数
builder
│
▼
TextBuilderのインスタンス
builder.makeTitle()が実行されたとき、Javaは実際に参照しているオブジェクトに対応するメソッドを実行します。
今回の実体はTextBuilderなので、
TextBuilder.makeTitle()
が実行されます。
HTMLBuilderを渡していれば、
HTMLBuilder.makeTitle()
が実行されます。
この仕組みがポリモーフィズム(多態性)です。
DirectorはTextBuilderとHTMLBuilderを判定していない
最初は、DirectorかJavaのどこかで、
これはTextBuilderか?
それともHTMLBuilderか?
と判定しているのではないかと思っていました。
しかし、Directorはそのような判定をしていません。
たとえば次のような処理は不要です。
if (builder instanceof TextBuilder) {
// TextBuilder用の処理
} else if (builder instanceof HTMLBuilder) {
// HTMLBuilder用の処理
}
Directorは単純に、
builder.makeTitle();
と呼んでいるだけです。
どの実装が呼ばれるかは、実際に格納されているオブジェクトによって決まります。
この仕組みによって、Directorは具体的なBuilderを意識する必要がなくなっています。
車や運転手に例える
この関係は「運転手」という共通の役割で考えるとイメージしやすくなります。
たとえば、
Driver
▲
継承
┌┴──────┐
TaxiDriver BusDriver
という関係があるとします。
共通のDriverには、
void start();
という操作があります。
TaxiDriverなら、
@Override
void start() {
// タクシーとして出発する
}
BusDriverなら、
@Override
void start() {
// バスとして出発する
}
という実装になります。
受付係が、
「運転手さん、出発してください」
と依頼するとします。
受付係は、その人がTaxiDriverなのかBusDriverなのかを意識する必要はありません。
「Driverである」という共通のルールだけ知っていればよいわけです。
実際にそこにいるのがTaxiDriverならタクシーとして出発し、BusDriverならバスとして出発します。
Builderパターンに当てはめると、
受付係 → Director
運転手 → Builder
タクシー運転手 → TextBuilder
バス運転手 → HTMLBuilder
出発する → makeTitle()など
という関係になります。
ただし、「実際のオブジェクトに応じて呼び出すメソッドが自動的に切り替わる」という部分はJavaの仕組みです。
現実世界の例えだけでは完全には再現できない部分でもあります。
TextBuilderのappend()はどこから来ているのか
TextBuilderを読んでいると、
buffer.append("...");
というコードが登場します。
最初はappend()がBuilderから継承されたメソッドなのかと思いました。
しかし、これはBuilderとは関係ありません。
TextBuilderには、たとえば次のようなフィールドがあります。
private StringBuilder buffer = new StringBuilder();
bufferはStringBuilderクラスのインスタンスです。
append()はJava標準ライブラリのStringBuilderが持っているメソッドです。
つまり、
buffer.append("Hello");
は
StringBuilder型のbuffer
↓
StringBuilderのappend()を呼び出す
という処理です。
継承関係ではありません。
TextBuilder
│
└── StringBuilder buffer
│
└── append()
TextBuilderが内部にStringBuilderを持っていて、その機能を利用しています。
子クラスだけに独自メソッドを追加してもよい
TextBuilderにBuilderには存在しない独自メソッドを追加することもできます。
たとえば、
public class TextBuilder extends Builder {
public void saveTextFile() {
// テキストファイルとして保存する
}
}
という実装です。
これはJavaとして問題ありません。
しかし、Directorからは呼び出せません。
Directorが持っているのは、
Builder builder;
だからです。
そのため、
builder.saveTextFile();
と書くことはできません。
BuilderにはsaveTextFile()が定義されていないためです。
一方、
TextBuilder textBuilder = new TextBuilder();
textBuilder.saveTextFile();
なら呼び出せます。
つまり、
Builder型として扱う
→ Builderに定義された操作だけ利用できる
TextBuilder型として扱う
→ Builderの操作 + TextBuilder独自の操作を利用できる
という違いがあります。
BuilderパターンとしてDirectorから呼びたいならBuilderに定義する
このことから、Builderパターンにおけるメソッドの置き場所についても整理できます。
Directorから使わせたい操作であれば、Builder側に定義する必要があります。
abstract class Builder {
abstract void makeTitle();
abstract void makeString();
}
Directorはこれを使います。
builder.makeTitle();
builder.makeString();
一方で、TextBuilderだけで必要な処理なら、TextBuilder独自のメソッドでも問題ありません。
たとえば、
String getTextResult()
のような完成品を取得する処理です。
これはDirectorが組み立てるための操作ではなく、Main側が完成品を取得するために使えます。
TextBuilder textBuilder = new TextBuilder();
Director director = new Director(textBuilder);
director.construct();
String result = textBuilder.getTextResult();
そのため、
Builderのメソッド
→ Directorが組み立てに使う共通操作
TextBuilder独自のメソッド
→ TextBuilder固有の処理
と考えると整理しやすくなります。
Builderはインターフェースでもよいのではないか
書籍ではBuilderが抽象クラスとして定義されています。
public abstract class Builder {
public abstract void makeTitle(String title);
public abstract void makeString(String str);
public abstract void makeItems(String[] items);
public abstract void close();
}
これを見て、
「共通処理を持っているわけではなく、メソッドの定義だけならインターフェースでもよいのではないか」
という疑問が出ました。
実際、インターフェースでもBuilderパターンは実装できます。
public interface Builder {
void makeTitle(String title);
void makeString(String str);
void makeItems(String[] items);
void close();
}
具体クラスは、
public class TextBuilder implements Builder {
// 実装
}
となります。
Director側は変わりません。
private Builder builder;
として扱えます。
なぜ書籍では抽象クラスなのか
Builderパターンそのものは、
Builderは必ず抽象クラスにする
というパターンではありません。
重要なのは、
Director
↓
Builderという抽象
↑
TextBuilder / HTMLBuilder
という関係になっていることです。
DirectorがTextBuilderやHTMLBuilderという具体クラスではなく、Builderという抽象に依存していることが重要です。
その抽象を、
abstract class Builder
で表現することもできますし、
interface Builder
で表現することもできます。
もしBuilderに共通のフィールドや処理を持たせたいなら、抽象クラスにする意味があります。
たとえば、
public abstract class Builder {
protected int itemCount;
protected void countItem() {
itemCount++;
}
public abstract void makeTitle(String title);
}
のようなケースです。
一方、今回の書籍のサンプルのように、Builderがほぼ抽象メソッドだけを持っているのであれば、インターフェースでも実現できます。
つまり、
Builderパターン
≠
抽象クラスを使うパターン
ということです。
Builderパターンを学んで理解できたこと
今回Builderパターンを学んで、特に重要だと感じたのは「Directorが具体的な実装を知らなくてよい」という点でした。
Directorは、
builder.makeTitle();
builder.makeString();
builder.makeItems();
という作業手順だけを知っています。
それを、
TextBuilder
HTMLBuilder
MarkdownBuilder
のどれが実行するかはDirectorにとって重要ではありません。
そのため、新しいBuilderを追加したとしても、Directorの処理を変更せずに済みます。
Director
↓
Builder
↑
┌─────┼──────┐
Text HTML Markdown
この構造によって、
「何をどの順番で作るか」
と、
「具体的にどう作るか」
を分離できます。
Builderパターンそのものを理解するだけでなく、
- 親クラス型の変数に子クラスのインスタンスを代入できる
- 実際のインスタンスに応じてオーバーライドされたメソッドが実行される
- 具体クラスではなく抽象に依存する
- 抽象クラスとインターフェースは目的によって使い分ける
といった、オブジェクト指向の基本的な考え方も改めて整理できました。
デザインパターンを学ぶことで、「このクラス構成を覚える」というよりも、その裏側にあるオブジェクト指向の考え方を理解することの方が重要なのだと感じました。