はじめに
この記事は個人的な勉強メモです。inputしたものはoutputしなくてはという強迫観念に駆られて記事を書いています。
あわよくば詳しい人に誤りの指摘やアドバイスを頂ければいいなという思いを込めてQiitaの記事にしています。
エンジニアとして社会人生活を送っていますが、デザインパターンについてちゃんと学んだことがなかったので勉強してみました。
ここに記載している内容は
https://github.com/ck-fm0211/notes_desigh_pattern
にuploadしています。
過去ログ
デザインパターンについて勉強してみた(個人的メモ)その1
デザインパターンについて勉強してみた(個人的メモ)その2
デザインパターンについて勉強してみた(個人的メモ)その3
デザインパターンについて勉強してみた(個人的メモ)その4
Compositeパターン
- Composite パターンは、「容器と中身を同一視する」ことで、再帰的な構造の取り扱いを容易にするもの
- 例:ファイルシステム
- あるフォルダ以下のファイルやフォルダをすべて削除したい場合など、それがファイルなのかフォルダなのかを意識せずに、同じように削除できたほうが都合が良い
実際に使ってみる
題材
- サンプルケースでは、ディレクトリとファイルを考える。
- Composite パターンを意識せずに、ファイルとディレクトリを表すクラスを作成してみる。
class File:
def __init__(self, name):
self._name = name
def remove(self):
print("{}を削除しました".format(self._name))
- ディレクトリを表す、Directory クラスは、List オブジェクトとして、配下のディレクトリとファイルのオブジェクトを管理し、remove メソッドが呼ばれた場合には、list に保持しているオブジェクトをすべて削除してから、自らを削除するものとする
class Directory:
def __init__(self, name):
self._name = name
self._list = []
def add(self, arg):
self._list.append(arg)
def remove(self):
itr = iter(self._list)
i = 0
while next(itr, None) is not None:
obj = self._list[i]
if isinstance(obj, File):
obj.remove()
elif isinstance(obj, Directory):
obj.remove()
else:
print("削除できません")
i += 1
print("{}を削除しました".format(self._name))
if __name__ == "__main__":
file1 = File("file1")
file2 = File("file2")
file3 = File("file3")
file4 = File("file4")
dir1 = Directory("dir1")
dir1.add(file1)
dir2 = Directory("dir2")
dir2.add(file2)
dir2.add(file3)
dir1.add(dir2)
dir1.add(file4)
dir1.remove()
- ここまでは問題ない。ここに「ディレクトリには、ディレクトリとファイルだけでなくシンボリックリンクも入るようにしたい」という要求が出てくると面倒になる
- Composite パターンでは、容器の中身と入れ物を同一視する。同一視するために、容器と中身が共通のインタフェースを実装するようにする。
- File と Directory が共通のインタフェース DirectoryEntry を実装するようにする
class DirectoryEntry(metaclass=ABCMeta):
@abstractmethod
def remove(self):
pass
- DirectoryEntry インタフェースでは、remove メソッドのみを定義する
- これを実装する形でFileクラス、Directoryクラスを実装する。
class File(DirectoryEntry):
def __init__(self, name):
self._name = name
def remove(self):
print("{}を削除しました".format(self._name))
class Directory(DirectoryEntry):
def __init__(self, name):
self._name = name
self._list = []
def add(self, entry: DirectoryEntry):
self._list.append(entry)
def remove(self):
itr = iter(self._list)
i = 0
while next(itr, None) is not None:
obj = self._list[i]
obj.remove()
i += 1
print("{}を削除しました".format(self._name))
- Directory クラス、File クラスを共に DirectoryEntry クラスを実装するクラスとすることで、 Directory クラスの remove メソッド内では、実態が File クラスのインスタンスであるのか、Directory クラスのインスタンスであるのかを気にせず、どちらも DirectoryEntry オブジェクトとして扱うことができるようになっている。
- このように Composite パターンを利用していることで、SymbolicLink クラスを追加する必要が生じた場合も、 柔軟に対応できる。
- DirectoryEntry インタフェースを 実装するように、SymbolicLink クラスを実装すればよい。
class SymbolicLink(DirectoryEntry):
def __init__(self, name):
self._name = name
def remove(self):
print("{}を削除しました".format(self._name))
Compositeパターンのまとめ
Decoratorパターン
- Decorator パターンでは、飾り枠と中身を同一視することで、より柔軟な機能拡張方法を提供する。
- Decoratorパターンは機能を一つひとつかぶせていくイメージ。ある機能を持ったDecorationをコアとなるものにかぶせていくイメージである。
実際に使ってみる
題材
- アイスクリーム屋では、自由にトッピングを選べるようになっている。客は、トッピングしなくても良いし、複数のトッピングを重ねて選択することもできる。
- アイスクリーム共通のインタフェースとして、以下のインタフェースを定義する。
class Icecream(metaclass=ABCMeta):
@abstractmethod
def get_name(self):
pass
@abstractmethod
def how_sweet(self):
pass
- これらのインタフェースを持つクラスとしては、バニラアイスクリームクラス、抹茶アイスクリームクラスなどが以下のように提供されている。
class VanillaIcecream(Icecream):
def get_name(self):
return "バニラアイスクリーム"
def how_sweet(self):
return "バニラ味"
class GreenTeaIcecream(Icecream):
def get_name(self):
return "抹茶アイスクリーム"
def how_sweet(self):
return "抹茶味"
- これらのアイスクリームインタフェース実装クラスにトッピングをしていくことを考える。
- トッピングとしては、カシューナッツ、スライスアーモンドを考えてみる。
- カシューナッツがトッピングされたバニラアイスクリームや、スライスアーモンドがトッピングされたバニラアイスクリームが要求される。
- ここでは、トッピングを乗せることで、名前(getName メソッドの返り値)が変わり、味(howSweet() メソッドの返り値) は変わらないことにする。
- このような要求を満たすために、カシューナッツがトッピングされたバニラアイスクリームを表現するために、カシューナッツバニラアイスクリームクラスを作成する方法が考えられる。
class CashewNutsVanillaIcecream(Icecream):
def get_name(self):
return "カシューナッツバニラアイスクリーム"
- このような「継承を利用した機能の追加」は、非常に固定的なものとなってしまう。
- 例えば、カシューナッツを乗せた抹茶アイスクリームを表すインスタンスが欲しい場合は、抹茶アイスクリーム継承クラスが必要となる。
- Decorator パターンは、このように、様々な機能追加を柔軟に行いたい場合に威力を発揮する。
- Decorator パターンを利用した設計では、拡張機能部分のみを持たせた別クラスを用意し、 そのクラスのインスタンス変数に、拡張対象となるインスタンスを持たせ、 拡張対象と同じインタフェースを実装させる。
class CashewNutsToppingIcecream(Icecream):
def __init__(self, ice: Icecream):
self._ice = ice
def get_name(self):
name = "カシューナッツ"
name += self._ice.get_name()
return name
def how_sweet(self):
return self._ice.how_sweet()
- CashewNutsToppingIcecream クラスは、カシューナッツがトッピングされたアイスクリームを表すクラス。
- このクラスは、Icecream インタフェースを実装し、その getName() メソッドでは、自身が持つインスタンス変数 ice(Icecream インスタンス) の getName() で得られる値に「カシューナッツ」という文字列を付加した値を返り値として返す。また、howSweet() メソッドでは、インスタンス変数 ice の howSweet() メソッドの返り値をそのまま返している。
- このような設計とすることで、以下のように、カシューナッツがトッピングされたバニラアイスクリームも、カシューナッツがトッピングされた抹茶アイスクリームも、 スライスアーモンドがトッピングされたバニラアイスや、スライスアーモンドと、カシューナッツの両方がトッピングされたバニラアイスクリームなど、多様な組合せでのトッピングが可能になる。
ice1 = CashewNutsToppingIcecream(VanillaIcecream()) # カシューナッツトッピングのバニラアイス
ice2 = CashewNutsToppingIcecream(GreenTeaIcecream()) # カシューナッツトッピングの抹茶アイス
Decoratorパターンのまとめ
Visitorパターン
- Visitor パターンでは、「処理」を訪問者である Visitor オブジェクトに記述することで、処理の追加を簡単にする。
- 処理対象となる、Acceptor オブジェクトは、Visitor オブジェクトを受け入れる accept(Visitor visitor)メソッドを実装している必要ある。
- 例
- 家の「水道工事」を行ってもらう場合、「水道工事業者」を家に呼んで、「よろしくお願いします。」と言って、後は全てお任せする。
- そのほかにも、電気工事業者を呼ぶことも、リフォーム業者を呼ぶこともある。
- これらの訪問者に対して、あなたは、「では、よろしく」と言って、ほとんどの作業をお任せする
- お任せの仕方に多少の違いがあるかもしれないが、最終的には、全てを業者にお任せすることになる。
- もし、新しいサービスを提供する業者が現れたときにも、各家庭は、なんら態度を変える必要が無く、その業者を呼んで、「よろしくお願いします。」というだけで、その新しいサービスを受けることができる。
- Visitor パターンでは、このように、受け入れる側に処理を追加することなく、処理を追加することができるパターン。
実際に使ってみる
題材
- 家庭訪問を例に考える。
- 各家庭では、先生であろうと、近所のおばちゃんであろうと、訪問者が訪れると、知らない人でなければ、「いらっしゃい」と言って受け入れる。
- この際、各家庭を Acceptor 、先生を Visitor として、Visitor パターンに当てはめて考えてみる。
- Visitor パターンでは、Visitor は、訪問対象となる、家庭を訪問する。訪問された家庭は、「ようこそいらっしゃいました」と先生を受けいれる。
- このとき、新人だろうがベテランだろうが、先生を受け入れる側は変化がない。
# -*- coding:utf-8 -*-
from abc import ABCMeta, abstractmethod
# 先生クラス
class Teacher(metaclass=ABCMeta):
def __init__(self, students):
self._students = students
@abstractmethod
def visit(self, student_home):
getattr(self, 'visit_' + student_home.__class__.__name__.lower())(student_home)
@abstractmethod
def get_student_list(self):
return self._students
# 新人先生クラス
class RookieTeacher(Teacher):
def __init__(self, students):
super().__init__(students)
def visit(self, student_home):
print("先生:こんにちは")
super().visit(student_home)
@staticmethod
def visit_tanaka(tanaka):
tanaka.praised_child()
@staticmethod
def visit_suzuki(suzuki):
suzuki.reproved_child()
def get_student_list(self):
return self._students
# 家庭クラス
class Home(metaclass=ABCMeta):
@staticmethod
def praised_child():
pass
@staticmethod
def reproved_child():
pass
# 受け入れインタフェース
class TeacherAcceptor(metaclass=ABCMeta):
def accept(self, teacher: Teacher):
pass
# 鈴木さんの家庭
class Suzuki(Home, TeacherAcceptor):
@staticmethod
def praised_child():
print("スズキ母:あら、先生ったらご冗談を")
@staticmethod
def reproved_child():
print("スズキ母:うちの子に限ってそんなことは・・・。")
def accept(self, teacher: Teacher):
teacher.visit(self.__class__)
# 田中さんの家庭
class Tanaka(Home, TeacherAcceptor):
@staticmethod
def praised_child():
print("タナカ母:あらあら、先生ったらご冗談を")
@staticmethod
def reproved_child():
print("タナカ母:まさか、うちの子に限ってそんなことは・・・。")
def accept(self, teacher: Teacher):
teacher.visit(self.__class__)
if __name__ == "__main__":
rt = RookieTeacher(["suzuki", "tanaka"])
rt.visit(Suzuki())
rt.visit(Tanaka())
- 各家庭のacceptメソッドが先生(visitor)のvisitメソッドを呼び出すことで、共通の処理を実現している
Visitorパターンのまとめ
所感
- もっといい例がある気がする・・・