0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

デザインパターンについて勉強してみた(個人的メモ)その4(AbstractFactoryパターン、Bridgeパターン、Strategyパターン)

Last updated at Posted at 2020-01-01

はじめに

この記事は個人的な勉強メモです。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パターンのまとめ

AbstractFactory.png

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パターンのまとめ

Bridge.png

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)

Strategyパターンのまとめ

Strategy.png

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?