はじめに
この記事は個人的な勉強メモです。inputしたものはoutputしなくてはという強迫観念に駆られて記事を書いています。
あわよくば詳しい人に誤りの指摘やアドバイスを頂ければいいなという思いを込めてQiitaの記事にしています。
エンジニアとして社会人生活を送っていますが、デザインパターンについてちゃんと学んだことがなかったので勉強してみました。
ここに記載している内容は
https://github.com/ck-fm0211/notes_desigh_pattern
にuploadしています。
過去ログ
デザインパターンについて勉強してみた(個人的メモ)その1
デザインパターンについて勉強してみた(個人的メモ)その2
デザインパターンについて勉強してみた(個人的メモ)その3
AbstractFactoryパターン
-
インスタンスの生成を専門に行うクラスを用意することで、整合性を必要とされる一連のオブジェクト群を間違いなく生成するためのパターン
-
例
- 「車」を作成するプログラムを考える
- 「車」オブジェクトである変数 car にタイヤとハンドルを追加してみる
car.add_tire(CarTire()); car.add_handle(CarHandle());
- プログラマのミスで異なるタイヤ(自転車のタイヤ)を渡してしまうこともあるかもしれない
car.add_tire(BicycleTire());
car.add_handle(CarHandle());
- 利用すべきではないオブジェクトを利用してしまうことはあり得る
- このようなとき、車を作成するために必要な一連のオブジェクトを作成することを一手に引き受ける工場クラスを用意し、タイヤやハンドルといったオブジェクトを生成するときは、この工場クラスを利用してオブジェクトの生成を行うようにすることで、先ほどのような間違いが起こることを回避することができる
- この工場クラスを変更することで、利用する一連のオブジェクトをごっそり変更することもできるようになる。
実際に使ってみる
題材
- 「鍋」を作ることを考える
- 構成要素
- スープ
- メインの具(たんぱく質)
- 野菜
- その他の具
- 鍋物をあらわす HotPot クラスは以下のように定義されているものとする
# -*- coding:utf-8 -*-
class HotPot:
pot = None
soup = None
protein = None
vegetables = []
other_ingredients = []
@staticmethod
def hot_pot(pot):
HotPot.pot = pot
@staticmethod
def add_soup(soup):
HotPot.soup = soup
@staticmethod
def add_main(protein):
HotPot.protein = protein
@staticmethod
def add_vegitables(vegitables):
HotPot.vegetables = vegitables
@staticmethod
def add_other_ingredients(other_ingredients):
HotPot.other_ingredients = other_ingredients
- 鍋を作る工程を抽象化した Factory クラスを用意する
class Factory(metaclass=ABCMeta):
@abstractmethod
def get_soup(self):
pass
@abstractmethod
def get_main(self):
pass
@abstractmethod
def get_vegetables(self):
pass
@abstractmethod
def get_other_ingredients(self):
pass
- これを継承したMizutaki(水炊き)Factoryクラスを用意し、これをもとに水炊きをつくる。
class MizutakiFactory(Factory):
def get_soup(self):
return "torigara"
def get_main(self):
return "chicken"
def get_vegetables(self):
return ["hakusai", "ninjin", "negi"]
def get_other_ingredients(self):
return ["tofu"]
class Sample:
@staticmethod
def main():
hp = HotPot()
mf = MizutakiFactory()
hp.add_soup(mf.get_main())
hp.add_main(mf.get_main())
hp.add_vegitables(mf.get_vegetables())
hp.add_other_ingredients(mf.get_other_ingredients())
- 引数に与えられた文字列によって、実際に利用する Factory クラスを選択して生成する
class Sample:
@staticmethod
def create_factory(s):
if s == "キムチ鍋":
return KimuchiFactory()
elif s == "すき焼き":
return SukiyakiFactory()
else:
return MizutakiFactory()
@staticmethod
def main(arg):
hp = HotPot()
fc = create_factory(arg)
hp.add_soup(fc.get_main())
hp.add_main(fc.get_main())
hp.add_vegitables(fc.get_vegetables())
hp.add_other_ingredients(fc.get_other_ingredients())
- main メソッドの中では、Factory メソッドの実際の型を知ることなく処理が進んでいる。すなわち、抽象的な Factory クラスを利用して処理を進めていっている。
- 「利用するオブジェクト群をごそっと入れ替える」という要求に応えることができる
Builderパターンのまとめ
Bridgeパターン
- 機能と実装を分離して、それぞれを独立に拡張することができるようになる
- 例
- ある methodA というメソッドを持つクラス MyClassA は、methodA メソッドの実装が異なる MyClassASub1、MyClassASub2 という2つのクラスによって継承されているとする
- MyClassA にmethodB というメソッドを追加するために、MyClassB クラスという MyClassA を継承するクラスを作成したとする
- MyClassB でも、MyClassASub1、MyClassASub2 で実装している methodA と同じ実装を利用したい場合、MyClassB クラスを継承する MyClassBSub1、MyClassBSub2 といったクラスを作成する必要がある ← めんどい
- 規模が大きくなると面倒さが倍々ゲームになる
実際に使ってみる
題材
- ソート機能を持つ抽象クラスSorter と、この Sorter クラスで定義されている抽象メソッドである sort(Object obj[]) メソッドを実装するクラス (QuickSorter クラス、BubbleSorter クラス) について考える
- Sorter クラス、QuickSorter クラス、BubbleSorter クラスのコードはそれぞれ以下のようになっている。ソート部分の実装に関しては、ここでは重要でないため省略する。
# -*- coding:utf-8 -*-
from abc import ABCMeta, abstractmethod
class Sorter(metaclass=ABCMeta)
@abstractmethod
def sort(self, obj):
pass
class QuickSorter(Sorter):
def sort(self, obj):
# クイックソート
pass
class BubbleSorter(Sorter):
def sort(self, obj):
# バブルソート
pass
- Sorterクラスにソートにかかった時間を表示する機能を持つ timer_sorter メソッドを追加したい
from datetime import datetime
class TimerSorter(metaclass=ABCMeta, Sorter):
@abstractmethod
def time_sorter(self, obj):
start = datetime.now()
Sorter.sort(obj)
end = datetime.now()
print("time:" + str((end - start)))
- このままだと、TimerSorterクラスに実装を与えられない
- 実装するためにはQuickSorter/BubbleSorter相当のクラスを用意する必要がある
- Bridgeパターンで考える。実装の変更が考えられるメソッドに関しては、実装用のクラス階層に委譲するように設計する。
- 実装用のクラス階層:ここでは sort メソッドの実装を与えるクラス階層として、SortImple クラスを親とするクラス階層を考える
class SortImple(metaclass=ABCMeta):
@abstractmethod
def sort(self, obj):
pass
class Sorter:
si = SortImple()
@staticmethod
def sorter(si):
Sorter.si = si
def sort(self, obj):
Sorter.si.sort(obj)
class QuickSorterImple(SortImple):
def sort(self, obj):
# クイックソート
pass
class BubbleSorterImple(SortImple):
def sort(self, obj):
# バブルソート
pass
- こうすることで機能を追加するために、Sorter クラスを拡張して作成した新しいクラスでも、すでに存在する実装部分を利用することができるようになる。例えば、Sorter クラスを拡張する TimerSorter クラスを作成する場合には、以下のようになる。
from datetime import datetime
class TimerSorter(metaclass=ABCMeta, Sorter):
def __init__(self, sort_impl):
super(sort_impl)
@abstractmethod
def time_sorter(self, obj):
start = datetime.now()
Sorter.sort(obj)
end = datetime.now()
print("time:" + str((end - start)))
- 機能を拡張するためのクラス階層と、実装を拡張するためのクラス階層を分けておくことで、実装階層クラスと機能拡張クラスを好みの組み合わせで利用することができるようになる
- 今回の例では、Sorter クラスと SortImple クラスが機能拡張クラス階層と実装拡張クラス階層を橋渡しする役目を果たしている
Builderパターンのまとめ
Strategyパターン
- 普通にプログラミングしていると、メソッドの中に溶け込んだ形でアルゴリズムを実装してしまうことがある
- if 文などで分岐させることでアルゴリズムを変更するなど
- Strategy パターンでは、戦略の部分を意識して別クラスとして作成するようにする
- 戦略x部分を別クラスとして作成しておき、戦略を変更したい場合には、利用する戦略クラスを変更するという方法で対応する
- Strategy パターンを利用することで、メソッドの中に溶け込んだ形のアルゴリズムより柔軟でメンテナンスしやすい設計となる
実際に使ってみる
題材
- 状況に応じてアルゴリズムを変えなければならないことは多々ある。例えばゲームのプログラムでは、難易度の設定によって、その戦略アルゴリズムを変えるなど。
- ここでは簡単に、大小の比較を行うアルゴリズムを考えてみる。
- まずは、人間を表す Human クラスを作成する。Human クラスは、名前、身長、体重、年齢の4つのパラメータを持つものとする。
# -*- coding:utf-8 -*-
from abc import ABCMeta, abstractmethod
class Human:
def __init__(self, name, height, weight, age):
self.name = name
self.height = height
self.weight = weight
self.age = age
- ここで、2つのHuman インスタンスが与えられた場合に、それらの大小を比較する SampleClass というクラスを考える。
class SampleClass:
@staticmethod
def compare(h1: Human, h2: Human) -> int:
if h1.age > h2.age:
return 1
elif h1.age == h2.age:
return 0
else:
return -1
- ここでは年齢を比較してその結果を返すことだけしか考えていない
- しかし、Human オブジェクトには複数のパラメータがあり、Human を比較する方法はいくつか考えられる
- 比較結果は、どのパラメータをどのように利用するかにより異なってしまう。
- 例えば、単純に年齢で比較する場合と、身長で比較する場合では異なる結果となる
- そこで、比較するパラメータを指定できるようなプログラムとすることを考える。
class SampleClass2:
type = -1
COMPARE_AGE = 1
COMPARE_HEIGHT = 2
COMPARE_WEIGHT = 3
def __init__(self, type):
SampleClass2.type = type
@staticmethod
def compare(h1: Human, h2: Human) -> int:
if SampleClass2.type == SampleClass2.COMPARE_AGE:
if h1.age > h2.age:
return 1
elif h1.age == h2.age:
return 0
else:
return -1
elif SampleClass2.type == SampleClass2.COMPARE_HEIGHT:
if h1.height > h2.height:
return 1
elif h1.height == h2.height:
return 0
else:
return -1
# ・・・
- 煩雑なコードになってしまう。Strategy パターンでは、状況に応じて、変更する必要のあるアルゴリズムの部分を、意識的に別クラスとして分離することで、アルゴリズムの修正、追加等の見通しを良くする。
- まずは、比較アルゴリズム部分をクラスとして分離する。例えば、年齢を比較するための、AgeComparatorクラスを作成する。
class AgeComparator:
@staticmethod
def compare(h1: Human, h2: Human) -> int:
if h1.age > h2.age:
return 1
elif h1.age == h2.age:
return 0
else:
return -1
- 比較アルゴリズム部分を分離し、実際の比較処理は、AgeComparator に委譲できるようにしておく。
class MyClass:
@staticmethod
def compare(h1: Human, h2: Human) -> int:
return AgeComparator.compare(h1, h2)
- これだけではメリットはなく、 Strategy パターンにもなっていない。Strategy パターンでは、分離したアルゴリズム部分が共通のインタフェースを持つようにすることが求められる。
- すなわち、アルゴリズムとして分離された複数のクラスが共通のインタフェースを持つ必要がある。
- サンプルケースでは、年齢を比較する AgeComparator クラス以外にも、身長を比較するための HeightComparatorクラス、体重を比較するための WeightComparatorクラスなどが考えられる。
- これらの比較アルゴリズムを表すクラスに共通のインタフェースを持たせる。
- ここでは、Comparator インタフェースを定義してみます。
class Comparator(metaclass=ABCMeta):
@staticmethod
@abstractmethod
def compare(h1: Human, h2: Human) -> int:
pass
class AgeComparator(Comparator):
@staticmethod
def compare(h1: Human, h2: Human) -> int:
if h1.age > h2.age:
return 1
elif h1.age == h2.age:
return 0
else:
return -1
class HeightComparator(Comparator):
@staticmethod
def compare(h1: Human, h2: Human) -> int:
if h1.height > h2.height:
return 1
elif h1.height == h2.height:
return 0
else:
return -1
- こうすることでSampleClassは以下のように書き換えられる
class SampleClass:
def __init__(self, comp: Comparator):
self._comp = comp
def compare(self, h1: Human, h2: Human) -> int:
return self._comp.compare(h1, h2)