はじめに
この記事は個人的な勉強メモです。inputしたものはoutputしなくてはという強迫観念に駆られて記事を書いています。
あわよくば詳しい人に誤りの指摘やアドバイスを頂ければいいなという思いを込めてQiitaの記事にしています。
エンジニアとして社会人生活を送っていますが、デザインパターンについてちゃんと学んだことがなかったので勉強してみました。
ここに記載している内容は
https://github.com/ck-fm0211/notes_desigh_pattern
にuploadしています。
過去ログ
デザインパターンについて勉強してみた(個人的メモ)その1
デザインパターンについて勉強してみた(個人的メモ)その2
デザインパターンについて勉強してみた(個人的メモ)その3
デザインパターンについて勉強してみた(個人的メモ)その4
デザインパターンについて勉強してみた(個人的メモ)その5
デザインパターンについて勉強してみた(個人的メモ)その6
デザインパターンについて勉強してみた(個人的メモ)その7
Proxyパターン
- Proxy: 代理人
- 代理人オブジェクトは、本人でなくてもできるような処理を任される。
- 代理人オブジェクトでできない処理は本人オブジェクトが引き受け、処理する。
実際に使ってみる
題材
- 営業としてお客様折衝をしている田中さん
- お客様からいくつか質問を受けたが、答えられないものがあり自社持ち帰り
- 自社で上司の鈴木さんに確認し、改めてお客様へ回答した
- 代理人オブジェクト:田中さん
- 本人オブジェクト:鈴木さん
# -*- coding:utf-8 -*-
from abc import ABCMeta, abstractmethod
class Sales(metaclass=ABCMeta):
"""営業interface"""
def __init__(self):
pass
@staticmethod
@abstractmethod
def question1():
pass
@staticmethod
@abstractmethod
def question2():
pass
@staticmethod
@abstractmethod
def question3():
pass
class Suzuki(Sales):
"""鈴木さんクラス(本人オブジェクト)"""
@staticmethod
def question1():
print("回答1")
@staticmethod
def question2():
print("回答2")
@staticmethod
def question3():
print("回答3")
class Tanaka(Sales):
"""田中さんクラス(代理人オブジェクト)"""
@staticmethod
def question1():
print("それは「回答1」です")
@staticmethod
def question2():
print("それは「回答2」です")
@staticmethod
def question3():
print("それは「")
# 答えられないので鈴木先生に聞く
Suzuki().question3()
print("」になります")
class Client:
"""お客様クラス"""
@staticmethod
def main():
# 質問1
Tanaka().question1()
# 質問2
Tanaka().question2()
# 質問3
Tanaka().question3()
if __name__ == '__main__':
c = Client()
c.main()
それは「回答1」です
それは「回答2」です
それは「
回答3
」になります
Proxyパターンのまとめ
Commandパターン
- あるオブジェクトに対して要求を送るということは、そのオブジェクトのメソッドを呼び出すことと同じ。
- メソッドにどのような引数を渡すか、ということによって要求の内容は表現される。
- さまざまな要求を送ろうとすると、引数の数や種類を増やさなければならない。
- 要求自体をオブジェクトにしてしまい、そのオブジェクトを引数に渡すようにする。
- 要求をCommandオブジェクトにして、それらを複数組み合わせて使えるようにするパターン。
実際に使ってみる
題材
- 理科の授業で、「水100gに食塩は何g溶けるか」という飽和食塩水の実験を行うことにした。手順は以下のとおり。
水100gに食塩を1gずつ加えて飽和食塩水を作る実験
- ビーカーに水を100g入れる
- ビーカーに食塩を1g入れる
- かき混ぜる
- 完全に溶ければ、2に戻る
- 食塩が溶け残ったら、そのときの水量、食塩量、濃度を記録する
- また、「食塩10gをすべて溶かすには水は何g必要か」という実験も行う。手順は以下のとおり。
食塩10gに水を10gずつ加えて飽和食塩水を作る実験
- ビーカーに食塩を10g入れる
- ビーカーに水を10g入れる
- かき混ぜる
- 完全に溶けなければ、2に戻る
- 食塩が完全に溶けたら、そのときの水量、食塩量、濃度を記録する
- 生徒全員に実験方法を記述させるのは大変なので、実験方法が載っている実験セットを用意し、それを生徒に渡し、実験させることにする。
# -*- coding:utf-8 -*-
ADD_SALT = 1 # 食塩を加えて、かき混ぜる場合
ADD_WATER = 2 # 水を加えて、かき混ぜる場合
class Beaker:
"""実験セット"""
def __init__(self, water: float, salt: float):
self._water = water
self._salt = salt
self._melted = False
self.mix()
def mix(self):
"""
溶液をかき混ぜるメソッド
溶けたか溶け残ったかをセットする
常温の飽和食塩水の濃度は約26.4%
"""
if self.get_density() < 26.4:
self._melted = True
else:
self._melted = False
def is_melted(self) -> bool:
return self._melted
def add_salt(self, salt: float):
self._salt += salt
def add_water(self, water: float):
self._water += water
def get_density(self):
return (self._salt/(self._water + self._salt))*100
def note(self):
print(f"水:{self._water}g")
print(f"食塩:{self._salt}g")
print(f"濃度:{self.get_density()}%")
def experiment(self, param: int):
"""実験を行うメソッド"""
if param == ADD_SALT:
# 食塩を1gずつ加えて飽和食塩水を作る実験をする場合
# 完全に溶けている間は食塩を加える
while self.is_melted():
self.add_salt(1) # 食塩を1g入れる
self.mix() # かき混ぜる
print("食塩を1gずつ加える実験")
self.note()
elif param == ADD_WATER:
# 水を10gずつ加えて飽和食塩水を作る実験をする場合
# 溶け残っている間は水を加える
while not self.is_melted():
self.add_water(10) # 水を10g入れる
self.mix() # かき混ぜる
print("水を10gずつ加える実験")
self.note()
class Student:
"""実験する生徒"""
def main(self):
# 水100gに食塩を1gずつ加えて飽和食塩水を作る実験
Beaker(100, 0).experiment(ADD_SALT)
# 食塩10gに水を10gずつ加えて飽和食塩水を作る実験
Beaker(0, 10).experiment(ADD_WATER)
if __name__ == '__main__':
s = Student()
s.main()
- ここで追加の実験(濃度10%の食塩水100gを作る実験)を行おうとすると、実験セットクラスの実験を行うメソッドを修正しなければならない
MAKE_SALT_WATER = 3 #食塩水を作る場合
# ...
class Beaker:
# ...
def experiment(self, param: int):
"""実験を行うメソッド"""
if param == ADD_SALT:
# ...
elif param == ADD_WATER:
# ...
elif param == MAKE_SALT_WATER:
# 食塩水を作る実験
self.mix()
# 濃度を測り、ノートに記述する
print("食塩水を作る実験")
self.note()
# ...
- 実験のパターンを増やすと実験セットクラスにif文を追加することになり、またパラメータも増やさなければならず、拡張性が悪い
- 実験の内容をintで表すことをやめて、実験そのものを1つのCommandオブジェクトで表現するようにする
- 実験内容、つまりCommandオブジェクトに共通のインターフェースを持たせることにより、実験セットクラスは、どんな種類の実験内容(Commandオブジェクト)を受け取っても、共通の実験を行うメソッドを実行すれば良いことになる。
# -*- coding:utf-8 -*-
from abc import ABCMeta, abstractmethod
class Beaker:
"""実験セット"""
def __init__(self, water: float, salt: float):
self._water = water
self._salt = salt
self._melted = False
self.mix()
def mix(self):
"""
溶液をかき混ぜるメソッド
溶けたか溶け残ったかをセットする
常温の飽和食塩水の濃度は約26.4%
"""
if self.get_density() < 26.4:
self._melted = True
else:
self._melted = False
def is_melted(self) -> bool:
return self._melted
def add_salt(self, salt: float):
self._salt += salt
def add_water(self, water: float):
self._water += water
def get_density(self):
return (self._salt/(self._water + self._salt))*100
def note(self):
print(f"水:{self._water}g")
print(f"食塩:{self._salt}g")
print(f"濃度:{self.get_density()}%")
class Command(metaclass=ABCMeta):
"""実験内容を表すクラスの共通インターフェースを提供するスーパークラス"""
def __init__(self):
self._beaker = None
def set_beaker(self, beaker: Beaker):
self._beaker = beaker
def execute(self):
pass
class AddSaltCommand(Command):
"""食塩を1gずつ加える実験のコマンドクラス"""
def execute(self):
while self._beaker.is_melted():
self._beaker.add_salt(1)
self._beaker.mix()
print("食塩を1gずつ加える実験")
self._beaker.note()
class AddWaterCommand(Command):
"""水を10gずつ加える実験のコマンドクラス"""
def execute(self):
while not self._beaker.is_melted():
self._beaker.add_water(10)
self._beaker.mix()
print("水を10gずつ加える実験")
self._beaker.note()
class MakeSaltWaterCommand(Command):
"""食塩水を作る実験のコマンドクラス"""
def execute(self):
self._beaker.mix()
print("食塩水を作る実験")
self._beaker.note()
class Student:
"""実験する生徒"""
def main(self):
add_salt = AddSaltCommand()
add_salt.set_beaker(Beaker(100, 0)) # 水100g入ったビーカーを用意する
add_water = AddWaterCommand()
add_water.set_beaker(Beaker(10, 10)) # 食塩10g入ったビーカーを用意する
make_saltwater = MakeSaltWaterCommand()
make_saltwater.set_beaker(Beaker(90, 10)) # 水90g、食塩10g入ったビーカーを用意する
add_salt.execute() # 水100gに食塩を1gずつ加えて飽和食塩水を作る実験
add_water.execute() # 食塩10gに水を10gずつ加えて飽和食塩水を作る実験
make_saltwater.execute() # 10%の食塩水100gを作る実験
- Commandパターンを適用すると、実験セットのソースコードを変更しなくても、いろいろな実験を追加することができる。
- また、既存の実験内容を組み合わせて、新たな実験を作ることも可能となる。
- 新しい実験内容のexecuteメソッド内に、既存の実験内容のexecuteメソッドを記述すれば、新しい実験内容が実行された際、記述した順に既存の実験内容も実行される。
- 再利用性も高くなる。
Commandパターンのまとめ
Interpreterパターン
- Interpreter: 解釈者・説明者
- 何らかのフォーマットで書かれたファイルの中身を、解析した結果に則って何らかの処理を行いたい場合がある。
- Interpreter パターンとは、このような「解析した結果」得られた手順に則った処理を実現するために最適なパターン。
実際に使ってみる
題材
-
カップラーメンの作り方で考える
-
工程は以下の通り
- カップめんに「粉末スープ」を入れる
- お湯を注ぐ
- 3分待つ
- 液体スープを入れる
-
この構文木から「処理」と「処理対象」を抜き出してみる。
-
「処理」に分類されるものは、「足す」「3分待つ」の2つ。
-
一方「処理対象」に分類されるものは、 「粉末スープ」「麺」「お湯」「液体スープ」だけでなく「粉末スープと麺を足したもの」 「粉末スープと麺を足したものにお湯を足したもの」「粉末スープと麺を足したものにお湯を足したものを3分置いたもの」 なども、処理対象と考えられる。
-
このように、「処理対象」には「処理結果」も含まれるため、この2つを同一視するために、 Interpreter パターンは Composit パターンと同じ構造をとる。
# -*- coding:utf-8 -*-
from abc import ABCMeta, abstractmethod
class Operand(metaclass=ABCMeta):
"""処理対象を表すインタフェース"""
@abstractmethod
def get_operand_string(self):
pass
- 「処理対象」「処理結果」を表すクラスは、このインタフェースを実装する
class Ingredient(Operand):
"""処理対象を表すクラス"""
def __init__(self, operand_string: str):
self._operand_string = operand_string
def get_operand_string(self) -> str:
return self._operand_string
class Expression(Operand):
"""処理結果を表すクラス"""
def __init__(self, operator):
"""処理内容を表す operator を引数に取る"""
self._operand_string = None
self._operator = operator
def get_operand_string(self):
return self._operator.execute().get_operand_string()
- 処理を表すインタフェースと実装クラスは、以下のようになる
class Operator(metaclass=ABCMeta):
"""処理を表すインタフェース"""
@abstractmethod
def execute(self):
pass
class Plus(Operator):
"""足し合わせる処理を表すクラス"""
def __init__(self, operand1: Operand, operand2: Operand):
self._operand1 = operand1
self._operand2 = operand2
def execute(self) -> Operand:
return Ingredient(f"{self._operand1.get_operand_string()}と{self._operand2.get_operand_string()}を足したもの")
class Wait(Operator):
"""「待つ」という処理を表すクラス"""
def __init__(self, minute: int, operand: Operand):
self._minute = minute
self._operand = operand
def execute(self) -> Operand:
return Ingredient(f"{self._operand.get_operand_string()}を{self._minute}分置いたもの")
- 実行するとこんな感じ
if __name__ == '__main__':
# 素材
material1 = Ingredient("麺")
material2 = Ingredient("粉末スープ")
material3 = Ingredient("お湯")
material4 = Ingredient("液体スープ")
# 工程
# 麺と粉末スープを入れる
step1 = Plus(material1, material2).execute()
# お湯を入れる
step2 = Plus(step1, material3).execute()
# 3分待つ
step3 = Wait(3, step2).execute()
# 液体スープを入れる
step4 = Plus(step3, material4).execute()
print(f"{step4.get_operand_string()}:それがカップラーメン!")
麺と粉末スープを足したものとお湯を足したものを3分置いたものと液体スープを足したもの:それがカップラーメン!
- Interpreter パターンでは、ひとつの文法規則をひとつのクラスで表現する。
- サンプルケースでは、「足す」「待つ」といった処理をひとつのクラスで表現しており、 構文の解析結果に合わせて、処理を実行していくことを可能にしている。
Interpreterパターンのまとめ
おわりに
- やっと全部終わった
- 実際の業務で使いこなすにはこれらをかなり意識してやらないとクソコード量産しそう
- 何回も読み返したり違うパターンで書いてみたり練習が必要だということがわかった
- 良いコード が書けるようになるまでの道のりは長い・・・・