Dyuichi Blog

オブジェクト指向シリーズⅡ: 三大要素(カプセル化,ポリモーフィズム,継承)

はじめに

本記事は「オブジェクト指向シリーズ」の第2回目の記事である.

「オブジェクト指向シリーズ」では,私がオブジェクト指向について勉強する中で特に重要であると思ったトピックについてまとめている.

誰かの勉強の際の手助けになれば幸いであるが,誤りがあれば以下に記載されているいずれかのSNSにご連絡いただけるとありがたい.

三大要素①: カプセル化

相互に関連性の高いデータの集合とそれらに対する操作を一つのオブジェクトの中に閉じ込め,抽象化することをカプセル化という.上手くカプセル化できているオブジェクトでは,オブジェクト内の操作の具体的な実装やデータが隠蔽されており,オブジェクトの利用者はその操作の仕様だけを知っていればいい.

例えば自動車はうまくカプセル化されているため,運転手はアクセルを踏むと前に進むということを理解していればよく,前に進むメカニズムを理解している必要はない.

また,オブジェクトのデータや変数のことをプロパティまたはフィールドメンバ変数と呼ばれ,操作や処理のことをメソッドまたはメンバ関数と呼ぶ.

デメテルの法則

オブジェクト間の疎結合を目的とした法則であり,次のような指針を持つ.

  • 各オブジェクトは,他のオブジェクトについて限られた情報しか持たない
  • 各オブジェクトは自分の友達とだけ話す
  • 知らない友達とは話さない

まとめると,「オブジェクトはその直接の友達とだけ話し,友達の友達とは話さない」ということである.つまりどういうことか,プログラム上で説明する.

以下がデメテルの法則に違反するプログラムである.

pythonclass Piston:
    def move(self):  # メソッド
        return "Piston is moving"

class Engine:
    def __init__(self):
        self.piston = Piston()  # プロパティ

class Car:
    def __init__(self):
        self.engine = Engine()  # プロパティ

    def start(self):  # メソッド
        # CarがEngineを通じてPistonのメソッドを直接呼び出している.
        return self.engine.piston.move()

以下がデメテルの法則に従ったプログラムである.

pythonclass Piston:
    def move(self):
        return "Piston is moving"

class Engine:
    def __init__(self):
        self.piston = Piston()

    # Engineクラスを通じてPistonのメソッドを呼び出すメソッドを追加
    def start(self):
        return self.piston.move()

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

    def start(self):
        # CarはEngineのメソッドのみを呼び出す
        return self.engine.start()

デメテルの法則を無視したプログラムでは,CarがPistonについて把握しておく必要があるのに対し,デメテルの法則に従ったプログラムでは,CarはEngineについて把握しておけばよく,Pistonについては何も知らなくていいことが分かる.

getter / setter

カプセル化の話の中でよくgetterとsetterが登場する.getterとsetterの目的は,プロパティが他のオブジェクトから直接的に呼び出されたり,書き換えられることを防ぐことである.

ただ,このgetterとsetterは多くの議論を招くもとになっており,どういう時に使うべきでどういう時に使わないべきかは以下の記事などを参考に,ご自身で判断していただくのが一番良い.

三大要素②: ポリモーフィズム

オブジェクトの操作方法(メソッド)は同じだが,実は中身が異なる実体が数多く存在するという,この様子がポリモーフィズムである.

以下にサンプルコードを示す.

pythonimport abc

class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"
    
class Human:
    def touch(self, animal: Animal):
        print(animal.speak())

human = Human()

human.touch(Dog())  # Woof!
human.touch(Cat())  # Meow!

speak() というメソッドは同じでも,得られる結果が異なっていることが分かる.

では,多態性が考慮されていない場合はどうなるか.実際にそのサンプルコードを以下に示す.

pythonimport abc

class Animal(metaclass=abc.ABCMeta):
    def __init__(self):
        self.type: str = None

class Dog(Animal):
    def __init__(self):
        self.type: str = "Dog"

class Cat(Animal):
    def __init__(self):
        self.type: str = "Cat"
    
class Human:
    def touch(self, animal: Animal):
        if animal.type == "Dog":
            print("Woof!")
        elif animal.type == "Cat":
            print("Meow!")

human = Human()

human.touch(Dog())  # Woof!
human.touch(Cat())  # Meow!

if分岐を用いて,異なる動物への対応を実装している.このような実装では,メンテナンスが面倒になる未来が十分に予測できる.

三大要素③: 継承

継承の実装例は,上のポリモーフィズムで既出している.継承では,以下のようにクラスが呼び分けられる.

  • 抽象クラス
    • 継承を前提とした(部分的に)実処理を持たないクラス(例:Animal)
    • インスタンス化不可
  • 派生クラス
    • 抽象クラスでも通常のクラスでも,継承したクラス(例:Dog,Cat)
    • 特に,抽象の実処理をすべて埋めたものを具象クラスと呼ぶ
    • インスタンス化可能
  • 基底クラス
    • 継承元になるクラス
    • 抽象クラスは基底クラスに含まれる
    • インスタンス化可能

インターフェース

インターフェースとは,一切の実処理を持たず,クラス仕様としての型定義をするものである.

ここで疑問に思うのが,「抽象クラスとインターフェースはどう使い分けるのか」である.

ざっくりとした回答は,

クラス仕様としての型定義をしたい ⇒ インターフェース

実処理の再利用をしたい ⇒ 抽象クラス

となる.

そもそも,抽象クラスは派生クラスにおける共通の処理をまとめて使えるようにすることが目的にあるのに対して,インターフェースでは,具体的な実処理は見ずにできることだけを定義し,クラスを使う人に提供することが目的となっているという違いがある.

ポリモーフィズムの実装例でいうと,Animalクラスはインターフェースでよい.ただしPythonにはインターフェースが提供されていないため,抽象クラスを使用している.

より詳しく抽象クラスとインターフェースの違いを深ぼりたい場合は,以下の記事が参考になる.

まとめ

オブジェクト指向には,カプセル化,ポリモーフィズム,継承という重要な三大要素がある.これらを理解し,意識して開発することでパッケージやオブジェクトの凝集度を高く,疎結合にすることができる.

これが最終的にパッケージの再利用性向上や,変化に強いソフトウェアにつながる.

参考文献

本記事では,オブジェクト指向における三大要素について,特に自分が勉強中気になった点を中心にざっくりと書いてみた.

次回はオブジェクト指向におけるSOLID原則について解説する.