Pythonによるデザインパターン5原則


参考

@kidach1 さんの投稿をPythonに書き換えてるだけです。
@kidach1 さん、いつもありがとうございます。
https://qiita.com/kidach1/items/4b63de9ad5a97726c50c

概要

改めて基本を学ぶ。
参考「Rubyによるデザインパターン第1章」→この投稿はPython

デザインパターンとは

  • プログラミングにおいて繰り返し現れる問題に対する、適切解のパターン。
  • 無駄無く設計されたオブジェクト指向プログラムの実現をサポート。

パターンとしてカタログ化されていることで
車輪の再発明を防ぐ

デザインパターンの根底にある5つの考え

  • 変わるものを変わらないものから分離する
  • プログラムはインターフェイスに対して行う(実装に対して行わない)
  • 継承より集約
  • 委譲、委譲、委譲
  • 必要になるまで作るな(YAGNI)

変わるものを変わらないものから分離する

ソフトウェアの仕様には必ず変更が加わるもの。
変わるものと変わらないものを分離しておくことで、
「仕様の変更」に対して「システムの変更」を出来る限り局所的にする。

プログラムはインターフェイスに対して行う(実装に対して行わない)

可能な限り「一般的・抽象的なもの」に対してプログラミングすること。
(ここで言うインターフェイスは、Javaの組み込み構文としてのインターフェイスではなく、
より広いレベルで「抽象度を高めたもの」を意味する。)

これにより、全体のコードの結合度を下げる。

具体性が高く、密結合なコード

if ( is_car()  ):
  my_car = Car()
  my_car.drive(200)
else:
  my_plane = AirPlane()
  my_plane.fly(200)

乗り物が増える度にコード全体に変更が必要(変更に弱い)。

抽象度が高く、疎結合なコード


my_vehicle = get_vehicle()
my_vehicle.travel(200)

乗り物の数が増えても本コードに変更を加える必要はない(変更に強い)。

継承より集約

継承は望ましくないつながりを作ってしまう。

具体的には

  • サブクラスの振る舞いは、スーパークラスの振る舞いに依存する。
  • サブクラスからスーパークラスの中身を覗くことが出来る。
class Vehicle:
  def start_engine(self):
    # エンジンを動かすためのもろもろの処理..
    print("engine started")
  def stop_engine(self):
    # エンジンを止めるためのもろもろの処理..
    print("engine stopped")

class Car(Vehicle):
  def drive(self):
    self.start_engine()
    # driving..
    self.stop_engine()

car = Car()
car.drive()

Carからエンジンの実装が丸見え
エンジンを使用しない乗り物を作りたい場合は大改造が必要

→変わりやすい部分(Engine)を変わりにくい部分(Vehicle)から分離できていない。

代替案

集約を使う。
つまり、

オブジェクトに、「他のオブジェクトに対する参照」を持たせる。

オブジェクトが何かの一種である(is-a-kind-of)関係は避けて、
何かを持っている(has-a)関係にする。

Car has a Engineの例

class Car:
  def __init__(self):
    self.engine = Engine()

  def drive(self):
    self.engine.start()
    # driving..
    self.engine.stop()


class Engine:
  def start(self):
    print ("engine started")

  def stop(self):
    print ("engine stopped")

car = Car()
car.drive()

これでEngineがVehicleから分離され、またカプセル化された。

これにより、Engineの手軽な切り替えが可能に。

class Car:
  def __init__(self):
    self.engine = GasolineEngine()

  def drive(self):
    self.engine.start()
    # ガソリンエンジンでドライブ..
    self.switch_to_diesel()
    # ディーゼルエンジンでドライブ..
    self.engine.stop()


  def switch_to_diesel(self):
    self.engine = DieselEngine()

委譲、委譲、委譲

委譲(delegation)
(継承パターンと同じように)start_engineとstop_engineを
クラスの外に公開したいこともある。

そんな場合でも、処理の実態はEngineクラスに任せてしまう。

class Car:
  def __init__(self):
    self.engine = GasolineEngine()

  def drive(self):
    self.engine.start()
    # driving..
    self.engine.stop()

  def switch_to_diesel(self):
    self.engine = DieselEngine()

  def start_engine(self):
    self.engine.start()  # Engineクラスに任せる

  def stop_engine(self):
    self.eigine.stop()  # Engineクラスに任せる

これで「継承より集約」を実現しつつ、継承時と同じ機能を持つクラスが作成できた。
つまり、

集約と委譲の組み合わせは、強力かつ柔軟な継承の代替手段。

※ start_engineやstop_engineは委譲のために書かなければならない無駄なコードに思えるが、
method_missing等の利用で解決できる。
これはProxyパターンで別途詳細する。

必要になるまで作るな(YAGNI/You Ain't Gonna Need It.)

「将来使うかも」は、大抵使わない。
いたずらに柔軟性を持たせようとオーバーエンジニアリングして
コードの複雑性を増していては、本末転倒。

デザインパターン自身は目的ではない。
問題を解決し、目的を達成するための手段として役立てるべきであり、それ以上のものではない。

デザインパターンを考える上での戒め。

以上5原則を前提に

各パターンを見ていく。

【Template Method】-テンプレは準備した、あとはお好きに-
https://qiita.com/kotetsu75/items/2900eac0fa24f09f775b

【Strategy】-取り替え可能パーツ群を戦略的に利用せよ-
https://qiita.com/kotetsu75/items/186af7006b44703c0379

【Observer】-本日のニュースをお届けします-
https://qiita.com/kotetsu75/items/719a8fc2e4cc7789c1b4

【Composite】 -世界は再帰的(部分は全体、全体は部分)-
制作中

【Iterator】-君の子供たちに伝えたいのだけど-
制作中