2021

10

13

オブジェクト指向を復習してやんよ!!!(多重継承&コンポジション)

タグ: |

Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

良書と言われる「オブジェクト指向設計ガイド」からオブジェクト指向の理解を深め中。

今回は7&8章から多重継承とコンポジションを掘り下げ

クラスの多重継承がもたらす複雑さ

通常の単一継承

# スーパークラス
class SuperClass:
  def __init__(self):
    self.a = 1

# SuperClassを継承したサブクラス
class SubClass(SuperClass):
  def __init__(self):
    super().__init__()
    self.b = 2

obj = SubClass()
print(obj.a + obj.b)    # --> 3

多重継承

Dクラスのインスタンスに対してhelloメソッドを呼び出すと、継承の流れでオーバーライドされたAクラスとCクラスのどちらのhelloメソッドが呼び出されるか?という問題

継承階層が構成される際に、どのメソッドが呼び出されるかや同じクラス(この場合はAクラス)がメソッド解決時に何度も登場することが問題になる

class A:
    def hello(self):
        print('Aから呼び出し')

class B(A):
    pass

class C(A):
    def hello(self):
        print('Cから呼び出し')

# BとCを派生させる
class D(B, C):
    pass


d = D()
d.hello()
# 出力されるのは A? C?
多重継承図

菱形(ひしがた)継承問題

  • 別名「ダイヤモンド継承問題」
  • pythonの場合、実行クラスの継承元は引数の左から順に探索されるので「D → B → C → A」となり、この例題の場合は最終的にAを継承してオーバーライドした「Cから呼び出し」が表示される
  • つまりクラス継承で縦軸の依存関係を明確にさせたにも関わらず、クラスの利便性を目的に横軸にも過剰な依存関係を乱立させることでそれぞれの継承関係の把握とメソッド探索がかえって困難になってしまう
  • 前章で出た開発者側がメソッドの動作を把握しなければならない認知的負荷が前提のダックタイピングの応用手法として、複数の継承を過剰に使おうとするとこの問題に陥ってしまう可能性がある

ミックスイン

  • この多重継承の複雑さを解決するのがミックスインというクラスを拡張のためのクラス
  • 単体で動作することを意図しないクラス、つまり他のクラスに継承されれば機能する
  • ミックスインの実行クラスの引数の左から順に優先で継承していくので継承関係も把握しやすい
class Person:
    def __init__(self, name):
        self.name = name
    
# 公式の予定
# ※Person を継承するので単体では機能しない
class OfficialMixin:
    def schedule(self):
        return self.name + 'は勉強会に参加します'

# 本当の予定
# ※Official を継承するので単体では機能しない
class SecretMixin:
    def schedule(self):
        return super().schedule() + ' という嘘をついて、遊びに行きます'


# Person と OfficialMixin のみをミックスイン
class FakeSchedule(Person, OfficialMixin):
    pass

# Person と OfficialMixin と SecretMixin をミックスイン
class TrueSchedule(Person, SecretMixin, OfficialMixin):
    pass


public = FakeSchedule('山田')
print("公式の予定:"+ public.schedule())
# --> 公式の予定:山田は勉強会に参加します

private = TrueSchedule('山田')
print("本当の予定:"+ private.schedule())
# --> 本当の予定:山田は勉強会に参加します という嘘をついて、遊びに行きます


	official = OfficialMixin() # --> エラー
official.schedule('山田')
# --> Personから名前を継承していないので単体で使えない

pythonにおけるモジュールの定義

  • pythonの場合、言語の理念上コードの可読性を1番に重視しているのでモジュールは別のプログラム内で特定のコードを再利用するためにファイル単位で物理的に整理する手法以上の意味合いを持たせておらず、基本的には別ファイルに作成したスーパークラス名を実行ファイルにimportするだけで利用可能。
    つまりimportしてないモジュールは実行ファイル内では利用してないと同意で共有できる
  • さらにpythonはマニアックなライブラリが多いので(wikipediaを丸々インポートできるヤツだったり、windowsを強制終了させるヤツだったり、小惑星と地球の衛星位置を計算するヤツだったり)モジュールをゼロから自作する機会もあまり無く、各モジュール同士の結合ロジックはFWのviewファイル内でやってまうので個人的にはここはあまり意識していない

コンポジション

  • コンポーネントオブジェクトにコンポジットオブジェクト(複合要素)を持たせる
  • コンポジットクラスは交換させる事を前提に「部品」に分解して利用する
  • 異なるクラスのオブジェクトを組み合わせることにより、複雑な型を作成できる
  • 「AはBを含んでいる」A has a Bの関係(例.自転車はサドルを含んでいる)
  • 「AはBである」のis-aの関係を持つことが前提の継承ではなく、単に機能を持たせたい場合には、継承ではなくコンポジションとすることが推奨される
# 給料クラス
class Salary:
    def __init__(self, monthly_income):
        self.monthly_income = monthly_income    # 月収
    # 年収
    def get_total(self):
        return (self.monthly_income * 12)
 

# 年俸クラス
class AnnualSalary:
    def __init__(self, monthly_income, bonus):
        self.monthly_income = monthly_income  # 月収
        self.bonus = bonus    # ボーナス
        self.obj_salary = Salary(self.monthly_income)   # 給料インスタンス
 
    # 年俸
    def annual_salary(self):
        return "合計: ¥" + str(self.obj_salary.get_total() + self.bonus)
 

# 社員の年俸(月収, ボーナス)
obj_emp = AnnualSalary(380000, 500000)
# 合計の年俸を出力
print(obj_emp.annual_salary())
# --> 合計: ¥5060000
  • 年俸クラス(AnnualSalary)は給料クラス(Salary)を継承していないがインスタンスを渡すことで年収のメソッドを利用している
  • 個人としては生活に必要な毎月に入ってくる基準値の方が大事だが、支払う側や管理する側の第三者としては最終的に出ていくお金の方が概念としては大事だけども「年俸は給与である」の継承の関係性とは直接結びつけにくい
  • こういった場合の「年俸はボーナスを含む」や「年俸には給与の12ヶ月分が含まれる」といったただ事実確認のための機能でいえばコンポジションで実装したほうが適切である

まとめ

  • 継承は親クラスを理解してコードを記述する必要があるが、コンポジションは「部品」はそれそのもので存在できるので、依存度が低い
  • 継承は構造の把握しやすさを優先させ、コンポジションはクラス単位の影響範囲の見えやすさを柔軟にする
  • 継承かコンポジションか?は動くコードでいえば思えばどちらでも成立させることは可能なので、どちらを選ぶ悩んだ際のより具体的な判断基準はこの解説が個人的にしっくりきた
    • 継承は分類法による分析
      • 分類法とは、物事を分類していくことで、その物事の本質を掴もうとする分析方法
      • 「猿と人は哺乳類の一種」「哺乳類と爬虫類は動物の一種」「動物と植物は生物の一種」
      • 物事をありのままにとらえ、その性質に従って分類していくと、物事同士の共通点が見える
      • 共通点を慎重に選り分けていった中に、本質が見えてくるのではないかという考え方
    • コンポジションは分割による分析
      • 分割法は、より小さく物事を分割していくことで、その物事の本質を掴もうとする分析方法
      • 「人間は頭、腕、脚、胴体を含んでいる」「腕は上腕、下腕、手を含んでいる」「手は手の甲、手のひら、指を含んでいる」
    • 物事はそのままでは複雑なので、適切に切り分けることで単純化し、より明確にする考え方
  • Effective Javaには「親クラスについての知識が必要で時に親クラスの世界を壊してしまう可能性のある継承よりも、コンポジションで考える方がよりふさわしい」という表記もあるらしいので最悪、迷ったときはコンポジションを選択
Pocket
LINEで送る
Facebook にシェア
reddit にシェア
LinkedIn にシェア

トップへ