Posted at

デザインパターン(Design Pattern)#Builder

More than 3 years have passed since last update.

設計を意識したコードが書けるようになる為に、デザインパターン修行しました。

他のDesign Patternもちょくちょく出していきます。


前置き

デザインパターンをどういう時に、何を、どう使うのかを理解することが一先ずの目標。

(Javaというか静的型付言語は初めてで、且つpython歴もそんなに長くないので、Pythonistaぽっくないところがあると思います。ご指摘ございましたらご教授ください。)

今回は、生成に関するパターンBuilder。


Builderとは

オブジェクトの生成手順が複雑な場合に、その生成過程をカプセル化し、構造を持ったインスタンスを組み上げていく。

また、色々なパターンのビルダーを用意することで様々なパターンの製造に対応ができ、複雑なメソッドの組み合わせでも、一括して実行ができます。


概要

サンプルとして、Builderパターンを使って「文章」を作成するプログラムを作ります。ここで作る文章は以下のような構造を持っています。


  • タイトルを1つ含む

  • 文字列をいくつか含む

  • 箇条書きの項目をいくつか含む

Builderクラスでは、文書を構成するためのメソッドを定めます。そして、Directorクラスがそのメソッドを使って、具体的な1つの文書を作ります、

Builderクラスは抽象クラスで、実際の処理は書かれていません。抽象メソッドが宣言されているだけです。文章作成のための具体的な処理を決定するのは、Builderクラスのサブクラスです。


  • TextBuilderクラス ・・・ プレーンテキスト(普通の文字列)を使って文書を作る

  • HTMLBuilderクラス ・・・ HTMLを使って文書を作る

DirectorがTextBuilderを使うとプレーンテキストの文書ができ、HTMLBuilderを使うとHTMLの文書ができます。


全体のクラス図


builder.py

from abc import ABCMeta, abstractmethod

class Builder(metaclass=ABCMeta):

@abstractmethod
def make_title(self, title):
pass

@abstractmethod
def make_string(self, string):
pass

@abstractmethod
def make_items(self, items):
pass

@abstractmethod
def close(self):
pass


Builderクラスは、「文書」を作るメソッドたちを宣言している抽象クラスです。make_title、make_string、make_itemsは、それぞれタイトル、文字列、箇条書きを文章中に構築するメソッドです。closeメソッドは文書を完成させるメソッドです。



director.py

class Director():

def __init__(self, builder):
self.__builder = builder

def construct(self):
self.__builder.make_title('Greeting')
self.__builder.make_string('朝から昼にかけて')
string = ['おはようございます。', 'こんにちは。']
self.__builder.make_items(string)
self.__builder.make_string('夜に')
string = ['こんばんは。', 'おやすみなさい。', 'さようなら。']
self.__builder.make_items(string)
self.__builder.close()


Directorクラスでは、Builderクラスで宣言されているメソッドを使って文書を作ります。

Directorクラスのコンストラクタの引数はBuilderクラスのサブクラス(TextBuilderクラスかHTMLBuilder)型が渡ってきます。Builderクラスは抽象クラスなので、インスタンスを作れません。与えられたBuilderクラスのサブクラスの種類によって、Directorクラスが作る具体的な文書形式が定まります。

constructメソッドは、文書を作るメソッドです。このメソッドを呼ぶと、文書が構築されます。constructメソッドが使うのは、Builderで宣言されているメソッドです。



text_builder.py

from builder import Builder

class TextBuilder(Builder):
__buffer = []

def make_title(self, title):
self.__buffer.append('=' * 20)
self.__buffer.append('[' + title + ']\n')
self.__buffer.append('\n')

def make_string(self, string):
self.__buffer.append('■' + string + '\n')
self.__buffer.append('\n')

def make_items(self, items):
for i in range(0, len(items)):
self.__buffer.append('●' + items[i] + '\n')
self.__buffer.append('\n')

def close(self):
self.__buffer.append('=' * 20)

def get_result(self):
for buffer in self.__buffer:
print(buffer)


TextBuilderクラスは、Builderクラスのサブクラスです。プレーンテキストを使って文書を構築します。結果はStringとして返します。



html_builder.py

import logging

from builder import Builder

class HTMLBuilder(Builder):

def make_title(self, title):
self.__filename = title + '.html'

try:
self.__writer = open(self.__filename, mode='w')
except IOError as e:
logging.exception(e)

self.__writer.write('<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>' + title + '</title></head><body>')
self.__writer.write('<h1>' + title + '<h1>')

def make_string(self, string):
self.__writer.write('<h1>' + string + '</p>')

def make_items(self, items):
self.__writer.write('<ul>')
for i in range(0, len(items)):
self.__writer.write('<li>' + items[i] + '</li>')
self.__writer.write('</ul>')

def close(self):
self.__writer.write('</body></html>')
self.__writer.close()

def get_result(self):
return self.__filename


HTMLBuilderクラスも、Builderクラスのサブクラスです。HTMLBuilderクラスはHTMLファイルとして文書を構築します。構築した結果はHTMLファイルのファイル名として返します。



main.py

import sys

from text_builder import TextBuilder
from html_builder import HTMLBuilder
from director import Director

def usage():
print('Usage: Python main plain')
print('Usage: Python main html')

def main():
if len(sys.argv) != 2:
usage()
sys.exit()

if sys.argv[1] == 'plain':
text_builder = TextBuilder()
director = Director(text_builder)
director.construct()
result = text_builder.get_result()
print(result)
elif sys.argv[1] == 'html':
html_builder = HTMLBuilder()
director = Director(html_builder)
director.construct()
filename = html_builder.get_result()
print(filename + 'が作成されました。')
else:
usage()
sys.exit()

if __name__ == "__main__":
main()


mainファイルは、Builderパターンのテストプログラムです。以下のように、コマンドラインで指定した形式に応じた文書を作ります。

python main.py plain ・・・ プレーンテキストで文書を作成

python main.py html ・・・ HTMLファイルで文書作成

コマンドラインでplainを指定した場合には、TextBuilderクラスのインスタンスをDirectorクラスのコンストラクタに渡します。また、コマンドラインでhtmlを指定した場合には、HTMLBuilderクラスのインスタンスをDirectorクラスのコンストラクタに渡します。

TextBuilderもHTMLBuilderもBuilderのサブクラスであり、DirectorはBuilderのメソッドのみを使って文書を作ります。Builderのメソッドのみを使うということは、Directorは、実際に動いているのがTextBuilderなのかHTMLBuilderなのかを意識していないことになります。

なので、Builderは、文書を構築するという目的を達成するのに必要かつ十分なメソッド群を宣言している必要があります。ただし、プレーンテキストやHTMLファイルに固有のメソッドまでをBuilderが提供してはいけません。

実行結果(plain)

====================

[Greeting]

■朝から昼にかけて

●おはようございます。

●こんにちは。

■夜に

●こんばんは。

●おやすみなさい。

●さようなら。
====================

実行結果(html)

Greeting.htmlが作成されました。

Greeting.png


まとめ

インスタンスの生成手順や生成内容が複雑な場合に、インスタンス生成作業を軽減するのに特化したBuilderパターン。

手順に関して言うと、Template MethodパターンとBuilderパターンの違いは、インスタンス生成の役割を誰が負うのかというところ。Template Methodパターンは、インスタンス生成の手順をスーパークラスで決めます。一方Builderパターンは、生成の手順はDirectorクラス(他のクラス)が責任を負います。

Builderパターンの重要なのは、誰がインスタンスの生成手順を知っているのか、つまりDirectorクラスが知っているということです。

また、Directorを使うユーザは、Builderで作られたインスタンスが何かということを知っていなければならないということも重要です。

インスタンス生成に手順がある場合、外部リソースを使ってインスタンスを作成しなければいけない場合、コンストラクタで引数の違うものがたくさんできてしまった場合にBuilderパターンの出番のようです。


参考