目的
これはHead Firstデザインパターン 第2版 ―頭とからだで覚えるデザインパターンの基本に出てくる内容について、
サンプルコードをPythonで補足した記事になります。(本書ではJavaで書かれているため)
本書との併用を前提としています。
第1章
第一章では最初のデザインパターンであるStrategyについてに開設されています。
本書では次のように説明されています。
Strategyパターンは一連のアルゴリズムを定義してカプセル化し、交換できるようにします。Strategyパターンを使うと、
アルゴリズムを利用するクライアントとは独立してアルゴリズムを変更できます
かみ砕くと、こんな感じですかね。
この原則では、複数のアルゴリズムを定義し、実行時に必要に応じて適切なアルゴリズムを選択することができます。このような手法によって、機能や振る舞いをより柔軟に変更できるようになります。また、Strategyパターンは、アルゴリズムの切り替えに関連するコードの複雑さを減らすことができます。
また、Strategyパターン主に以下の3つのコンポーネントで構成される
- Context: 現在のStrategyオブジェクトを保持するクラス。クライアントはこのクラスを通じてStrategyを利用します。
- Strategy: すべての具体的なStrategyが実装するインターフェースや抽象クラス。
- ConcreteStrategy: Strategyインターフェースを実装する各具体的なクラス。これらは具体的なアルゴリズムや方法を提供します。
では、さっそく内容についてみていきましょう。
それは簡単なSimUDuckアプリから始まった
ジョーが最初に用意したDuckクラス
本書P37のジョーがカモ池シミュレーションゲームのために用意したDuckクラスになります。
from abc import ABC, abstractmethod
# スーパークラスの定義
class Duck(ABC):
# カモが泳ぐ振る舞いの実装
def swim(self):
print("泳ぐよ!")
# カモが飛ぶ振る舞いの実装
def quack(self):
print("ガーガー!")
@abstractmethod
def display(self):
# 抽象メソッド
raise NotImplementedError
# カモサブクラス(マガモ)
class MallardDuck(Duck):
def display(self):
print("マガモを表示するよ!")
# カモサブクラス(赤毛ガモ)
class RedheadDuck(Duck):
def display(self):
print("赤毛ガモを表示するよ!")
カモが飛ぶようになり、ゴムのアヒルまで飛び回るように。。。(本書P38,39)
本書ではカモが空を飛ぶ必要が出てきます。
そこで単純にDuckクラスにflyメソッドを追加しています。同時にゴムのアヒルのサブクラスRubberDuckも追加します。
するとどうでしょう。ゴムのアヒルまで飛べてしまいます!!
# 簡単なSimUDuckアプリから始まった
from abc import ABC, abstractmethod
# スーパークラスの定義
class Duck(ABC):
# カモが泳ぐ振る舞いの実装
def swim(self):
print("泳ぐよ!")
# カモが鳴く振る舞いの実装
def quack(self):
print("ガーガー!")
@abstractmethod
def display(self):
# 抽象メソッド
raise NotImplementedError
# カモが飛ぶ振る舞いの実装 ------新規実装------
def fly(self):
print("飛ぶよ!")
# カモサブクラス(マガモ)
class MallardDuck(Duck):
def display(self):
print("マガモを表示するよ!")
# カモサブクラス(赤毛ガモ)
class RedheadDuck(Duck):
def display(self):
print("赤毛ガモを表示するよ!")
# カモサブクラス(ゴムのアヒル) ------新規実装------
class RubberDuck(Duck):
def quack(self):
# 鳴く振る舞いをオーバーライド
print("キューキュー!")
def display(self):
print("ゴムのアヒルを表示するよ!")
rubber_duck = RubberDuck()
rubber_duck.quack() # "キューキュー!"
### ゴムのアヒルが飛べてしまう!!!! ###
rubber_duck.fly() # 飛ぶよ!
以下のようにRubberDuckクラス内でflyメソッドをオーバーライドし、何もしないようにすればいいかもしれませんが、
それではカモサブクラスを実装するたびに、カモ毎に設定が必要になってしまいますね。。
# flyメソッドをオーバーライドし、何もしないようにする
class RubberDuck(Duck):
def fly(self):
pass
振る舞いをインターフェースに(本書P41)
上記問題を解決するため、書籍ではインターフェースを利用しています。
以下はカモが空を飛ぶ振る舞いと鳴く振る舞いをそれぞれFlyable、Quackableというインターフェース化しています。
from abc import ABC, abstractmethod
# 飛ぶ振る舞いのインターフェース ------新規実装------
class Flyable(ABC):
@abstractmethod
def fly(self):
raise NotImplementedError
# 鳴く振る舞いのインターフェース ------新規実装------
class Quackable(ABC):
@abstractmethod
def quack(self):
raise NotImplementedError
# スーパークラスの定義
class Duck(ABC):
def swim(self):
# 泳ぐよ
print("泳ぐよ!")
@abstractmethod
def display(self):
# 抽象メソッド
raise NotImplementedError
# カモサブクラス(マガモ)
class MallardDuck(Duck, Flyable, Quackable):
def display(self):
print("マガモを表示するよ!")
# 実際の飛ぶ振る舞い
def fly(self):
print("飛ぶよ!")
# 実際の鳴く振る舞い
def quack(self):
print("ガーガー!")
# カモサブクラス(赤毛ガモ)
class RedheadDuck(Duck, Flyable, Quackable):
def display(self):
print("赤毛ガモを表示するよ!")
def fly(self):
print("飛ぶよ!")
def quack(self):
print("ガーガー!")
# カモサブクラス(ゴムのアヒル)
class RubberDuck(Duck, Quackable):
def display(self):
print("ゴムのアヒルを表示するよ!")
def quack(self):
print("キューキュー!")
# カモサブクラス(ダミーダック)
class DecoyDuck(Duck):
def display(self):
print("ダミーを表示するよ!")
こうすることで、飛ぶ振る舞い(鳴く振る舞い)をするカモサブクラスにだけ取り入れればよくなりますね。
しかし、ここで新たな問題が発生します。もしカモの飛ぶ振る舞いに(ロケットで飛ぶように)変更がかかった場合はどうなるでしょう。
Flyableインターフェースを使っているカモサブクラスすべてに対して変更をかける必要が出てきます。
(本書P42でも女性が同じことを言っていますね。)
振る舞いを実装する(本書P48)
次に、インターフェースで表された振る舞いを使用するため、振る舞いの実際の実装をしています。
※インターフェース名が変更されているので注意
from abc import ABC, abstractmethod
# FlyBehaviorという抽象クラスを定義し、fly()メソッドを抽象メソッドとして定義しています。
# 飛ぶ振る舞いをするためのインターフェース
class FlyBehavior(ABC):
@abstractmethod
def fly(self):
raise NotImplementedError
# QuackBehaviorという抽象クラスを定義し、quack()メソッドを抽象メソッドとして定義しています。
# 鳴く振る舞いをするためのインターフェース
class QuackBehavior(ABC):
@abstractmethod
def quack(self):
raise NotImplementedError
# スーパークラスの定義
class Duck(ABC):
def swim(self):
# 泳ぐよ
print("泳ぐよ!")
@abstractmethod
def display(self):
# 抽象メソッド
raise NotImplementedError
# 飛ぶ振る舞いの実装
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("<<沈黙>>")
# カモサブクラス(マガモ)
class MallardDuck(Duck, FlyWithWings, Quack):
def display(self):
print("マガモだよ!")
# カモサブクラス(赤毛ガモ)
class RedheadDuck(Duck, FlyWithWings, Quack):
def display(self):
print("赤毛ガモだよ!")
# カモサブクラス(ゴムのアヒル)
class RubberDuck(Duck, FlyNoWay, Squeak):
def display(self):
print("ゴム製ガモだよ!")
# カモサブクラス(ダミーダック)
class DecoyDuck(Duck, FlyNoWay, MuteQuack):
def display(self):
print("ダミーガモだよ!")
これで、既存の振る舞いの変更に強い設計になりました。
また、新しい振る舞いを追加する際も、既存の振る舞いやスーパークラスのDuckクラスに影響は出なくなります。
カモの振る舞いを統合する
本書P50にあるように、最後にカモの振る舞いをDuckクラスに統合します。
といっても、Duckクラスにまたflyメソッドなどを追加しては元に戻ってしまいます。
なので、これらのメソッドを使用する代わりに、飛ぶ(鳴く)振る舞いに譲渡します。
from abc import ABC, abstractmethod
# FlyBehaviorという抽象クラスを定義し、fly()メソッドを抽象メソッドとして定義しています。
# 飛ぶ振る舞いをするためのインターフェース
class FlyBehavior(ABC):
@abstractmethod
def fly(self):
pass
# QuackBehaviorという抽象クラスを定義し、quack()メソッドを抽象メソッドとして定義しています。
# 鳴く振る舞いをするためのインターフェース
class QuackBehavior(ABC):
@abstractmethod
def quack(self):
pass
class Duck(ABC):
# fly_behaviorとquack_behaviorはFlyBehaviorクラスとQuackBehaviorクラスのインスタンスであることを示します。
# すべてのカモサブクラスはこの2つの変数を継承します。
fly_behavior: FlyBehavior = None
quack_behavior: QuackBehavior = None
def __init__(self):
pass
# display()メソッドはabstractmethodとして定義されており、派生クラスで実装する必要があります。
@abstractmethod
def display(self):
pass
# perform_fly()メソッドで、Duckクラスに指定されたfly_behavior(FlyBehavior)の飛行を実行します。
# 飛ぶ振る舞いを実行するには、Duckはfly_behaviorが参照するオブジェクトに飛ぶように依頼するだけでよいです。
def perform_fly(self):
self.fly_behavior.fly()
# perform_quack()メソッドで、Duckクラスに指定されたquack_behavior(QuackBehavior)の鳴き声を出力します。
# 鳴く振る舞いを実行するには、Duckはquack_behaviorが参照するオブジェクトに鳴くように依頼するだけでよいです。
def perform_quack(self):
self.quack_behavior.quack()
def swim(self):
print("すべてのカモは浮きます。おとりのカモでも!")
# 飛ぶ振る舞いをする実装クラス
class FlyWithWings(FlyBehavior):
def fly(self):
print("飛んでいます!!")
# 飛べない振る舞いをする実装クラス
class FlyNoWay(FlyBehavior):
def fly(self):
print("飛べません")
# ガーガー鳴く振る舞いをする実装クラス
class Quack(QuackBehavior):
def quack(self):
print("ガーガー")
# 鳴かない振る舞いをする実装クラス
class MuteQuack(QuackBehavior):
def quack(self):
print("<<沈黙>>")
# キューキュー鳴く振る舞いをする実装クラス
class Squeak(QuackBehavior):
def quack(self):
print("キューキュー")
# カモサブクラス(マガモ)
class MallardDuck(Duck):
def __init__(self):
self.quack_behavior = Quack()
self.fly_behavior = FlyWithWings()
def display(self):
print("マガモだよ!")
ここまでで大分当初の設計原理に近づいた形になりました。
振る舞いを動的に設定する(本書P55)
最後に、カモの振る舞いを実行時に変更できるようにしていきます。
その際に、Duckクラスのセッターメソッドを呼び出すだけで変更できるようにします。
from abc import ABC, abstractmethod
# FlyBehaviorという抽象クラスを定義し、fly()メソッドを抽象メソッドとして定義しています。
# 飛ぶ振る舞いをするためのインターフェース
class FlyBehavior(ABC):
@abstractmethod
def fly(self):
pass
# QuackBehaviorという抽象クラスを定義し、quack()メソッドを抽象メソッドとして定義しています。
# 鳴く振る舞いをするためのインターフェース
class QuackBehavior(ABC):
@abstractmethod
def quack(self):
pass
class Duck(ABC):
# fly_behaviorとquack_behaviorはFlyBehaviorクラスとQuackBehaviorクラスのインスタンスであることを示します。
# すべてのカモサブクラスはこの2つの変数を継承します。
fly_behavior: FlyBehavior = None
quack_behavior: QuackBehavior = None
def __init__(self):
pass
# display()メソッドはabstractmethodとして定義されており、派生クラスで実装する必要があります。
@abstractmethod
def display(self):
pass
# perform_fly()メソッドで、Duckクラスに指定されたfly_behavior(FlyBehavior)の飛行を実行します。
# 飛ぶ振る舞いを実行するには、Duckはfly_behaviorが参照するオブジェクトに飛ぶように依頼するだけでよい
def perform_fly(self):
self.fly_behavior.fly()
# perform_quack()メソッドで、Duckクラスに指定されたquack_behavior(QuackBehavior)の鳴き声を出力します。
# 鳴く振る舞いを実行するには、Duckはquack_behaviorが参照するオブジェクトに鳴くように依頼するだけでよい
def perform_quack(self):
self.quack_behavior.quack()
# swim()メソッドはすべてのカモが泳げることを表すメソッドです。
def swim(self):
print("すべてのカモは浮きます。おとりのカモでも!")
# 飛ぶ振る舞いを動的に設定するメソッドです。
def set_fly_behavior(self, fb: FlyBehavior):
self.fly_behavior = fb
# 鳴く振る舞いを動的に設定するメソッドです。
def set_quack_behavior(self, qb: QuackBehavior):
self.quack_behavior = qb
# 飛ぶ振る舞いをする実装クラス
class FlyWithWings(FlyBehavior):
# FlyBehaviorを継承したFlyWithWingsクラスでは、fly()メソッドを定義します。
def fly(self):
print("飛んでいます!!")
# 飛べない振る舞いをする実装クラス
class FlyNoWay(FlyBehavior):
# FlyBehaviorを継承したFlyNoWayクラスでは、fly()メソッドを定義します。
def fly(self):
print("飛べません")
# ガーガー鳴く振る舞いをする実装クラス
class Quack(QuackBehavior):
# QuackBehaviorを継承したQuackクラスでは、quack()メソッドを定義します。
def quack(self):
print("ガーガー")
# 鳴かない振る舞いをする実装クラス
class MuteQuack(QuackBehavior):
# QuackBehaviorを継承したMuteQuackクラスでは、quack()メソッドを定義します。
def quack(self):
print("<<沈黙>>")
# キューキュー鳴く振る舞いをする実装クラス
class Squeak(QuackBehavior):
# QuackBehaviorを継承したSqueakクラスでは、quack()メソッドを定義します。
def quack(self):
print("キューキュー")
# カモサブクラス(模型のカモ)
class ModelDuck(Duck):
# MallardDuckクラスではDuckクラスを継承し、__init__()メソッドにセットアップ処理が含まれ、quack_behaviorにQuackクラスのインスタンス、fly_behaviorにFlyWithWingsクラスのインスタンスを指定します。
def __init__(self):
self.fly_behavior = FlyNoWay()
self.quack_behavior = Quack()
# display()メソッドを実装しています。
def display(self):
print("模型のカモです")
# 新しい飛ぶ振る舞いを定義
class FlyRocketPowered(FlyBehavior):
def fly(self):
print("ロケットで飛んでいます!")
# インスタンス化
model = ModelDuck()
model.perform_fly() # 飛べません
model.set_fly_behavior(FlyRocketPowered())
model.perform_fly() # ロケットで飛んでいます!
以上で本書の第1章のデザインパターンStrategyについての補足は終わりです。コードを
Strategyパターンの3つのコンポーネント
最後に、これまでのコードをStrategyパターンの以下の3つのコンポーネントに分けてまとめます。
- Context: 現在のStrategyオブジェクトを保持するクラス。クライアントはこのクラスを通じてStrategyを利用します。
- Strategy: すべての具体的なStrategyが実装するインターフェースや抽象クラス。
- ConcreteStrategy: Strategyインターフェースを実装する各具体的なクラス。これらは具体的なアルゴリズムや方法を提供します。
Context:
Duckクラス:
これはコンテキストとなるクラスで、特定の行動(飛ぶ、鳴くなど)を実行するためのインターフェースを提供します。このクラスは具体的なアルゴリズム(具体的な飛ぶ方法や鳴く方法)を知りませんが、そのアルゴリズムを実行するためのインターフェースを持っています。
ModelDuckクラス:
Duckの具体的なサブクラス。
Strategy:
FlyBehaviorクラス:
飛ぶ振る舞いに関するアルゴリズムの家族のためのインターフェース(抽象クラス)。
QuackBehaviorクラス:
鳴く振る舞いに関するアルゴリズムの家族のためのインターフェース(抽象クラス)。
ConcreteStrategy:
飛ぶ振る舞いの具体的な戦略:
FlyWithWingsクラス
FlyNoWayクラス
FlyRocketPoweredクラス
鳴く振る舞いの具体的な戦略:
Quackクラス
MuteQuackクラス
Squeakクラス
Duckクラス(Context)は、飛ぶ振る舞いと鳴く振る舞いに関する具体的なアルゴリズムを知らず、それらの振る舞いをStrategyインターフェース(FlyBehaviorとQuackBehavior)を通じて行います。具体的な振る舞い(ConcreteStrategy)は、それぞれの具体的なクラス(例:FlyWithWings, Quackなど)によって定義されます。
まとめ
オブジェクト指向、デザインパターンは慣れていないと逆にコーディングに時間がかかりますね。。
ただ、運用・保守の観点からも設計原則に沿ったコードのほうが長期的なコストは下がるはずです。
考え方だけでなく、経験も含めもっときれいなコードを書けるようにしていきましょう。(戒め)