Builder
何らかの構造を持った成果物を組み立てる際
概要
人間がHTMLで文章を書くとき、 HTMLのシンタックスをいちいち組み立てるのではなく、コンテンツ(とタグの種類)を与えたら、正しいシンタックスのタグを自動生成してほしいという考え。 そのタグの自動生成はBuilderクラスにやらせる。
人間はBuilderの具体的な挙動を知らなくても良い。
マンガでわかる Builder
マンガでわかる Builder #デザインパターン - Qiita
でざぱたんで覚える Builder
ちびキャラは「ビルダーたん」。「切断面(インターフェース)さえ合っていれば何でも付け替えられる」を信条に、対象ごとに特化した組替え技術を極めた狂気の天才ナース。作り方を知る者(ConcreteBuilder)と何を作るか指示する者(Director)を分ければ、同じ部品群から無限の完成品が組み上がる、という生成者×指示者の掛け算構造を体現する。この掛け算が最初の設計から出てきたら見直すべき、という釘も刺されている。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.023〜)
登場人物
- Builder: インスタンス生成のためのAPIを定める抽象クラス
- ConcreteBuilder: BuilderのAPIを実装し、具体的なものを作る
- Director: Builderインタフェースを使ってインスタンスを生成する
- ConcreteBuilderの種類に関係なく使えるように、Builderメソッドのみ呼ぶ
- Client: 利用者。Main
やり方
- Builderにインスタンス生成のための抽象メソッドを定義する
- DirectorはBuilder型のプロパティを持ち、Builder型のメソッドを呼ぶ
- MainはConcreteBuilderとDirectorを生成し、Directorに「作れ」と一言言う。
- DirectorはBuilderメソッドを通じてConcreteBuilderから成果物を作る
クラス図
このサイトの実装(あいさつ文書ビルダー)での対応関係:
classDiagram
class Builder {
<<abstract>>
+makeTitle(title)
+makeString(str)
+makeItems(items)
+close()
}
class TextBuilder {
+getResult() String
}
class HTMLBuilder {
+getResult() String
}
class Director {
+construct()
}
Builder <|-- TextBuilder
Builder <|-- HTMLBuilder
Director o-- Builder
メリット(用途)
「交換可能性」: 入れ替えられるからこそ部品としての信頼性が高くなる
- ClientはBuilderのメソッドは知らない。ClientがDirectorにお願いするとDirectorのなかでむにゃむにゃ仕事が進んでいい感じの成果物ができている
- DirectorはConcreteBuilderの実装を知らない。知らないのでHTMLBuilderだろうがMarkdownBuilderだろうが問題なく動かせる。
- 近い将来扱う書式が増えそうなとき有効なパターン
応用例
CSV, XML形式を作り分ける
- ディレクタは属性と値だけ指定する
- ビルダがデリミターやタグなどに従って適切に配置する
SQLを組み立てる
- SELECT, UPDATE, INSERTなど組み立てたいクエリの見出し
- テーブル名
- 属性
- 条件
などを指定すると、ビルダが勝手にSQLを組み立ててくれる
Java
public class TextBuilder extends Builder {
private StringBuffer buffer = new StringBuffer(); // このフィールドに文書を構築していく
public void makeTitle(String title) { // プレーンテキストでのタイトル
buffer.append("==============================\n"); // 飾り線
buffer.append("『" + title + "』\n"); // 『』つきのタイトル
buffer.append("\n"); // 空行
}
public void makeString(String str) { // プレーンテキストでの文字列
buffer.append('■' + str + "\n"); // ■つきの文字列
buffer.append("\n"); // 空行
}
public void makeItems(String[] items) { // プレーンテキストでの箇条書き
for (int i = 0; i < items.length; i++) {
buffer.append(" ・" + items[i] + "\n"); // ・つきの項目
}
buffer.append("\n"); // 空行
}
public void close() { // 文書の完成
buffer.append("==============================\n"); // 飾り線
}
public String getResult() { // 完成した文書
return buffer.toString(); // StringBufferをStringに変換
}
}
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();
}
import java.io.*;
public class HTMLBuilder extends Builder {
private String filename; // 作成するファイル名
private PrintWriter writer; // ファイルに書き込むPrintWriter
public void makeTitle(String title) { // HTMLファイルでのタイトル
filename = title + ".html"; // タイトルを元にファイル名決定
try {
writer = new PrintWriter(new FileWriter(filename)); // PrintWriterを作る
} catch (IOException e) {
e.printStackTrace();
}
writer.println("<html><head><title>" + title + "</title></head><body>"); // タイトルを出力
writer.println("<h1>" + title + "</h1>");
}
public void makeString(String str) { // HTMLファイルでの文字列
writer.println("<p>" + str + "</p>"); // <p>タグで出力
}
public void makeItems(String[] items) { // HTMLファイルでの箇条書き
writer.println("<ul>"); // <ul>と<li>で出力
for (int i = 0; i < items.length; i++) {
writer.println("<li>" + items[i] + "</li>");
}
writer.println("</ul>");
}
public void close() { // 文書の完成
writer.println("</body></html>"); // タグを閉じる
writer.close(); // ファイルをクローズ
}
public String getResult() { // 完成した文書
return filename; // ファイル名を返す
}
}
public class Director {
private Builder builder;
public Director(Builder builder) { // Builderのサブクラスのインスタンスが与えられるので、
this.builder = builder; // builderフィールドに保持しておく。
}
public void construct() { // 文書構築
builder.makeTitle("Greeting"); // タイトル
builder.makeString("朝から昼にかけて"); // 文字列
builder.makeItems(new String[]{ // 箇条書き
"おはようございます。",
"こんにちは。",
});
builder.makeString("夜に"); // 別の文字列
builder.makeItems(new String[]{ // 別の箇条書き
"こんばんは。",
"おやすみなさい。",
"さようなら。",
});
builder.close(); // 文書を完成させる
}
}
public class Main {
public static void main(String[] args) {
if (args.length != 1) {
usage();
System.exit(0);
}
if (args[0].equals("plain")) {
TextBuilder textbuilder = new TextBuilder();
Director director = new Director(textbuilder);
director.construct();
String result = textbuilder.getResult();
System.out.println(result);
} else if (args[0].equals("html")) {
HTMLBuilder htmlbuilder = new HTMLBuilder();
Director director = new Director(htmlbuilder);
director.construct();
String filename = htmlbuilder.getResult();
System.out.println(filename + "が作成されました。");
} else {
usage();
System.exit(0);
}
}
public static void usage() {
System.out.println("Usage: java Main plain プレーンテキストで文書作成");
System.out.println("Usage: java Main html HTMLファイルで文書作成");
}
}
Go
Director(組み立て手順)は共通のまま、渡す ConcreteBuilder を差し替えるとテキスト/HTMLへ表現が変わる。GetResult は戻り値の意味が ConcreteBuilder ごとに違うので Builder interface には載せず、具体型にだけ持たせる(Java版と同じ判断)。
実行: go run ./GoF/patterns/Builder/go [plain|html]
package main
// Builder は文書の各部品を作る「手順」を定めるインタフェース。
// 何を作るか(タイトル・文字列・箇条書き)だけを規定し、
// どう表現するか(テキスト or HTML)は ConcreteBuilder に任せる。
type Builder interface {
MakeTitle(title string)
MakeString(str string)
MakeItems(items []string)
Close()
}
// Director は Builder を使って文書を「組み立てる」役。
// どの ConcreteBuilder を渡されても、組み立て手順(Construct)は一切変わらない。
type Director struct {
builder Builder
}
func NewDirector(builder Builder) *Director {
return &Director{builder: builder}
}
// Construct は組み立て手順そのもの。Builder interface のメソッドしか呼ばない。
func (d *Director) Construct() {
d.builder.MakeTitle("Greeting")
d.builder.MakeString("朝から昼にかけて")
d.builder.MakeItems([]string{
"おはようございます。",
"こんにちは。",
})
d.builder.MakeString("夜に")
d.builder.MakeItems([]string{
"こんばんは。",
"おやすみなさい。",
"さようなら。",
})
d.builder.Close()
}
package main
import (
"fmt"
"strings"
)
// TextBuilder はプレーンテキストで文書を組み立てる ConcreteBuilder。
type TextBuilder struct {
buffer strings.Builder
}
func (b *TextBuilder) MakeTitle(title string) {
b.buffer.WriteString("==============================\n")
fmt.Fprintf(&b.buffer, "『%s』\n\n", title)
}
func (b *TextBuilder) MakeString(str string) {
fmt.Fprintf(&b.buffer, "■%s\n\n", str)
}
func (b *TextBuilder) MakeItems(items []string) {
for _, item := range items {
fmt.Fprintf(&b.buffer, " ・%s\n", item)
}
b.buffer.WriteString("\n")
}
func (b *TextBuilder) Close() {
b.buffer.WriteString("==============================\n")
}
// GetResult は Builder interface には載せない。
// ConcreteBuilder ごとに戻り値の意味が違う(テキスト本文 / HTML本文)ため、
// 呼び出し側は具体型を知った上で取り出す。
func (b *TextBuilder) GetResult() string {
return b.buffer.String()
}
package main
import (
"fmt"
"strings"
)
// HTMLBuilder は HTML で文書を組み立てる ConcreteBuilder。
// Java版はファイルへ書き出して getResult() でファイル名を返すが、
// ここでは runnable にするためバッファへ組み立て、GetResult() で HTML 本文を返す。
type HTMLBuilder struct {
buffer strings.Builder
}
func (b *HTMLBuilder) MakeTitle(title string) {
fmt.Fprintf(&b.buffer, "<html><head><title>%s</title></head><body>\n", title)
fmt.Fprintf(&b.buffer, "<h1>%s</h1>\n", title)
}
func (b *HTMLBuilder) MakeString(str string) {
fmt.Fprintf(&b.buffer, "<p>%s</p>\n", str)
}
func (b *HTMLBuilder) MakeItems(items []string) {
b.buffer.WriteString("<ul>\n")
for _, item := range items {
fmt.Fprintf(&b.buffer, "<li>%s</li>\n", item)
}
b.buffer.WriteString("</ul>\n")
}
func (b *HTMLBuilder) Close() {
b.buffer.WriteString("</body></html>\n")
}
func (b *HTMLBuilder) GetResult() string {
return b.buffer.String()
}
package main
import (
"fmt"
"os"
)
// 実行:
//
// go run ./GoF/patterns/Builder/go plain # プレーンテキストで文書生成
// go run ./GoF/patterns/Builder/go html # HTMLで文書生成
//
// Director(組み立て手順)は共通のまま、渡す ConcreteBuilder を変えるだけで
// 出力の表現がまるごと変わる。
func main() {
if len(os.Args) != 2 {
usage()
os.Exit(1)
}
switch os.Args[1] {
case "plain":
builder := &TextBuilder{}
NewDirector(builder).Construct()
fmt.Print(builder.GetResult())
case "html":
builder := &HTMLBuilder{}
NewDirector(builder).Construct()
fmt.Print(builder.GetResult())
default:
usage()
os.Exit(1)
}
}
func usage() {
fmt.Fprintln(os.Stderr, "Usage: go run ./GoF/patterns/Builder/go plain プレーンテキストで文書作成")
fmt.Fprintln(os.Stderr, " go run ./GoF/patterns/Builder/go html HTMLで文書作成")
}
PHP
<?php
/**
* 複数のディレクタと複数のビルダ
* 多種多様なことを実現する
*
* 比較的業務に落とし込みやすいが、
* 最初からBuilderにすることを見越すのは難しい
* リファクタリング時に構造を見極めるのが現実的
*
* ビルダの構造
* ディレクタ:ビルダを使う機能を持つ その上で中身を作る
* ビルダ:インスタンスを生成、目的に応じたプロパティを付与
* ビルディング:生成物
*
*/
// Builder
interface DocumentBuilder
{
public function setTitle($str);
public function setHeader($str);
public function setBody($str);
public function setFooter($str);
public function getResult();
}
// Director 中身を作る
// レポート書きたい人
class ReportDirector
{
private $builder;
public function __construct(DocumentBuilder $builder)
{
$this->builder = $builder;
}
public function build()
{
$this->builder->setTitle("報告書");
$this->builder->setHeader("失敗");
$this->builder->setBody("プロトタイプを無駄にする");
$this->builder->setFooter("次頑張りましょう");
return $this->builder->getResult();
}
}
// director2
class DiaryDirector
{
private $builder;
public function __construct(DocumentBuilder $builder)
{
$this->builder = $builder;
}
public function build()
{
$this->builder->setTitle("日記2020/03/12");
$this->builder->setHeader("いい日だった");
$this->builder->setBody("プロトタイプを多数生成");
$this->builder->setFooter("明日も頑張る");
return $this->builder->getResult();
}
}
// builder1 雛形を作る
class HtmlBuilder implements DocumentBuilder
{
private $header = null;
private $body = null;
private $footer = null;
private $title = null;
public function getBody()
{
return $this->body;
}
public function getFooter()
{
return $this->footer;
}
public function getHeader()
{
return $this->header;
}
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
public function setHeader($header)
{
$this->header = $header;
}
public function setBody($body)
{
$this->body = $body;
}
public function setFooter($footer)
{
$this->footer = $footer;
}
public function getResult()
{
ob_start();
?>
<html>
<head>
<title><?= htmlspecialchars($this->title, ENT_QUOTES) ?></title>
</head>
<body>
<h1><?= htmlspecialchars($this->header, ENT_QUOTES) ?></h1>
<div>
<p>
<?= htmlspecialchars($this->body) ?>
</p>
</div>
<hr>
<div><?= htmlspecialchars($this->footer, ENT_QUOTES) ?></div>
</body>
</html>
<?php return ob_get_clean();
}
}
// builder 2 雛形を作る
class MarkdownBuilder implements DocumentBuilder
{
private $header = null;
private $body = null;
private $footer = null;
private $title = null;
public function getBody()
{
return $this->body;
}
public function getFooter()
{
return $this->footer;
}
public function getHeader()
{
return $this->header;
}
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
public function setHeader($header)
{
$this->header = $header;
}
public function setBody($body)
{
$this->body = $body;
}
public function setFooter($footer)
{
$this->footer = $footer;
}
public function getResult()
{
ob_start();
?>
# <?= htmlspecialchars($this->title, ENT_QUOTES) ?>
## <?= htmlspecialchars($this->header, ENT_QUOTES) ?>
<?= htmlspecialchars($this->body, ENT_QUOTES) ?>
------------------------------------------
<?= htmlspecialchars($this->footer, ENT_QUOTES) ?>
<?php return ob_get_clean();
}
}
// 雛形を作るやつをディレクタはプロパティに持つ
$hr = new ReportDirector(new HtmlBuilder());
$mr = new ReportDirector(new MarkdownBuilder());
print($hr->build());
print($mr->build());
// 日記を生成
$hd = new DiaryDirector(new HtmlBuilder());
$md = new DiaryDirector(new MarkdownBuilder());
echo $hd->build();//directorがbuildするのではなくてプロパティのbuilderが手順に従ってbuildしてくれる
echo $md->build();
TypeScript
Go版と同じ構成。Builder は interface で表現し、GetResult(TSではgetResult)は具体型ごとに戻り値の型が違うので、やはりinterfaceには載せない。
実行: npx tsx GoF/patterns/Builder/typescript/main.ts [plain|html]
// Builder: 文書の各部品を作る「手順」を定めるインタフェース。
// TypeScriptのinterfaceはJavaのinterfaceとほぼ同じ感覚で書ける
// (Strategy版TSでの記法を踏襲。Go版のような暗黙実装ではなく、
// ConcreteBuilder側で`implements`を明示する)。
// 何を作るか(タイトル・文字列・箇条書き)だけを規定し、
// どう表現するか(テキスト or HTML)はConcreteBuilderに任せる。
export interface Builder {
makeTitle(title: string): void;
makeString(str: string): void;
makeItems(items: string[]): void;
close(): void;
}
// Director: Builderを使って文書を「組み立てる」役。
// どのConcreteBuilderを渡されても、組み立て手順(construct)は一切変わらない。
export class Director {
constructor(private readonly builder: Builder) {}
// construct は組み立て手順そのもの。Builder interfaceのメソッドしか呼ばない。
construct(): void {
this.builder.makeTitle("Greeting");
this.builder.makeString("朝から昼にかけて");
this.builder.makeItems(["おはようございます。", "こんにちは。"]);
this.builder.makeString("夜に");
this.builder.makeItems(["こんばんは。", "おやすみなさい。", "さようなら。"]);
this.builder.close();
}
}
import type { Builder } from "./builder";
// TextBuilder: プレーンテキストで文書を組み立てるConcreteBuilder。
export class TextBuilder implements Builder {
private buffer = "";
makeTitle(title: string): void {
this.buffer += "==============================\n";
this.buffer += `『${title}』\n`;
this.buffer += "\n";
}
makeString(str: string): void {
this.buffer += `■${str}\n`;
this.buffer += "\n";
}
makeItems(items: string[]): void {
for (const item of items) {
this.buffer += ` ・${item}\n`;
}
this.buffer += "\n";
}
close(): void {
this.buffer += "==============================\n";
}
// getResultはBuilder interfaceには載せない。ConcreteBuilderごとに戻り値の意味が
// 違う(テキスト本文 / HTML本文)ため、呼び出し側は具体型を知った上で取り出す
// (Go版のGetResultと同じ設計)。
getResult(): string {
return this.buffer;
}
}
import type { Builder } from "./builder";
// HTMLBuilder: HTMLで文書を組み立てるConcreteBuilder。
// Java版はファイルへ書き出してgetResult()でファイル名を返すが、
// Go版に倣ってrunnableにするためバッファへ組み立て、getResult()でHTML本文を返す。
export class HTMLBuilder implements Builder {
private buffer = "";
makeTitle(title: string): void {
this.buffer += `<html><head><title>${title}</title></head><body>\n`;
this.buffer += `<h1>${title}</h1>\n`;
}
makeString(str: string): void {
this.buffer += `<p>${str}</p>\n`;
}
makeItems(items: string[]): void {
this.buffer += "<ul>\n";
for (const item of items) {
this.buffer += `<li>${item}</li>\n`;
}
this.buffer += "</ul>\n";
}
close(): void {
this.buffer += "</body></html>\n";
}
getResult(): string {
return this.buffer;
}
}
// Builder パターン: 文書構築 (Java版と同じ題材)
//
// 実行:
// npx tsx main.ts plain # プレーンテキストで文書生成
// npx tsx main.ts html # HTMLで文書生成
//
// Director(組み立て手順)は共通のまま、渡すConcreteBuilderを変えるだけで
// 出力の表現がまるごと変わる (Go版main.goと同じ構成)。
import { Director } from "./builder";
import { TextBuilder } from "./text_builder";
import { HTMLBuilder } from "./html_builder";
function usage(): void {
console.error("Usage: npx tsx main.ts plain プレーンテキストで文書作成");
console.error("Usage: npx tsx main.ts html HTMLで文書作成");
}
function main(): void {
const args = process.argv.slice(2);
if (args.length !== 1) {
usage();
process.exit(1);
}
if (args[0] === "plain") {
const textBuilder = new TextBuilder();
new Director(textBuilder).construct();
console.log(textBuilder.getResult());
} else if (args[0] === "html") {
const htmlBuilder = new HTMLBuilder();
new Director(htmlBuilder).construct();
console.log(htmlBuilder.getResult());
} else {
usage();
process.exit(1);
}
}
main();
Python
Builder は abc.ABC で表現。HTMLBuilderはJava版のようにファイルへ書き出さず、Go/TS版と同じくバッファに組み立てて返す。
実行: python3 GoF/patterns/Builder/python/main.py [plain|html]
"""Builder: 文書の各部品を作る「手順」を定める。
PythonにはJavaのinterfaceに相当する言語機能はないため、Strategy版と同じく
抽象基底クラス(ABC, abcモジュール)で表す。Goのような暗黙実装ではなく、
Java同様に継承(Builderを継承)して満たす必要がある。
何を作るか(タイトル・文字列・箇条書き)だけを規定し、
どう表現するか(テキスト or HTML)はConcreteBuilderに任せる。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
class Builder(ABC):
@abstractmethod
def make_title(self, title: str) -> None: ...
@abstractmethod
def make_string(self, str_: str) -> None: ...
@abstractmethod
def make_items(self, items: list[str]) -> None: ...
@abstractmethod
def close(self) -> None: ...
class Director:
"""Builderを使って文書を「組み立てる」役。
どのConcreteBuilderを渡されても、組み立て手順(construct)は一切変わらない。
"""
def __init__(self, builder: Builder) -> None:
self._builder = builder
def construct(self) -> None:
"""construct は組み立て手順そのもの。Builderのメソッドしか呼ばない。"""
self._builder.make_title("Greeting")
self._builder.make_string("朝から昼にかけて")
self._builder.make_items(["おはようございます。", "こんにちは。"])
self._builder.make_string("夜に")
self._builder.make_items(["こんばんは。", "おやすみなさい。", "さようなら。"])
self._builder.close()
"""TextBuilder: プレーンテキストで文書を組み立てるConcreteBuilder。"""
from __future__ import annotations
from builder import Builder
class TextBuilder(Builder):
def __init__(self) -> None:
self._buffer = ""
def make_title(self, title: str) -> None:
self._buffer += "==============================\n"
self._buffer += f"『{title}』\n"
self._buffer += "\n"
def make_string(self, str_: str) -> None:
self._buffer += f"■{str_}\n"
self._buffer += "\n"
def make_items(self, items: list[str]) -> None:
for item in items:
self._buffer += f" ・{item}\n"
self._buffer += "\n"
def close(self) -> None:
self._buffer += "==============================\n"
def get_result(self) -> str:
"""get_resultはBuilderの抽象メソッドには含めない。ConcreteBuilderごとに
戻り値の意味が違う(テキスト本文 / HTML本文)ため、呼び出し側は具体型を
知った上で取り出す(Go/TS版と同じ設計)。
"""
return self._buffer
"""HTMLBuilder: HTMLで文書を組み立てるConcreteBuilder。
Java版はファイルへ書き出してget_result()でファイル名を返すが、Go/TS版に倣って
runnableにするためバッファへ組み立て、get_result()でHTML本文を返す。
"""
from __future__ import annotations
from builder import Builder
class HTMLBuilder(Builder):
def __init__(self) -> None:
self._buffer = ""
def make_title(self, title: str) -> None:
self._buffer += f"<html><head><title>{title}</title></head><body>\n"
self._buffer += f"<h1>{title}</h1>\n"
def make_string(self, str_: str) -> None:
self._buffer += f"<p>{str_}</p>\n"
def make_items(self, items: list[str]) -> None:
self._buffer += "<ul>\n"
for item in items:
self._buffer += f"<li>{item}</li>\n"
self._buffer += "</ul>\n"
def close(self) -> None:
self._buffer += "</body></html>\n"
def get_result(self) -> str:
return self._buffer
"""Builder パターン: 文書構築 (Java版と同じ題材)
実行:
python3 main.py plain # プレーンテキストで文書生成
python3 main.py html # HTMLで文書生成
Director(組み立て手順)は共通のまま、渡すConcreteBuilderを変えるだけで
出力の表現がまるごと変わる (Go/TS版main.goと同じ構成)。
"""
from __future__ import annotations
import sys
from builder import Director
from html_builder import HTMLBuilder
from text_builder import TextBuilder
def usage() -> None:
print("Usage: python3 main.py plain プレーンテキストで文書作成", file=sys.stderr)
print("Usage: python3 main.py html HTMLで文書作成", file=sys.stderr)
def main() -> None:
if len(sys.argv) != 2:
usage()
sys.exit(1)
if sys.argv[1] == "plain":
text_builder = TextBuilder()
Director(text_builder).construct()
print(text_builder.get_result())
elif sys.argv[1] == "html":
html_builder = HTMLBuilder()
Director(html_builder).construct()
print(html_builder.get_result())
else:
usage()
sys.exit(1)
if __name__ == "__main__":
main()