🤔

[学習メモ]Pytho3で学ぶデザインパターン~振る舞いに関するデザインパターンその2~

2022/06/27に公開約21,900字

この記事について

振る舞いに関するデザインパターン11種類について、個人の学習メモです。
(恥ずかしながら初勉強です・・・自身の整理のためにもまとめていこうと思います)

今回は11種類のうち6種類について、「その2」として学んでいきます。

概要

デザインパターンについて

簡単にいうと設計のノウハウ集です。

OOPL(オブジェクト指向プログラミング)によって、ソフトウェアの再利用部品を作ることが可能になりました。クラスライブラリやフレームワークと呼ばれているものです。このクラスライブラリやフレームワークを作成する過程で、機能拡張が容易で再利用がしやすい先人たちの設計アイデアから共通部分を抜き出して(抽象化して)、設計パターンとしてまとめたものです。

ここでは広く一般的に知られている、優れた技術者4人GoF(ゴフ、ギャングオブフォー)が書いた「書籍:デザインパターン」にて発表されたGoFのデザインパターンについて扱います。

取り扱い注意事項

  1. 手段を目的にしない
    目的(問題を解決したい)解決する手段(デザインパターンを使用する) です。デザインパターンはよくある問題を解決する手段(糸口)です。
  2. デザインパターンを当てはめて問題を探さない(あら探し)
    問題を探す手段ではなく解決手段です。
  3. 銀の弾丸はありません

GoFのデザインパターン

生成に関するデザインパターン

  • Factory Method
  • Abstract Factory
  • Builder
  • Prototype
  • Singleton

構造に関するデザインパターン

  • Adapter
  • Bridge
  • Composite
  • Decorator
  • Facade
  • Flyweight
  • Proxy

振る舞いに関するデザインパターンその1

  • Chain of Responsibility
  • Command
  • Interpreter
  • Iterator
  • Mediator

振る舞いに関するデザインパターンその2  <- この記事です。

  • Memento
  • Observer
  • State
  • Strategy
  • Template Method
  • Visitor

振る舞いに関するデザインパターン

そもそも、「振る舞いに関するデザインパターン」って何でしょうか?
まずはこの言葉について知ります。

2より引用

「ふるまいに関するデザインパターン」では、いかにして物事を成し遂げるか、ということに力点が置かれている。これらのパターンは、処理について考えたり整理するための強力な方法を提供する

5より引用

It is concerned with assignment of responsibilities between the objects. What makes them different from structural patterns is they don't just specify the structure but also outline the patterns for message passing/communication between them. Or in other words, they assist in answering "How to run a behavior in software component?"
(機械翻訳)これは、オブジェクト間の責任の分担に関係する。構造パターンと異なるのは、単に構造を指定するだけでなく、オブジェクト間のメッセージの受け渡しや通信のパターンも概説している点である。言い換えれば、"ある振る舞いをソフトウェアコンポーネントで実行するにはどうすればよいか?"に答える手助けをするものである。

言いたいことを察するに、振る舞いに関するデザインパターンは「オブジェクト間の責任範囲(振る舞い)を整理することで、オブジェクトの責任を分割しつつも正しく動作をさせること」について書かれたものだと思います。

つまり、構造に関するデザインパターンとは 「オブジェクト間の責任範囲で問題が起きているんだけど、オブジェクトの責任範囲(振る舞い)はどうすればいいんだろう?」といったことに対して解決する手段(糸口) をパターンとして示しています。

Mementoパターン

利用場面

4より引用

GoFによれば,Memento パターンの目的は, 「カプセル化を破壊せずに,オブジェクトの内部状態を捉えて外面化しておき,オブジェクトを後にこの状態に戻すことができるようにする。」
GoFによれば,Memento パターンの別名は,Token です。
GoFによれば,次の2点がなり立つ場合にMemento パターンを使う。
・オブジェクトの状態(の一部)のスナップショットを,後にオブジェクトをその状態に戻すことができるように,セーブしておかなければならない場合。
・状態を得るための直接的なインタフェースが,実装の詳細を公開し,オブジェクトのカプセル化を破壊する場合。

Mementoというのは記念品の意味です。
概要は「オブジェクトのバックアップを残して活用する」です。
利用シーンとしては「オブジェクトのバックアップをとっておいて後で以前の状態に戻したい場合(undo)」です。

具体的に考えてみる

テキストエディタなどで、「undo(1つ前の状態に戻す)」という機能が実装されていますが、あの機能のように「現在の状態をバックアップしておいて前の状態に戻す」ことを可能にするのがMementoパターンです。

上記はあくまで例です。
全てのundoがMementoパターンを利用しているわけではないのでご承知おきください。

作成方法

  • 1. Originatorを作成する。Mementoを利用して状態を保存するクラス
  • 2. Mementoを作成する。状態を保存するクラスでOriginatorの情報を持つ
  • 3. CarreTakerを作成する。Originatorのバックアップやundoなどの処理を担う

オブジェクト思考による作成方法のイメージ

文章を作成&保存するプログラムがあるとします。ここに、保存後に1つ前の状態に戻れる機能を作成するとします。

Editor(Originator)は、内容を更新したりEditorMemento(Memento)を保存します。
保存した状態は、CareTakerからバックアップすることでその状態を保存できます。
CareTakerはメモリ上にMementoを保存しておくイメージです。

Python3で書いてみるでは、Editorに文章を保存し何回か内容を変更します。その後状態を保存しundoで1つ前の状態に戻れるようにしています。また、変更履歴を表示できるようにしてあります。

python3で書いてみる

from datetime import datetime

class EditorMemento:
    '''Memento
    '''
    def __init__(self, day, content):
        self.day = day
        self.content = content
    
    def get_day(self):
        return self.day

    def get_content(self):
        return self.content

class Editor:
    '''Originator
    '''
    def __init__(self, day, content):
        self.day = day
        self.content = content

    def change_content(self, new_content):
        print('contentの変更')
        self.content = self.content+new_content
        self.day = datetime.now()

    def save(self):
        print('保存しました。日付:「{0}」。内容:「{1}」'.format(self.day, self.content))
        return EditorMemento(self.day, self.content)

class CareTaker:
    def __init__(self):
        self.mementos = []
    
    def backup(self, editor_memento: EditorMemento):
        print('状態を保存しました')
        self.mementos.append(editor_memento)
    
    def undo(self):
        print('undoの実行しました')
        if len(self.mementos) == 0:
            return
        memento = self.mementos.pop()
        return memento

    def show_history(self):
        print('変更履歴')
        for memento in self.mementos:
            day = memento.get_day()
            content = memento.get_content()
            print('保存時間:{}。内容{}'.format(day, content))

editor = Editor(datetime.now(),'最初の文章です。')
memento = editor.save()
# 状態を保存
care_taker = CareTaker()
care_taker.backup(memento)

print('-'*10)
editor.change_content('2番目の文章です。')
editor.change_content('3番目の文章です。')
memento = editor.save()
# 状態を保存
care_taker.backup(memento)
# 履歴を表示
care_taker.show_history()
print('-'*10)

# 状態を戻す
undo = care_taker.undo()
print('-'*10)
# 履歴を表示
care_taker.show_history()

実行結果

保存しました。日付:「2022-05-21 14:45:58.204257」。内容:「最初の文章です。」
状態を保存しました
----------
contentの変更
contentの変更
保存しました。日付:「2022-05-21 14:45:58.204332」。内容:「最初の文章です。2番目の文章です。3番目の文章です。」
状態を保存しました
変更履歴
保存時間:2022-05-21 14:45:58.204257。内容最初の文章です。
保存時間:2022-05-21 14:45:58.204332。内容最初の文章です。2番目の文章です。3番目の文章です。
----------
undoの実行しました
----------
変更履歴
保存時間:2022-05-21 14:45:58.204257。内容最初の文章です。

Observerパターン

利用場面

4より引用

GoFによれば,Observer パターンの目的は, 「あるオブジェクトが状態を変えたときに,それに依存するすべてのオブジェクトに自動的にそのことが知らされ,また,それらが更新されるように,オブジェクト間に一対多の依存関係を定義する。」
~(中略)~
GoFによれば,次のような状況で Observer パターンを使う。
・抽象化により,2つの面が,一方が他方に依存しているという形で現れる場合。これらの面をそれぞれ別々のオブジェクトにカプセル化することにより,それらを独立に変更したり,再利用することが可能になる。
・1つのオブジェクトを変化させるときに,それに伴いその他のオブジェクトも変化させる必要があり,しかも変化させる必要があるオブジェクトを固定的に決められない場合。
・オブジェクトが,他のオブジェクトに対して,それがどのようなものなのかを仮定せずに通知できるようにする場合。別の言い方をすると,これらのオブジェクトを密に結合したくない場合。

Observerという言葉は、観察者の意味です。
概要は「オブジェクトの状態を監視をして変更があれば通知する」です。
利用シーンとしては「変更を通知する場合」です。

具体的に考えてみる

6より引用

Real world example
A good example would be the job seekers where they subscribe to some job posting site and they are notified whenever there is a matching job opportunity.

(機械翻訳)現実世界の例
例えば、求職者が求人情報サイトを購読し、マッチングする求人があるたびに通知を受けるような場合です。

システムが求人サイトを自動監視(Observer)をしていて、新しい求人が発行されたらそれを求職者に通知することを想像するともっとわかりやすいかもしれません。

作成方法

  • 1. Observerを作成する。観察者。
  • 2. Subjectを作成する。される側
  • 3. ConcreteObserverを作成する。Observerの具像クラス。
  • 4. ConcreteSubjectを作成する。Subjectの具像クラス。Observerを登録したり削除する

オブジェクト思考による作成方法のイメージ

6より引用元の求職者の例を参考にします。

求職者(JobSeeker)はConcreteObserverに該当しており、求人情報(JobPostings)を監視しています。求人情報にはConcreteObserver(監視者)を登録することが可能です。

求人情報が追加されるとSubjectはObserverを呼び出します(Observerの通知を実行するメソッドを呼び出します)

python3で書いてみる

from abc import ABC, abstractmethod


class Subject(ABC):
    
    def __init__(self):
        self._observers = []
    
    def attach(self, observer):
        self._observers.append(observer)
        
    @abstractmethod
    def notify(self):
        pass

class JobPostings(Subject):
    '''ConcreteSubject
    '''
    def notify(self):
        for observer in self._observers:
            observer.on_job_posted(self)
    
    def add_job(self, title):
        self.title = title
        self.notify()
    
class Observer(ABC):
    @abstractmethod
    def on_job_posted(self, subject: Subject):
        pass

class JobSeeker(Observer):
    '''ConcreteObserver
    '''
    def __init__(self, name):
        self.name = name

    def on_job_posted(self, subject: Subject):
        print('{}さん。求人情報{}が公開されました'.format(self.name, subject.title))

hoge = JobSeeker('hogeさん')
fuga = JobSeeker('fugaさん')

# hogeとfugaを登録
job_post = JobPostings()
job_post.attach(hoge)
job_post.attach(fuga)

# 求人を登録すると、notifyでObserverが呼び出される
job_post.add_job('フロントエンド')

実行結果

hogeさんさん。求人情報フロントエンドが公開されました
fugaさんさん。求人情報フロントエンドが公開されました

Stateパターン

利用場面

4より引用

GoFによれば,State パターンの目的は, 「オブジェクトの内部状態が変化したときに,オブジェクトが振る舞いを変えるようにする。クラス内では,振る舞いの変化を記述せず,状態を表すオブジェクトを導入することでこれを実現する。」

GoFによれば,次に示すいずれかの場合に,State パターンを利用する。
・オブジェクトの振る舞いが状態に依存し,実行時にはオブジェクトがその状態により振る舞いを変えなければならない場合。
・オペレーションが,オブジェクトの状態に依存した多岐にわたる条件文を持っている場合。この状態はたいてい1つ以上の列挙型の定数で表されており,たびたび複数のオペレーションに同じ条件構造が現れる。State パターンでは,1つ1つの条件分岐を別々のクラスに受け持たせる。これにより,オブジェクトの各状態を1つのオブジェクトとして扱うことができるようになる。

Stateという言葉は、状態の意味です。
概要は「オブジェクトの状態をクラスで表現する」です。
利用シーンとしては「状態に応じて処理を分けたい場合。オブジェクトが複数の条件分で分岐するような場合」です。

具体的に考えてみる

6より引用

Real world example
Imagine you are using some drawing application, you choose the paint brush to draw. Now the brush changes its behavior based on the selected color i.e. if you have chosen red color it will draw in red, if blue then it will be in blue etc.

(機械翻訳)現実世界の例
ある作図アプリケーションを使用しているとき、ペイントブラシを選択して絵を描くとします。例えば、赤色を選択した場合は赤で、青色を選択した場合は青で描かれます。

お絵描きソフトで色を選択する際に、状態(赤色、青色)によって描ける線の色(赤色、青色)を変えたい時に一つのクラス内でif文を利用して振る舞いを変更していると、色が追加になった時にクラスが長くなっていき拡張性が下がります。このようなケースを防ぐために状態ごとにクラスを分けてしまおくというのがStateパターンです。

作成方法

    1. Stateを作成する。インターフェースで各状態で振る舞いを変えるメソッドを持っておく
    1. ConcreteStateを作成する。Stateの具像クラス。
    1. Contextを作成する。状態の管理をするクラス

オブジェクト思考による作成方法のイメージ

今、緊急度の高い仕事がおりてきたとします。
午前に緊急度の高い仕事が来れば午前中に対応可能だし、午後の時間なら終業時間までに対応可能です。

繰り返しになりますが、「午前、午後のどちらかに実行できるかは仕事がおりてきた時間帯」によって変わります、この午前と午後が状態を指しています。Stateを継承して作成される具像クラスがAmState(ConcreteState1)、PmState(ConcreteState2)です。

Contextでは状態を管理しておりStateを利用します。また、状態が時間で変化するようにしています。

python3で書いてみる

from abc import ABC, abstractmethod

class State(ABC):
    '''State
    '''
    @abstractmethod
    def start(self, work):
        pass

    @abstractmethod
    def end(self, work):
        pass

class AmState(State):
    '''ConcreteState1
    '''

    def start(self, work):
        print('午前の業務「{}」を開始します'.format(work))
    
    def end(self, work):
        print('午前の業務「{}」を終了します'.format(work))

class PmState(State):
    '''ConcreteState2
    '''
    def start(self, work):
        print('午後の業務「{}」を開始します'.format(work))
    
    def end(self, work):
        print('午後の業務「{}」を終了します'.format(work))

class Context:
    '''Context
    '''
    def __init__(self):
        self.state = AmState()

    def run(self,hour,work):
        self.change_state(hour)
        self.state.start(work)
        self.state.end(work)

    def change_state(self, hour):
        if hour >= 9 and hour <= 12:
            self.state = AmState()
        if hour >= 13 and hour <= 18:
            self.state = PmState()

context = Context()
# 午前の仕事
now = 12
context.run(now, '書類整理')
# 午後の仕事
now = 13
context.run(now, '会議')

実行結果

午前の業務「書類整理」を開始します
午前の業務「書類整理」を終了します
午後の業務「会議」を開始します
午後の業務「会議」を終了します

Strategyパターン

利用場面

4より引用

GoFによれば,Strategy パターンの目的は, 「アルゴリズムの集合を定義し,各アルゴリズムをカプセル化して,それらを交換可能にする。Strategy パターンを利用することで,アルゴリズムを,それを利用するクライアントからは独立に変更することができるようになる。」

2より引用

アルゴリズムの集合があり、それぞれを必要に応じて交換しながら用いたい場合、Strategyパターンを用いれば、アルゴリズムの集合をカプセル化できる

Strategyという言葉は、戦略の意味です。
概要は「クラスで分割した複数のアルゴリズム(戦略)で目標を実現する」です。
利用シーンとしては「状況に応じて戦略(アルゴリズム)を変えたい場合」です。

具体的に考えてみる

6より引用

Real world example
Consider the example of sorting, we implemented bubble sort but the data started to grow and bubble sort started getting very slow. In order to tackle this we implemented Quick sort. But now although the quick sort algorithm was doing better for large datasets, it was very slow for smaller datasets. In order to handle this we implemented a strategy where for small datasets, bubble sort will be used and for larger, quick sort.

(機械翻訳)現実世界の例
ソートの例として、バブルソートを実装しましたが、データが大きくなるにつれて、バブルソートが非常に遅くなるようになりました。そこで、クイックソートを実装しました。しかし、現在、クイックソートのアルゴリズムは、大きなデータセットではうまくいっているものの、小さなデータセットでは非常に遅くなってしまっているのです。これを処理するために、小さいデータセットにはバブルソートを、大きいデータセットにはクイックソートを使用する戦略を導入しました。

状況(データセットの大きさ)に応じてアルゴリズムを変更する戦略を導入しています、これをStrategyパターンといいます。

作成方法

    1. Strategyを作成する。インターフェース。
    1. ConcreteStrategyを作成する。Strategyの具像クラス。具体的な戦略を書く
    1. Contextを作成する。状況に応じて戦略を実行するクラス

オブジェクト思考による作成方法のイメージは、Stateパターンと構成が同じなので省略します。

python3で書いてみる

6の引用元にあるソートアルゴリズムの例で書こうと思います。

from abc import ABC, abstractmethod

class SortStrategy(ABC):
    '''Starategy
    '''
    @abstractmethod
    def sort(self, dataset):
        pass

class BubbleSortStrategy(SortStrategy):
    '''ConcreteStrategy1
    '''
    def sort(self, dataset):
        print('バブルソートの実行')
        return dataset

class QuickSortStrategy(SortStrategy):
    '''ConcreteStrategy2
    '''
    def sort(self, dataset):
        print('クイックソートの実行')
        return dataset

class Sorter:
    '''Context
    '''
    def __init__(self, sorter):
        self.__sorter = sorter

    def execute(self, dataset):
        self.__sorter.sort(dataset)

dataset = [1,2,4,6,8,12]
# dataset = [x for x in range(200)]

if len(dataset) >= 100:
    sorter = Sorter(QuickSortStrategy())
    sorter.execute(dataset)
else:
    sorter = Sorter(BubbleSortStrategy())
    sorter.execute(dataset)

実行結果

クイックソートの実行

Template Methodパターン

利用場面

4より引用

GoFによれば,Template Method パターンの目的は, 「1つのオペレーションにアルゴリズムのスケルトンを定義しておき,その中のいくつかのステップについては,サブクラスでの定義に任せることにする。Template Method パターンでは,アルゴリズムの構造を変えずに,アルゴリズム中のあるステップをサブクラスで再定義する。」
GoFによれば,Template Method パターンは次のような場合に利用する。
・アルゴリズムの不変な部分をまず実装し,振る舞いが変わり得る部分の実装はサブクラスに残しておく場合。

概要は「親クラス(インターフェース)で処理のステップを作成しておき、サブクラスで具体的な内容を定める」です。利用シーンとしては「振る舞いが変わり得る部分の実装はサブクラスにしたい場合」です。

具体的に考えてみる

Real world example
Suppose we are getting some house built. The steps for building might look like

  • Prepare the base of house
  • Build the walls
  • Add roof
  • Add other floors
    The order of these steps could never be changed i.e. you can't build the roof before building the walls etc but each of the steps could be modified for example walls can be made of wood or polyester or stone.

(機械翻訳)現実世界の例
何か家を建てることになったとします。建築の手順は次のような感じでしょうか

  • 家の土台(基礎)を準備する
  • 壁を作る
  • 屋根を追加する
  • 他のフロアの追加
    これらのステップの順序を変更することはできません。つまり、壁などを構築する前に屋根を構築することはできませんが、各ステップを変更することはできます。たとえば、壁を木、ポリエステル、または石で作ることができます。

翻訳が微妙なので補足します。
ステップの順序(アルゴリズムの順番)は変更することはできないけれど、各ステップ中で何をどう作成するか?(家の土台をコンクリートで作る。石で作る・・・など)具体的なことを決めることができます。

利用場面に照らし合わせると「各ステップ中で」の箇所がサブクラスの話になり、「ステップの順序」が親クラスの話です。

作成方法

    1. AbstractClassを作成する。外部から利用できるメソッド(テンプレートメソッド)を実装する。テンプレートメソッド内で利用する予定の抽象メソッドも記述しておく
    1. ConcreteClassを作成する。AbstractClassを継承し、具体的なメソッドにする

オブジェクト思考による作成方法のイメージ

6の引用元にある「ソフトウェアテストを支援するビルドツール」の例をPythonに書き直させていただきます。

BuilderはAbstractClassの役です。ビルドツールのステップとしては「テスト」「CoddeのLint」「アセンブリ実行」「サーバーにデプロイ」の順番です。このステップをテンプレートメソッドとしてbuild()を用意しています。

ソフトウェアのビルド方法はIosとAndoroidで分かれていることにします。これはサブクラス(具像クラス)の「ConcreteClass1」「ConcreteClass2」が該当します。buildを呼ぶ際はサブクラスから呼びます。

python3で書いてみる

from abc import ABC, abstractmethod

class Builder(ABC):
    '''AbstractClass
    '''
    def build(self):
        '''テンプレートメソッド
        '''
        self.test()
        self.lint()
        self.assemble()
        self.deploy()
    
    @abstractmethod
    def test(self):
        pass

    @abstractmethod
    def lint(self):
        pass

    @abstractmethod
    def assemble(self):
        pass

    @abstractmethod
    def deploy(self):
        pass


class AndroidBuilder(Builder):
    '''ConcreteClass1
    '''
    def test(self):
        print('Androidのテストを実行')

    def lint(self):
        print('AndroidコードのLintを実行')

    def assemble(self):
        print('Androidビルドのアセンブリを実行')

    def deploy(self):
        print('Androidコードをサーバーにデプロイ')


class IosBuilder(Builder):
    '''ConcreteClass2
    '''
    def test(self):
        print('Iosのテストを実行')

    def lint(self):
        print('IosコードのLintを実行')

    def assemble(self):
        print('Iosビルドのアセンブリを実行')

    def deploy(self):
        print('Iosコードをサーバーにデプロイ')

android_builder = AndroidBuilder()
android_builder.build()

print('-'*100)

ios_builder = IosBuilder()
ios_builder.build()

実行結果

Androidのテストを実行
AndroidコードのLintを実行
Androidビルドのアセンブリを実行
Androidコードをサーバーにデプロイ
----------------------------------------------------------------------------------------------------
Iosのテストを実行
IosコードのLintを実行
Iosビルドのアセンブリを実行
Iosコードをサーバーにデプロイ

Visitorパターン

利用場面

4より引用

GoFによれば,Visitor パターンの目的は, 「あるオブジェクト構造上の要素で実行されるオペレーションを表現する。Visitor パターンにより,オペレーションを加えるオブジェクトのクラスに変更を加えずに,新しいオペレーションを定義することができるようになる。」
GoFによれば,Visitor パターンは次のような場合に使用する。
・オブジェクト構造にインタフェースが異なる多くのクラスのオブジェクトが存在し,これらのオブジェクトに対して,各クラスで別々に定義されているオペレーションを実行したい場合。
(~省略)

Visitorという言葉は、訪問者の意味です。
概要は「元クラスを変化させずにVisitorで機能を追加する」です。
利用シーンとしては「複数クラスをまたぎながら処理をする機能を作成したい場合」です。

具体的に考えてみる

6より引用

Real world example
Consider someone visiting Dubai. They just need a way (i.e. visa) to enter Dubai. After arrival, they can come and visit any place in Dubai on their own without having to ask for permission or to do some leg work in order to visit any place here; just let them know of a place and they can visit it. Visitor pattern lets you do just that, it helps you add places to visit so that they can visit as much as they can without having to do any legwork.

(機械翻訳)現実世界の例
ドバイを訪れる人を考えてみましょう。ドバイに入国するための方法(=ビザ)さえあればいいのです。到着後、ドバイのどの場所に行くにも、許可を得たり、足を運んだりする必要はなく、ただ場所を知らせれば、自分たちで訪れることができるのです。Visitorパターンを使えば、訪問先を追加することで、足を運ばずにいくらでも訪問できるようになります。

ビザを持って入国した(訪れた側)は、ビザさえあればブルジュ・ハリファでもダウンタウンでも自由にいけます。ダウンタウンに行く予定をブルジュ・ハリファに変更したとしても、ドバイに行き先を変更を告げなくても良いです。つまり。訪れた側に機能変更を加えるだけでドバイ側に変更を伝えなくて良いですよね?ということです。

作成方法

    1. Visitorを作成する。Elementに接続するための抽象メソッドを定義しておく
    1. ConreteVisitorを作成する。Visitorを継承した具像クラス
    1. Elementを作成する。Vistorが訪れる対象のクラスを定義する。インターフェース
    1. ConcreteElementを作成する。Elementを継承した具像クラス

オブジェクト思考による作成方法のイメージ

6の引用元にある動物園のシミュレーションを参考にしております。

動物園オペレーションを考えます。
各動物のところへ訪問し清掃員が掃除するとします。これをCleaning(ConcreteVisitor)とします。AnimalOperationを継承したConreteVisitorは、訪れる側で各動物(Element)のところへいくためのメソッドを持っています。緑枠は訪問される側です(Element、ConcreteElement)


ここでは掃除のみを扱いましたが、見回りする機能を追加するとします。その場合はAnimalOperationを継承して見回りクラスを作成すればよくなり、Element側を変更する必要がないです。これが概要の「元クラスを変化させずにVisitorで機能を追加する」です。

python3で書いてみる

from abc import ABC, abstractmethod

class AnimalOperation(ABC):
    '''Visitor
    '''
    @abstractmethod
    def visit_monkey(monkey):
        pass

    @abstractmethod
    def visit_lion(lion):
        pass

    @abstractmethod
    def visit_dolphin(dolphin):
        pass

class Animal(ABC):
    '''Element
    '''
    @abstractmethod
    def accept(self, visitor: AnimalOperation):
        pass

class Monkey(Animal):
    '''ConcreteElement1
    '''
    def accept(self, visitor: AnimalOperation):
        return visitor.visit_monkey()

class Lion(Animal):
    '''ConcreteElement2
    '''
    def accept(self, visitor: AnimalOperation):
        return visitor.visit_lion()

class Dolphin(Animal):
    '''ConcreteElement3
    '''
    def accept(self, visitor: AnimalOperation):
        return visitor.visit_dolphin()

class Cleaning(AnimalOperation):
    '''ConcreteVisitor
    '''
    def visit_monkey(self):
       print('モンキースペースの掃除完了')

    def visit_lion(self):
       print('ライオンスペースの掃除完了')

    def visit_dolphin(self):
        print('イルカスペースの掃除完了')

monkey = Monkey()
lion = Lion()
dolphin = Dolphin()
cleaning_operation = Cleaning()

monkey.accept(cleaning_operation)
lion.accept(cleaning_operation)
dolphin.accept(cleaning_operation)

実行結果

モンキースペースの掃除完了
ライオンスペースの掃除完了
イルカスペースの掃除完了

まとめ

パターン名称 概要 利用シーン
Memento オブジェクトのバックアップを残して活用する オブジェクトのバックアップをとっておいて後で以前の状態に戻したい場合(undo)
Observer オブジェクトの状態を監視をして変更があれば通知する 変更を通知する場合
State オブジェクトの状態をクラスで表現する 状態に応じて処理を分けたい場合。オブジェクトが複数の条件分で分岐するような場合
Strategy クラスで分割した複数のアルゴリズム(戦略)で目標を実現する 状況に応じて戦略(アルゴリズム)を変えたい場合
Template Method 親クラス(インターフェース)で処理のステップを作成しておき、サブクラスで具体的な内容を定める 振る舞いが変わり得る部分の実装はサブクラスにしたい場合
Visitor 元クラスを変化させずにVisitorで機能を追加する 複数クラスをまたぎながら処理をする機能を作成したい場合

振る舞いに関するデザインパターン11種類のまとめ

パターン名称 概要 利用シーン
Chain of Responsibility 個々の処理をチェーン状に繋いで、チェーンに従い各担当が確認処理をしていく 複数の処理をつなげて1つの処理としたい場合。事前に最終担当が決まってない場合
Command コマンド(要求/命令)をオブジェクトとしてカプセル化する 多種多様なコマンドに対応する場合。複雑なコマンドに対応する場合。コマンドの再利用性を高めたい場合
Interpreter 構文を解析するものを作成する 文法を構文解析してチェックするような場合。与えた文字列をプログラムとして解釈し操作をさせたい場面(文字列でアプリの操作させたいような場面)
Iterator 集約したオブジェクトの列挙(実装など中身を見ずに集合の中にある要素に順にアクセスする) 実装など中身を見ずに集合の中にある要素に順にアクセスしたい場合
Mediator オブジェクト同士のやりとりをカプセル化するオブジェクトを作成し独立性を高める 複雑な方法でオブジェクト間でやりとりしている場合。オブジェクトが他のオブジェクトに対して相互参照しているような場合
Memento オブジェクトのバックアップを残して活用する オブジェクトのバックアップをとっておいて後で以前の状態に戻したい場合(undo)
Observer オブジェクトの状態を監視をして変更があれば通知する 変更を通知する場合
State オブジェクトの状態をクラスで表現する 状態に応じて処理を分けたい場合。オブジェクトが複数の条件分で分岐するような場合
Strategy クラスで分割した複数のアルゴリズム(戦略)で目標を実現する 状況に応じて戦略(アルゴリズム)を変えたい場合
Template Method 親クラス(インターフェース)で処理のステップを作成しておき、サブクラスで具体的な内容を定める 振る舞いが変わり得る部分の実装はサブクラスにしたい場合
Visitor 元クラスを変化させずにVisitorで機能を追加する 複数クラスをまたぎながら処理をする機能を作成したい場合

感想

今回は「振る舞いに関するデザインパターン」11種類のうち6種類についてPython3を使用して理解してみるチャレンジを行いました。初めてデザインパターンに触りましたが、こういったものを一度で理解するのは難しいと割り切って60点くらいの出来を目指しております。
今回、勉強するにあたり多くの参考資料を拝見させていただきました。どれも非常にわかりやすく理解が進みました!ありがとうございました。

デザインパターンを使用することを目的とせずに、これからも精進していければと思います。

参考にさせていただいたもの(学習教材)

  1. オブジェクト指向でなぜ作るのか? 3版
  2. 実践Python3
    一部引用させていただきました。著作権の問題があるので最低限の引用にとどめる形をとっています(できるかぎり引用しないようにしました)。興味がわいた方はせひ書籍を読んでください。
  3. Wikipedia
    各パターンのWikipediaを参照
  4. 機械学習 ベストプラクティス
    このHPのコンテンツはソースコードを含めて自由に使っていただいて結構です。というHP上の明言により多く引用させていただきました。情報も多くわかりやすくて感謝しております。
    自分が説明できてない部分も多いので、興味がわいた方はぜひ読んでみてください。
  5. design-patterns-for-humans
    CC BY 4.0に基づき文章の引用や、サンプルコード(PHP)をPythonコードに変換または編集しております。

イラスト素材

いらすとや

この記事(学習メモ)について

  • Zennのプラットフォーム上ですが、著者には収益化の意図は一切ありません。
  • 予告なく非公開になる場合があります。
  • 販売・転載・加工・引用など一切の行為を禁止いたします。(①個人の学習メモを公開しているだけで保証されたものでないため②引用元の力や先人の知恵で成り立っているため)

Discussion

ログインするとコメントできます