📝

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

2022/05/22に公開約17,700字

この記事について

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

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

概要

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

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

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?"
(機械翻訳)これは、オブジェクト間の責任の分担に関係する。構造パターンと異なるのは、単に構造を指定するだけでなく、オブジェクト間のメッセージの受け渡しや通信のパターンも概説している点である。言い換えれば、"ある振る舞いをソフトウェアコンポーネントで実行するにはどうすればよいか?"に答える手助けをするものである。

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

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

Chain of Responsibilityパターン

利用場面

4より引用

GoFによれば,Chain of Responsibility パターンの目的は, 「1つ以上のオブジェクトに要求を処理する機会を与えることにより,要求を送信するオブジェクトと受信するオブジェクトの結合を避ける。受信する複数のオブジェクトをチェーン状につなぎ,あるオブジェクトがその要求を処理するまで,そのチェーンに沿って要求を渡していく。」
GoFによれば,Chain Of Responsibility パターンは,次のような場合に使うことができる。
・要求を処理するオブジェクトの候補が複数存在し,最終的にどのオブジェクトが担当するのかは,前もってわからない場合。担当オブジェクトは自動的に決められる。
・受け手を明確にせずに,複数あるオブジェクトの1つに対して要求を発行したい場合。
・要求を処理することができるオブジェクトの集合が動的に明確化される場合。

概要は「個々の処理をチェーン状に繋いで、チェーンに従い各担当が確認処理をしていく」です。
利用シーンとしては「複数の処理をつなげて1つの処理としたい場合。事前に最終担当が決まってない場合」です。

具体的に考えてみる

6の引用元を参考にしております。
ブランド品を購入するとします。代金は購入フローのようになっており銀行からの引き落としで完結するとします(架空の支払い方法です)。100万円を引き落とせる口座がどこにあるかを探しています。

「A銀行で確認->B銀行で確認->C銀行で確認」・・と確認処理がチェーン状に続き、引き落としができる口座を探しつつけます。また、各銀行が口座残高を確認するのは、その銀行の担当の責任です。

このようにチェーン状に繋いで、チェーンに従い各担当が確認処理をしていく(責任の連鎖で1つの責任として完成する)パターンが「Chain of Responsibilityパターン」です。

作成方法

    1. Handlerを作る。要求を処理するインターフェースです。自分自身のインスタンスを利用します。
    1. ConcreteHandlerを作る。 Handlerを継承した具像クラスです。

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

銀行の例で作成します。
Handlerを作成し、ConcreteHandlerとしてA銀行とB銀行を作成しました。
HandlerはHandlerを利用する形となります。

python3で書いてみる

from abc import ABC,abstractmethod

class Handler(ABC):
    handle = None

    def __init__(self,bank_name,balance):
        self.bank_name = bank_name
        self.balance = balance

    def set_next(self, handler):
        self.handle = handler
        return handler

    def next(self, price: int):
        if self.check(price):
            self.complete(price)
        else:
            self.error()     
            if self.handle:
                return self.handle.next(price)

    @abstractmethod
    def check(self,price: int):
        pass

    @abstractmethod
    def error(self,price: int):
        pass

    def complete(self, price):
        balance = int(self.balance) - price
        print('{0}銀行で支払いが完了しました。残高は{1}です'.format(self.bank_name,balance))

class AbankBalanceCheckHandler(Handler):
    '''ConcreteHandler
    '''
    def check(self, price):
        if int(self.balance) >= price:
            return True
        else:
            return False
    def error(self):
        print('A銀行で支払いが出来ませんでした')


class BbankBalanceCheckHandler(Handler):
    '''ConcreteHandler
    '''
    def check(self, price):
        if int(self.balance) >= price:
            return True
        else:
            return False
    def error(self):
        print('B銀行で支払いが出来ませんでした')

item_price = 1000000

a_bank_handler = AbankBalanceCheckHandler('A', 10000) # 残高
b_bank_handler = BbankBalanceCheckHandler('B', 1000000) # 残高

a_bank_handler.set_next(b_bank_handler)
a_bank_handler.next(item_price)

実行結果

A銀行で支払いが出来ませんでした
B銀行で支払いが完了しました。残高は900001です

Commandパターン

利用場面

4より引用

GoFによれば,Command パターンの目的は, 「要求をオブジェクトとしてカプセル化することによって,異なる要求や,要求からなるキューやログにより,クライアントをパラメータ化する。また,取り消し可能なオペレーションをサポートする。」
GoFによれば,次のような場合に Command パターンを使用することができる。
・先にあげた MenuItem オブジェクトのように,実行する動作によりオブジェクトをパラメータ化したい場合。手続き型言語では,そのようなパラメータ化をコールバック関数を使って表現する。すなわち,コールバック関数を,呼び出してほしいところに登録しておく,という形になる。Command パターンでは,そのようなコールバック関数の代わりにオブジェクトを使う。
(以下省略)

2より引用

Commandパターンは、コマンド(命令)をオブジェクトとしてカプセル化するために用いられる。これにより、例えば、遅延実行を行うための一連のコマンドを構築したり、取り消し可能なコマンドを作ったりできる。

概要は「コマンド(要求/命令)をオブジェクトとしてカプセル化する」です。
利用シーンとしては「多種多様なコマンドに対応する場合。複雑なコマンドに対応する場合。コマンドの再利用性を高めたい場合」です。

具体的に考えてみる

6より引用

Real world example
A generic example would be you ordering food at a restaurant. You (i.e. Client) ask the waiter (i.e. Invoker) to bring some food (i.e. Command) and waiter simply forwards the request to Chef (i.e. Receiver) who has the knowledge of what and how to cook. Another example would be you (i.e. Client) switching on (i.e. Command) the television (i.e. Receiver) using a remote control (Invoker).

(機械翻訳)現実世界の例
一般的な例としては、レストランで料理を注文することが挙げられます。あなた(=Client)はウェイター(=Invoker)に料理を持ってくるように頼み(=Command)、ウェイターは何をどう料理するかの知識を持つシェフ(=Receiver)にその依頼を転送するだけでいいのです。また、あなた(=Client)がリモコン(=Invoker)を使ってテレビ(=Receiver)のスイッチを入れる(=Command)のも良い例でしょう。

どちらの例も非常にわかりやすく補足することはないので図表は省略します。

作成方法

    1. Receiverを作成する。Commandの要求/命令を実行する対象
    1. Commandを作成する。要求/命令を定義した。インターフェース
    1. ConcreteCommandを作成する。Commandの具像クラス。Reciverを利用する
    1. Invokerを作成する。Commandを呼び出す役目のクラス

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

6引用元にある、お客がウェイターに料理を注文し、ウェイターはシェフに依頼を出すことを考えます。
Receiverはシェフです。Commandは注文(命令)とし、ConcreteCommandは料理の注文とします。ウェイターはInvokerでCommandを呼び出します。

python3で書いてみる

from abc import ABC, abstractmethod

class Shef:
    '''Receiver
    '''    
    def order(self, order):
        print('{}を作成します'.format(order))

class Command(ABC):
    '''Command(インターフェース)
    '''
    @abstractmethod
    def execute(self):
        pass

class Order(Command):
    ''''ConcreteCommand
    '''
    def __init__(self):
        self.shef = Shef()

    def execute(self, order):
        self.shef.order(order)

class Waiter:
    '''Invoker
    '''
    def __init__(self, order_commands: Order):
        self.order_commands = order_commands
    
    def order_command(self, order):
        self.order_commands.execute(order)

order = Order()
waiter = Waiter(order)
waiter.order_command('オムライス')

実行結果

オムライスを作成します

Interpreterパターン

利用場面

4より引用

GoFによれば,Interpreter パターンの目的は, 「言語に対して,文法表現と,それを使用して文を解釈するインタプリタを一緒に定義する。」
GoFによれば,Interpreter パターンは,次のような場合に使うことができる。
ステートメントがアブストラクト・シンタックスツリーとして表現できるような言語を解釈する際に Interpreter パターンを使う。(〜省略)

Intepreterとは通訳者の意味です。プログラムの実行方法などにも使用される言葉でもあります。プログラムなど言語の構文解析(構文が正しいかのチェック)をするものを生成する時に活用できるパターンです。
デザインパターンとして記載がないケースあります。利用されることが少ないのかもしれません。

概要は「構文を解析するものを作成する」です。
利用シーンとしては「文法を構文解析してチェックするような場合。与えた文字列をプログラムとして解釈し操作をさせたい場面(文字列でアプリの操作させたいような場面)」です。

Interpreterパターンを具体例を示して作成するのに良い例が思い浮かばないため、以下は省略させていただきます。

  • 具体的に考えてみる
  • 作成方法
  • オブジェクト思考による作成方法のイメージ
  • python3で書いてみる

ご興味のある方は、引用元4にJavaで書かれたデザインパターンをPythonで表現したものがダウンロードできるようになっているので、そちらを参考にしていただくのが良いと思います。

Iteratorパターン

利用場面

GoFによれば,Iterator パターンの目的は, 「集約オブジェクトが基にある内部表現を公開せずに,その要素に順にアクセスする方法を提供する。」 (~中略~)
GoFによれば,次のような場合に Iterator パターンを使うとよい。
・コンテナオブジェクトの内部表現を公開せずに,その中にあるオブジェクトにアクセスしたい場合。
・コンテナオオブジェクトに対して,複数の走査をサポートしたい場合。
・異なるコンテナオ構造の走査に対して,単一のインタフェースを提供したい(すなわち,ポリモルフィックな iteration をサポートしたい)場合。

概要は「集約したオブジェクトの列挙(実装など中身を見ずに集合の中にある要素に順にアクセスする)」です。
利用シーンとしては「実装など中身を見ずに集合の中にある要素に順にアクセスしたい場合」です。

具体的に考えてみる

本棚(集合)に収納された本(要素)を順番に取り出すイメージです。

作成方法

    1. Aggregateを作成する。インターフェース。イテレータを作成できるメソッドを持っている
    1. ConcreteAggregateを作成する。Aggregateの具像クラス。イテレータを作成できる
    1. Iteratorを作成する。インターフェース、要素を順番にアクセスするメソッドをもつ
    1. ConcreteIteratorを作成する。 Iteratorの具像クラス

Pythonには標準ライブラリで、Iteratorを作成できる機能が備わっている。
通常は標準ライブラリで書くのが良いと思います(一般的だし、コード量が少なくなるので)

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

本棚を例にして、本棚から本を取り出してみます。
緑枠がAggregateとConcreteAggregateでイテレータを作る側です。
赤枠がIteratorとConcreteIteratorで次の要素を返したり、次の要素があるかを調べます。

ConcreteAggregateをインスタンス化して、自信の持っているiterator()メソッドから自身のイテレータを作成する。そして、作成したイテレータのnext()とhasNext()を利用して要素にアクセスします。

python3で書いてみる

from abc import ABC, abstractmethod

class Book:
    '''本の要素
    '''
    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

class Aggregate(ABC):
    '''Aggregate(インターフェース)
    '''
    @abstractmethod
    def iterator(self):
        pass

class BookShelf(Aggregate):
    '''ConcreteAggregate
    '''
    def __init__(self):
        self.__book = []

    def get(self, index):
        return self.__book[index]

    def add(self, book: Book):
        self.__book.append(book)

    def iterator(self):
        return BookShelfIterator(self)

    def get_length(self):
        return len(self.__book)

class Iterator:
    '''Iterator
    '''
        
    @abstractmethod
    def next(self):
        pass

    @abstractmethod
    def has_next(self):
        pass


class BookShelfIterator(Iterator):
    '''ConcreteIterator
    '''
    def __init__(self, book_shelf: BookShelf):
        self.__book_shelf = book_shelf
        self.__index = 0

    def next(self):
        book = self.__book_shelf.get(self.__index)
        self.__index += 1
        return book

    def has_next(self):
        if self.__index < self.__book_shelf.get_length():
            return True
        else:
            return False

bookshelf = BookShelf()
bookshelf.add(Book('Hoge 1'))
bookshelf.add(Book('Hoge 2'))
bookshelf.add(Book('Hoge 3'))
bookshelf.add(Book('Hoge 4'))
bookshelf.add(Book('Hoge 8'))
bookshelf.add(Book('Hoge 10'))

book_iterator = bookshelf.iterator()

while(book_iterator.has_next()):
    book = book_iterator.next()
    print(book.get_name())

実行結果

Hoge 1
Hoge 2
Hoge 3
Hoge 4
Hoge 8
Hoge 10

python3で書いてみる(標準ライブラリver)

from collections.abc import Iterable,Iterator

class Book:
    '''本の要素
    '''
    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

class BookShelf(Iterable):
    '''ConcreteAggregate
    '''
    def __init__(self):
        self.__book = []

    def get(self, index):
        return self.__book[index]

    def add(self, book: Book):
        self.__book.append(book)

    def __iter__(self):
        '''イテレータに該当
        '''
        return BookShelfIterator(self)

class BookShelfIterator(Iterator):
    '''ConcreteIterator
    '''
    def __init__(self, book_shelf: BookShelf):
        self.__book_shelf = book_shelf
        self.__index = 0

    def __next__(self):
        try:
            book = self.__book_shelf.get(self.__index)
            self.__index += 1
        except IndexError:
            raise StopIteration
        return book

bookshelf = BookShelf()
bookshelf.add(Book('Hoge 1'))
bookshelf.add(Book('Hoge 2'))
bookshelf.add(Book('Hoge 3'))
bookshelf.add(Book('Hoge 4'))
bookshelf.add(Book('Hoge 8'))
bookshelf.add(Book('Hoge 10'))

for book in bookshelf:
    print(book.get_name())

from collections.abc import Iterable,Iteratorは使用しなくてもかけます。利用するメリットは__next__や__iter__のメソッドを書くことを強制できる点です。

実行結果

Hoge 1
Hoge 2
Hoge 3
Hoge 4
Hoge 8
Hoge 10

Mediatorパターン

利用場面

4より引用

GoFによれば,Mediator パターンの目的は, 「オブジェクト群の相互作用をカプセル化するオブジェクトを定義する。Mediator パターンは,オブジェクト同士がお互いを明示的に参照し合うことがないようにして,結合度を低めることを促進する。それにより,オブジェクトの相互作用を独立に変えることができるようになる。 」
GoFによれば,Mediator パターンは次のような場合に使う。
・しっかりと定義されているが複雑な方法で,オブジェクトの集まりが通信する場合。その結果,オブジェクト間の依存関係が構造化できず,理解が難しい。
・あるオブジェクトが他の多くのオブジェクトに対して参照を持ち,それらと通信するので,それを再利用するのが難しい場合。
・いくつかのクラス間に分配された振る舞いを,できるだけサブクラス化を行わずにカスタマイズしたい場合。

Mediatorという言葉は、仲裁人の意味です。
目的を見てもらうとわかるのですが、オブジェクト同士のやりとりをカプセル化するオブジェクトを作成し独立性を高めることにあります。

概要は「オブジェクト同士のやりとりをカプセル化するオブジェクトを作成し独立性を高める」です。利用シーンとしては「複雑な方法でオブジェクト間でやりとりしている場合。オブジェクトが他のオブジェクトに対して相互参照しているような場合」です。

補足:Mediatorはミディエーターと読みます。あとでColleagueが出てきますが、こちらは同僚の意味でコリーグズと読みます。

具体的に考えてみる

6より引用

Real world example
A general example would be when you talk to someone on your mobile phone, there is a network provider sitting between you and them and your conversation goes through it instead of being directly sent. In this case network provider is mediator.

(機械翻訳)現実世界の例
一般的な例としては、携帯電話で誰かと話すとき、あなたと相手の間にネットワークプロバイダーが存在し、あなたの会話は直接送信されるのではなく、ネットワークプロバイダーを経由して行われます。この場合、ネットワークプロバイダーが仲裁役となります。

オブジェクト指向の世界の話は現実世界に落とし込むのが難しいです。現実世界に落とすならば↑のようなプロバイダーの役目が仲裁役(Mediator)のイメージになるということです。

作成方法

    1. Mediatorを作成する。Colleagueと通信するインターフェース
    1. ConcreateMediatorを作成する。Mediatorの具像クラスでConcreateColleagueを調整する
    1. Colleagueを作成する。Mediatorと通信するインターフェース
    1. ConcreateColleagueを作成する。Colleagueの具像クラスで仲介される役(処理を仲裁される)

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

6引用元にある、チャットルームと互いにメッセージを送信するユーザーの例を参考にさせていただきます。

今チャットルームがあって AndroidユーザーとiOSユーザーが参加できるとします。

チャットルームがMediator役、ユーザーがColleague役です。
Colleagueは属性としてMediatorをもちます。MediatorはColleagueの状態に応じて処理をします。

python3で書いてみる

from abc import ABC, abstractmethod
from datetime import date

class User(ABC):
    '''Colleague(インターフェース)
    '''
    def __init__(self, name, message=None):
        self.name = name
        self._message = message
        
    @property
    def set_message(self):
        return self._message

    @set_message.setter
    def set_message(self, message):
        self._message = message

    @abstractmethod
    def get_name(self):
        pass

    @abstractmethod
    def send(self, msg):
        pass


class AndroidUser(User):
    '''ConcreateColleague1
    '''
    def get_name(self):
        return self.name
        
    def send(self, msg):
        self.set_message = msg
        return self._message

class iOSUser(User):
    '''ConcreateColleague2
    '''
    def get_name(self):
        return self.name

    def send(self, msg):
        self.set_message = msg
        return self._message

class Mediator(ABC):
    '''Mediator(インターフェース)
    '''
    @abstractmethod
    def show_msg(self, message):
        pass

class ChatRoom(Mediator):
    '''ConcreateMediator
    '''
    def __init__(self, android_user:AndroidUser, ios_user:iOSUser):
        self.android_user = android_user
        self.ios_user = ios_user
        android_user = self
        ios_user = self

    def show_msg(self):
        time = date.today()
        sender = self.android_user.get_name()
        print('日付{0}。ユーザー名{1}'.format(time,sender))
        message = self.android_user.send(self.android_user._message)
        print('メッセージ:{}'.format(message))

        sender = self.ios_user.get_name()
        print('日付{0}。ユーザー名{1}'.format(time,sender))
        message = self.ios_user.send(self.ios_user._message)
        print('メッセージ:{}'.format(message))

hoge = AndroidUser('hoge')
fuga = iOSUser('fuga')
hoge.set_message = 'HELLO'
fuga.set_message = 'YO'
chatroom_mediator = ChatRoom(hoge, fuga)
chatroom_mediator.show_msg()

実行結果

日付2022-05-21。ユーザー名hoge
メッセージ:HELLO
日付2022-05-21。ユーザー名fuga
メッセージ:YO

まとめ

パターン名称 概要 利用シーン
Chain of Responsibility 個々の処理をチェーン状に繋いで、チェーンに従い各担当が確認処理をしていく 複数の処理をつなげて1つの処理としたい場合。事前に最終担当が決まってない場合
Command コマンド(要求/命令)をオブジェクトとしてカプセル化する 多種多様なコマンドに対応する場合。複雑なコマンドに対応する場合。コマンドの再利用性を高めたい場合
Interpreter 構文を解析するものを作成する 文法を構文解析してチェックするような場合。与えた文字列をプログラムとして解釈し操作をさせたい場面(文字列でアプリの操作させたいような場面)
Iterator 集約したオブジェクトの列挙(実装など中身を見ずに集合の中にある要素に順にアクセスする) 実装など中身を見ずに集合の中にある要素に順にアクセスしたい場合
Mediator オブジェクト同士のやりとりをカプセル化するオブジェクトを作成し独立性を高める 複雑な方法でオブジェクト間でやりとりしている場合。オブジェクトが他のオブジェクトに対して相互参照しているような場合

感想

今回は「振る舞いに関するデザインパターン」11種類のうち5種類について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

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