0
1

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.

Pythonでデザインパターンの勉強をしてみる # Strategyパターン

Posted at

はじめに

この記事では「Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本」という書籍を使ったデザインパターンの学習のメモを書いていきます。
書籍内ではJavaでコードが書かれていますが、それをPythonで真似てみることで、Pythonの学習にも役立てる予定です。デザインパターンもPythonも知識が浅いので間違っている部分があれば指摘していただけるとめちゃくちゃありがたいです。

問題の始まり

書籍内では、SimUDuckという鴨池シュミレーションゲームの開発会社に発生した問題を見ていきます。(SLG好きとしては、こんなゲームあったらちょっとやってみたい)

「鴨が[飛ぶ]機能を追加」しようとするところから問題が始まりました。
以下はDuckというスーパークラスを鴨サブクラスが継承している状態のコードです。

python
# すべてのカモが鳴いたり泳いだりするため、
# スーパークラスが実装コードを備える
class Duck():
    def quack(self):
        print('ガーガー')
    def swim(self):
        print('泳いでいます。')

    # すべてのカモサブタイプは異なるので、display()メソッドは書籍内では抽象メソッド
    # それぞれのカモサブタイプはスクリーン上での表示方法に関する振る舞いであるDisplay()について、独自の実装を担当します。
    def display(self):
        pass

    # カモに関する他のメソッドは以下に追加
    # ここにfly()メソッドを追加して解決しようとした。


class MallardDuck():
    def display(self):
        #マガモ(mallard)の表示
        print('本物のマガモです。')

class RedheadDuck():
    #アメリカホシハジロ(redhead)の表示
    def display(self):
        print('本物のアメリカホシハジロです。')

ここで、スーパークラスであるDuckにfly()メソッドを追加するだけで解決すると思われました。
正直私もそう思いました。。。

しかし、それではすべての鴨サブクラスがflyの機能をもってしまいます。飛べない鴨(ゴム製おもちゃの鴨)も飛ぶ機能を持ってしまうところが問題となりました。

解決方針

悪い解決策の例として、Flyableというインターフェースを使用し、それぞれの鴨サブクラスで振る舞いを実装するという方法でした。しかし、この方法だと同じ空を飛ぶ挙動を何度も書くことになるので却下されていました。これはそうだよねえってさらっとながしました。

そこで、変化に柔軟に変更も拡張もしやすいように設計するにはどうするか。

まず、プログラムの中の、不変の部分と変化する部分を分離させる。
そして、変化する部分を取り出し、変更しても不変部分に影響を及ぼさないようにする(カプセル化)。

そして次に

「実装に対してではなく、インターフェースに対してプログラミングする」
という設計原則がでてきました。
これを見た時は正直意味が分からず、あとからでてくる実装コードをみてやっと意味が分かりました。(たぶん)
悪い解決方法だと、インターフェースは使っているものの、実際の振る舞いは鴨サブクラスの実装のの中でプログラミングしています。そうではなく、インターフェースを実装するクラスを専用で作り、それをインターフェースを介して鴨サブクラスで使うみていなことだと思っています。(振る舞いを実装したクラスをインターフェースでWrapしているようなイメージだけど、うまく言語化できないのでおそらくまだ理解しきれていない)

「インターフェースに対するプログラミング」とは、「スーパータイプに対するプログラミング」を意味する

ともありましたが、わかったようでわかっていないような感じです。
個々の実装をスーパータイプのパーツ(HAS A)としておくことで、実際に実行するときに使うスーパータイプを介して個々の実装を使用するようになるというイメージでしょうか?

この時点で、FlyBehaviorインターフェースを実装する振る舞いクラス(FlyWithWings,FlyNoWay)を作ることで、鴨サブクラスがインターフェースで定義されたfly()メソッドを使うという実装を変えずに、実際の振る舞いを変更できることは何となく理解しました。
ただ、鴨サブクラスがどうやって個々の振る舞いクラスのうちの一つを選ぶんだ?という疑問が強くわいている状態でした。

その状態で読み進めたときのサンプルコードをPythonでなんとかやったのが以下です。

python
# STEP1 不変部分と変化する部分を整理
# fly()とquack()をのぞけばDuckクラスはうまく機能している

# STEP2 分離するために、分離部分を実装するクラス群を作成
# 今回は鳴くためのクラス群と飛ぶためのクラス群
# それぞれのクラス群には多様な振る舞いが実装される
# 例)ガーガーとなくクラス、キューキューとなくクラス、沈黙するクラス、etc..

# STEP3 分離部分のクラス群の設計する
# 設計原則:実装に対してではなく、インターフェースに対してプログラミングする
# 目的は柔軟性を保つこと
# インスタンスごとに特定の種類の飛ぶ振る舞いで初期化したい(動的に変更できた方がいい
# 振る舞いの「設定メソッド」をカモクラスに持たせて、インスタンス実行時に飛ぶ振る舞いを変更できるようにすべき

# それぞれの振る舞いを表すインターフェースを仕様する(FlyBehaviorとか
# 振る舞いのクラス群はこのインターフェースを実装する(インターフェースとは、Javaのソースでイメージつかむ


class FlyBehavior():
    def fly(self):
        pass

class QuackBehavior():
    def quack(self):
        pass

class Duck():
    flyBehavior = FlyBehavior()
    quackBehavior = QuackBehavior()

    def performFly(self):
        self.flyBehavior.fly(); 

    # Duckオブジェクトは鳴く振舞い自体を処理せず、その振る舞いをquackBehaviorで
    # 参照されるオブジェクトに委譲する(参照先はいつきまる?)
    def performQuack(self):
        self.quackBehavior.quack(); 


    def swim(self):
        pass

    # すべてのカモサブタイプは異なるので、display()メソッドは抽象メソッド
    # それぞれのカモサブタイプはスクリーン上での表示方法に関する振る舞いであるDisplay()について、独自の実装を担当します。
    @abstractmethod
    def display(self):
        pass

class FlyWithWings(FlyBehavior):
    def fly(self):
        print('飛んでいます')

class FlyNoWay(FlyBehavior):
    def fly(self):
        print('飛べません')

class Quack(QuackBehavior):
    def quack(self):
        print('ガーガー')

class Squeak(QuackBehavior):
    def quack(self):
        print('キューキュー')

class MuteQuack(QuackBehavior):
    def quack(self):
        print('<<沈黙>>')


# flyBehavior,quackBehaviorインスタンス変数の設定方法を考える
class MallardDuck(Duck):
    def __init__(self):
        self.quackBehavior  = Quack()
        # MallardDuckはFlyWithWingsクラスを使用して飛ぶ振る舞いを処理しているので、
        # performFly()が呼び出されると、振る舞いの責務をFlyWithWingsオブジェクトに委譲し、実際の振る舞いを取得する
        # でもこれ実装に対してプログラミングしてない??、具象実装クラスのインスタンスをコンストラクタで作成してる!
        self.flyBehavior    = FlyWithWings()
    def display(self):
        #マガモ(mallard)の表示
        print('本物のマガモです')

class RedheadDuck(Duck):
    #アメリカホシハジロ(redhead)の表示
    def display(self):
        pass

#問題
# Duckスーパークラスにflyメソッドを追加したため、空飛ぶゴム製のカモができてしまった
# 継承の問題点:再利用には良いが、保守的にはあまり適切でない
# fly()をオーバーライドして何もしないようにする方法はあるが、
# 木製のカモ人形を追加した時にはquack(),fly()をオーバーライドする必要がある

# fly(),quack()は変更がある部分として、Duckクラスから除き、Flyable()インターフェースの実装という手段の検討
# これは48種類のカモサブクラスがあったとしたら、何度もインターフェースの実装をしないといけない(重複コード)

# 設計原則
# アプリケーション内の変化する部分を特定し、不変な部分と分離する


# Duckコードのテスト
if __name__ == '__main__':
    mallard = MallardDuck()
    mallard.performQuack()
    mallard.performFly()
    print('終了')

鴨サブクラスのコンストラクタでどの振る舞い決めてるけどいいの!?実装に対してプログラミングしてない!?ってなりました。
そう思ったら同じようなことが次のページで書かれていたので安心しました。

インターフェースの実装クラス代入を動的に行う

ソースとしては以下が完成形です。

python

class FlyBehavior():
    def fly(self):
        pass

class QuackBehavior():
    def quack(self):
        pass

class Duck():
    flyBehavior = FlyBehavior()
    quackBehavior = QuackBehavior()

    # 振る舞いを動的に設定できるようにする
    # コンストラクタでインスタンス化するのではなく、設定メソッドで変更する
    def setFlyBehavior(self, FlyBehavior):
        self.flyBehavior = FlyBehavior

    def setQuackBehavior(self, QuackBehavior):
        self.quackBehavior = QuackBehavior

    def performFly(self):
        self.flyBehavior.fly()

    # Duckオブジェクトは鳴く振舞い自体を処理せず、その振る舞いをquackBehaviorで
    # 参照されるオブジェクトに委譲する(参照先はいつきまる?)
    def performQuack(self):
        self.quackBehavior.quack()
    
    
    def swim(self):
        pass
    
    # すべてのカモサブタイプは異なるので、display()メソッドは抽象メソッド
    # それぞれのカモサブタイプはスクリーン上での表示方法に関する振る舞いであるDisplay()について、独自の実装を担当します。
    @abstractmethod
    def display(self):
        pass

class FlyWithWings(FlyBehavior):
    def fly(self):
        print('飛んでいます')

class FlyNoWay(FlyBehavior):
    def fly(self):
        print('飛べません')

class FlyRocketPowered(FlyBehavior):
    def fly(self):
        print('ロケットで飛んでいます!')

class Quack(QuackBehavior):
    def quack(self):
        print('ガーガー')

class Squeak(QuackBehavior):
    def quack(self):
        print('キューキュー')

class MuteQuack(QuackBehavior):
    def quack(self):
        print('<<沈黙>>')


# flyBehavior,quackBehaviorインスタンス変数の設定方法を考える
class MallardDuck(Duck):
    def __init__(self):
        self.quackBehavior  = Quack()
        # MallardDuckはFlyWithWingsクラスを使用して飛ぶ振る舞いを処理しているので、
        # performFly()が呼び出されると、振る舞いの責務をFlyWithWingsオブジェクトに委譲し、実際の振る舞いを取得する
        # でもこれ実装に対してプログラミングしてない??、具象実装クラスのインスタンスをコンストラクタで作成してる!
        self.flyBehavior    = FlyWithWings()
    def display(self):
        #マガモ(mallard)の表示
        print('本物のマガモです')

class ModelDuck(Duck):
    def __init__(self):
        self.quackBehavior  = Quack()
        # MallardDuckはFlyWithWingsクラスを使用して飛ぶ振る舞いを処理しているので、
        # performFly()が呼び出されると、振る舞いの責務をFlyWithWingsオブジェクトに委譲し、実際の振る舞いを取得する
        # でもこれ実装に対してプログラミングしてない??、具象実装クラスのインスタンスをコンストラクタで作成してる!
        self.flyBehavior    = FlyNoWay()

    def display(self):
        print('模型のカモです。')

class RedheadDuck(Duck):
    #アメリカホシハジロ(redhead)の表示
    def display(self):
        pass


# Duckコードのテスト
if __name__ == '__main__':
    mallard = MallardDuck()
    mallard.performQuack()
    mallard.performFly()

    model = ModelDuck()
    model.performFly()
    model.setFlyBehavior(FlyRocketPowered())
    model.performFly()
    print('終了')
結果
ガーガー
飛んでいます
飛べません
ロケットで飛んでいます!
終了

ここで、それぞれの鴨サブクラスは飛ぶ・鳴くの振る舞いを継承する代わりに、適切な振る舞いオブジェクトで構成(compose)されることで振る舞いを取得しています。
これが設計原則として以下のように書かれていました。
「継承よりコンポジションを好む」

継承は「IS A」、コンポジションは「HAS A」として書かれているようでした。
MallardDuck IS A Duck
MallardDuck HAS A FlyBehavior
という感じですかね。

まとめ

オブジェクト指向の継承には再利用性というメリットはあるが、開発後の保守・変更の面では欠点がある。それを補うためにもデザインパターンは有用なのかなと。
個人的には、「おお~~なるほど!」「あああ、そういうことなのか!」と一人楽しみながらできたので良かったです。
ただ、PythonにはJavaのインターフェースと同等の機能はないようだったので結構適当にしてしまいました。Duckも書籍内では抽象くらすになっていたのですが、そうしなくても結果は同じになってしまって放置してしまっています。そのあたりをちゃんと調べていきたいなあと思います。

かなり走り書きでここまで一気に書いてしまったので、コード内のコメントが本を読みながら書いている内容そのままだったりしてかなり見にくいので、あとから整理したいなあとも思いますね、、
UMLを追加すると見返したときにかなり思い出しやすくなりそうかなあ

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?