検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
当サイトのドメイン名は " getwebtips.net " です。
トップレベルドメインは .net であり、他の .com / .shop といったトップレベルドメインのサイトとは一切関係ありません。
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Builder Pattern - Part. 1 「第5章: ビルダーパターン - 概要」の巻 投稿一覧へ戻る

Published 2022年5月27日20:23 by T.Tsuyoshi

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

Practical Python Design Patterns - The Builder Pattern 編
「第5章: ビルダーパターン - 概要」の巻

Chapter 5: Builder Pattern - Overview
(第5章: ビルダーパターン - 概要)

ゲームにおいてプレイヤーからキャラクターの名前を入力してもらったり、表計算ソフトのセルにデータの入力があったり、と、ソフトウェアではユーザーからの何らかの入力に対処する必要があることがほとんどです。アプリケーションの肝がフォーム (forms) である、なんてことも度々です。実際のところ、アプリケーションは、情報の入力 (input)、変換 (transform)、フィードバック (feedback) といった「データのやり取り / 流れ」に対処しているケースの1つ、と言い切ることさえできるかもしれません。
入力に対応したウィジェット (widgets) やインターフェース (interfaces) には多くの種類がありますが、代表的なものは以下のものでしょう:
Text boxes
Dropdown and Select lists
Radio buttons
File upload fields
Buttons
これらのウィジェットを様々に組み合わせることで、ユーザーから色々なタイプの入力を受け付けることが可能になります。そしてこの章で我々が行うのは、こうした様々なウィジェットから構成されたフォームを容易に作成できるようにするためにはどのようなスクリプトを記述すればよいのか、ということです。例として HTML Web フォームの生成を行いますが、ここで学ぶ手法に少し変更を加えることで、モバイルアプリケーションインターフェース、フォーム構成を模した JSON や XML、その他想像可能なあらゆるものに対応させることができます。
まずは手始めに、簡単なフォームを生成するための関数を記述してみましょう:
basicform_generator.py
def generate_webform(field_list: list[str]) -> str:
generated_fields = '\n'.join(
map(
lambda x: f'{x}:<br><input type="text" name="{x}"><br>',
field_list
)
)
return f'<form>{generated_fields}</from>'

if __name__ == '__main__':
fields = ['name', 'age', 'email', 'telephone']
print(generate_webform(fields))
この非常に単純なスクリプトでは、作成するのはテキストフィールドだけ、と決め打ちをしています。また、渡されたリストの各文字列要素をフィールド名、及び、フィールドのラベルとして利用しており、その数だけ webフォームにフィールドを追加しています。最終的に、webフォームを構成する HTML からなる文字列を返します。
このプログラムをもう一歩進めるとするなら、このスクリプトからの返り値を受け取り、ウェブブラウザで表示可能な通常の HTML ファイルを生成するスクリプトを追加することもできるでしょう:
html_form_generator.py
def generate_webform(field_list: list[str]) -> str:
generated_fields = '\n'.join(
map(
lambda x: f'{x}:<br><input type="text" name="{x}"><br>',
field_list
)
)
return f'<form>{generated_fields}</from>'

def build_html_form(fields):
with open('form_file.html', mode='w', encoding='utf-8') as f:
f.write(f'<html><body>{generate_webform(fields)}</body></html>')

if __name__ == '__main__':
fields = ['name', 'age', 'email', 'telephone']
build_html_form(fields)
この章の最初でも述べたように、webフォームは単純なテキストフィールドだけではなくその他様々なタイプのウィジェットから成り立っています。我々のスクリプトでも名前付き引数 (named parameters) を利用することで様々なフィールドタイプを追加することが可能になります。試しにチェックボックスを追加してみましょう:
html_form_generator.py
def generate_webform(text_field_list: list[str] = [], checkbox_field_list: list[str] = []) -> str:
generated_fields = '\n'.join(
map(
lambda x: f'{x}:<br><input type="text" name="{x}"><br>',
text_field_list
)
)

generated_fields += '\n'.join(
map(
lambda x: f'<input type="checkbox" id="{x}" value="{x}"><label for={x}>{x}</label><br>',
checkbox_field_list
)
)

return f'<form>{generated_fields}</from>'

def build_html_form(text_field_list: list[str] = [], checkbox_field_list: list[str] = []):
with open('form_file.html', mode='w', encoding='utf-8') as f:
f.write(
f"""<html><body>{generate_webform(
text_field_list=text_field_list,
checkbox_field_list=checkbox_field_list
)}</body></html>"""

)

if __name__ == '__main__':
text_fields = ['name', 'age', 'email', 'telephone']
checkbox_fields = ['awesome', 'bad']
build_html_form(text_field_list=text_fields, checkbox_field_list=checkbox_fields)
このアプローチには明らかに欠点がありますね。そのうちの1つは、ラベルやフィールド名以外の情報・属性等のオプションをフィールドに設定したい場合に対処できないことです。また、フォームに表示するラベルとフィールド名を異なるものにすることさえできません。
では、それぞれ異なる属性を持つ多様なフィールドを扱えるように formgenerator 関数の機能を拡張することにしましょう。また、あるフィールドタイプと他のフィールドタイプを混在させられない (交互に表示できない) という問題にも対処しましょう。
私が考えている方法は、現在関数に渡している名前付き引数の代わりに dictionaries を要素とするリストを渡そう、というものです。関数側ではリストに含まれる辞書の情報からフィールドを生成するようにします:
html_dictionary_form_generator.py
def generate_webform(field_dict_list: list[dict[str, str]]) -> str:
generated_field_list: str = []

for field_dict in field_dict_list:
if field_dict['type'] == 'text_field':
generated_field_list.append(
f"""{field_dict['label']}:<br>
<input type='text' name='{field_dict['name']}'><br>"""

)
elif field_dict['type'] == 'checkbox':
generated_field_list.append(
f"""<input type='checkbox'
id='{field_dict['id']}'
value='{field_dict['value']}'>
<label for='{field_dict['id']}'>{field_dict['label']}</label><br>"""

)

generated_fields = '\n'.join(generated_field_list)
return f'<form>{generated_fields}</form>'

def build_html_form(field_list: list[dict[str, str]]):
with open('form_file.html', mode='w', encoding='utf-8') as f:
f.write(
f'<html><body>{generate_webform(field_list)}</body></html>'
)

if __name__ == '__main__':
field_list = [
{
'type': 'text_field',
'label': '今までで最高の文章!',
'name': 'best_text',
},
{
'type': 'checkbox',
'id': 'check_it',
'value': '1',
'label': '1 ならチェック!',
},
{
'type': 'text_field',
'label': 'もう1つテキストフィールド',
'name': 'text_field2',
},
]

build_html_form(field_list)
引数として渡す dictionary には 'type' キーを含んでいて、フォームに追加するフィールドタイプを指定します。それ以外の要素として、'label'、'name' といったそのフィールドに設定したい属性を定義しています。このコードを、想像できる限りの様々なフォームを生成可能な form generator を記述するたたき台とすることができるかもしれません。
しかしこのコードを見て「危険な香り」をちゃんと感じたでしょうか? for ループとその中で折り重なっていく if...elif...else 条件文が、このコードを難読化し、メンテンナスを困難なものにするのは時間の問題です。もう少しこのコードをクリーンにする必要があります。
そこで、各々のフィールド用のタグ文字列を生成するコードを抜き出して別関数として定義し、その関数に dictionary データを渡し、作成した HTML 文字列を返してもらうようにします。この変更の要点は、呼び出し元からの入力、呼び出し元への出力、提供する機能を一切変えずにコードをクリーンアップすることです:
cleaned_html_dictionary_form_generator.py
def generate_webform(field_dict_list: list[dict[str, str]]) -> str:
generated_field_list: str = []

for field_dict in field_dict_list:
if field_dict['type'] == 'text_field':
field_html = generate_text_field(field_dict)
elif field_dict['type'] == 'checkbox':
field_html = generate_checkbox(field_dict)

generated_field_list.append(field_html)

generated_fields = '\n'.join(generated_field_list)
return f'<form>{generated_fields}</form>'

def generate_text_field(field_dict: dict[str, str]) -> str:
return f"""{field_dict['label']}:<br>
<input type='text' name='{field_dict['name']}'><br>"""


def generate_checkbox(field_dict: dict[str, str]) -> str:
return f"""<input type='checkbox'
id='{field_dict['id']}'
value='{field_dict['value']}'>
<label for='{field_dict['id']}'>{field_dict['label']}</label><br>"""


def build_html_form(field_list: list[dict[str, str]]):
with open('form_file.html', mode='w', encoding='utf-8') as f:
f.write(
f'<html><body>{generate_webform(field_list)}</body></html>'
)

if __name__ == '__main__':
field_list = [
{
'type': 'text_field',
'label': '今までで最高の文章!',
'name': 'best_text',
},
{
'type': 'checkbox',
'id': 'check_it',
'value': '1',
'label': '1 ならチェック!',
},
{
'type': 'text_field',
'label': 'もう1つテキストフィールド',
'name': 'text_field2',
},
]

build_html_form(field_list)
if 文はまだ健在ですが、少なくとも少しはスッキリしたのではないでしょうか。しかし、この form generator 関数で新たなフィールドタイプを処理できるようにするたびに if 文は巨大化し続けます。そこで、オブジェクト指向的アプローチを採用して状況の改善を図りたいと思います。ポリモーフィズム (polymorphism: 多態性) の概念を採用することで、我々が現在フィールドの生成時に抱えている問題に対処できるはずです:
oop_html_form_generator.py
class HtmlField:
def __init__(self, **kwargs):
self.html = ""

if kwargs['field_type'] == 'text_field':
self.html = self.construct_text_field(
kwargs['label'],
kwargs['field_name']
)
elif kwargs['field_type'] == 'checkbox':
self.html = self.construct_checkbox(
kwargs['field_id'],
kwargs['value'],
kwargs['label']
)

def construct_text_field(self, label: str, field_name: str) -> str:
return f"""{label}:<br>
<input type='text' name='{field_name}'><br>"""


def construct_checkbox(self, field_id: str, value: str, label: str) -> str:
return f"""<input type='checkbox'
id='{field_id}'
value='{value}'>
<label for='{field_id}'>{label}</label><br>"""


def __str__(self):
return self.html

def generate_webform(field_dict_list: list[dict[str, str]]) -> str:
generated_field_list: str = []

for field in field_dict_list:
try:
generated_field_list.append(str(HtmlField(**field)))
except Exception as e:
print(f'error: {e}')

generated_fields = '\n'.join(generated_field_list)
return f'<form>{generated_fields}</form>'

def build_html_form(field_list: list[dict[str, str]]):
with open('form_file.html', mode='w', encoding='utf-8') as f:
f.write(
f'<html><body>{generate_webform(field_list)}</body></html>'
)

if __name__ == '__main__':
field_list = [
{
'field_type': 'text_field',
'label': '今までで最高の文章!',
'field_name': 'best_text',
},
{
'field_type': 'checkbox',
'field_id': 'check_it',
'value': '1',
'label': '1 ならチェック!',
},
{
'field_type': 'text_field',
'label': 'もう1つテキストフィールド',
'field_name': 'text_field2',
},
]

build_html_form(field_list)
残念ながらこの実装方法でも、新たなフィールドタイプを追加するたびにコンストラクタは肥大を続けます。また、フィールドタイプごとに必要とする属性の種類も数も異なるため、コンストラクタに渡されるパラメータの数も大きく増減することになり、俗に言われる telescoping constructor anti-pattern に陥ることになります。デザインパターン (design patterns) 同様、アンチパターン (anti-patterns) も現実世界におけるソフトウェアデザインや開発プロセスで一般的に目にするものですが、その多くは「欠陥」と見なされるものです。
Java などいくつかのプログラミング言語では、受け取ったパラメータの数や種類に応じてコンストラクタをオーバーロードでき (つまり1つのクラスに複数のコンストラクタを記述可能)、また、あるコンストラクタで (パラメータを処理した上で) 他のコンストラクタを呼び出すことで、多くの状況に対応するようにしています (1つのクラスで多くの異なる状況に対応することからこれも polymorphism です)。
また、__init__() コンストラクタメソッド内の if 文の方が、一般の関数内で定義している同等のものよりも「スッキリ」していることも分かると思います。
もちろん、この実装方法についても多くの反対意見があると思いますし、逆に、改善点が見つけられなければいけません。しかし、更なるコードの「クリーン化」の前に、アンチパターンについて少し取り上げたいと思います。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)