🦸

ポリモーフィズムで依存関係を制御する

2024/09/27に公開

はじめに

ソフトウェア設計において、柔軟性と拡張性は重要な要素です。この記事では、オブジェクト指向の基本概念であるポリモーフィズムを用いて、いかにして依存性を削減し、依存関係を制御することで、柔軟な設計を実現できるかを解説します。

1. ポリモーフィズムとは

ポリモーフィズムとは、オブジェクト指向プログラミングの特徴の一つで、異なる実装を持つオブジェクトを抽象化して同一の方法で扱える能力です。

ポリモーフィズムの基本的な例

例えば、動物を表す抽象クラス Animal があり、それを継承する DogCat という具象クラスがあるとします。

abstract class Animal {
    abstract void makeSound();
}

class Dog extends Animal {
    void makeSound() {
        System.out.println("Bark");
    }
}

class Cat extends Animal {
    void makeSound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.makeSound(); // "Bark"
        cat.makeSound(); // "Meow"
    }
}

この例では、Animal クラスが makeSound という共通のメソッドを提供し、具体的な実装は DogCat がそれぞれ定義しています。このように、ポリモーフィズムを活用することで、同じAnimal クラスとして扱うことができます。

2. 変更に強いコード

ポリモーフィズムを用いることで、具体的なクラスへの依存を減らし、インターフェースや抽象クラスに対して依存する設計が可能です。これにより、既存のコードを変更せずに新しい機能を追加できるようになります。

インターフェースを使った設計

例えば、IOデバイス間でデータをコピーするプログラムを考えます。

class DeviceService {
    private IODevice inputDevice;
    private IODevice outputDevice;
    ...

    public void copy() {
        String data = inputDevice.readData();
        outputDevice.writeData(data);
    }
}
interface IODevice {
    String readData();
    void writeData(String data);
}
// ファイルの読み書きデバイス
class FileDevice implements IODevice {

    @Override
    public String readData() {
        //実際の処理
        ...
    }

    @Override
    public void writeData(String data) {
        //実際の処理
        ...
    }
}

// USBデバイスの読み書きデバイス
class USBDevice implements IODevice {

    @Override
    public String readData() {
        //実際の処理
        ...
    }

    @Override
    public void writeData(String data) {
        //実際の処理
        ...
    }
}

もし新たなデバイスを追加したい場合、新たなIODeviceの実装クラスを用意すればよく、copyメソッドは何も変更する必要はありません。(開放/閉鎖原則)

もしポリモーフィズムを使用しないとすると、copyメソッドは次のようになります。

public void copy() {
    String data = "";

    // 入力デバイスの種類ごとの処理
    if (inputDeviceType.equals("file")) {
        data = readFromFile();
    } else if (inputDeviceType.equals("usb")) {
        data = readFromUSB();
    } else {
        System.out.println("Unsupported input device");
        return;
    }

    // 出力デバイスの種類ごとの処理
    if (outputDeviceType.equals("file")) {
        writeToFile(data);
    } else if (outputDeviceType.equals("usb")) {
        writeToUSB(data);
    } else {
        System.out.println("Unsupported output device");
        return;
    }
}

これは新たなデバイスを追加するたびにif文を追加していくことになり、copyメソッドはどんどん複雑化し、保守が困難になります。加えて、copy以外の他の処理においても同様に複数の分岐が発生してしまいます。機能追加のたびに既存コードの変更箇所を洗い出し、既存の処理を壊さないように修正しなければならないため、変更に対して弱い設計といえます。

このように、ポリモーフィズムを用いてインターフェースに対してプログラミングを行うことで、変更に強いコードを記述することが可能です。別の例として、Strategyパターンは、変更されやすい箇所をカプセル化し、クライアント側ではインターフェースに対してプログラミングすることで、アルゴリズムの実装クラスを独立して切り替えるようになっています。

3. 依存関係の制御

ポリモーフィズムを活用することで、依存関係の方向を制御できます。

クラスAがクラスBを利用している例を考えます。この場合、クラスAはクラスBに依存している(クラスBを変更するとクラスAに影響するが、クラスAを変更してもクラスBは影響を受けない)と言います。通常の実装方法では、制御の向き(A→Bを呼び出す)と依存関係の向き(A→Bに依存)は同じ方向になります。

ここで、インターフェースを用いてみます。

クラスA側で必要な処理を定義したインターフェースを用意し、クラスBをインターフェースの実装クラスにすることで、クラスBがインターフェースに依存するようになります。このようにして依存関係の向きを実際の制御の流れと逆にすることができます(依存関係逆転)。

このように、ポリモーフィズムを利用することで、依存関係の方向を自由に制御できるようになります。

依存関係逆転の例

上記の身近な例で、ユーザーインターフェース、ビジネスルール、データベースの三つのコンポーネントでできたシステムを考えます。先ほどの様にポリモーフィズムを用いることで、システムの核であるビジネスルールに対してUIとデータベースが依存するようにすることができます。

こうすることで、ビジネスルールはUIとデータベースなどの詳細(低レイヤ)コンポーネントの変更の影響を一切受けなくなります。UIやデータベースはプラグインのように切り替えることも可能になります。例えば、WebアプリケーションからCLIアプリケーションへの変更や、リレーショナルデータベースからテスト中はインメモリデータベースに切り替えたりをビジネスルールを壊すことなく行えます。そのため、純粋なビジネスルールに集中して開発することが可能であり、独立してデプロイも可能になります。詳細は割愛しますが、クリーンアーキテクチャ(ヘキサゴナルアーキテクチャ)は、ポリモーフィズムを用いて依存関係の中心をビジネスルールとするアーキテクチャで、ドメイン駆動設計と相性が良いです。

おわりに

本記事では、ポリモーフィズムを活用した柔軟で拡張性の高いソフトウェア設計について解説しました。特に、依存関係を効果的に制御できる点が重要です。インターフェースを活用することで、クラス間の依存を逆転させ、ビジネスルールやコア機能が外部コンポーネントに影響されることなく保護されます。このような設計は、システムの拡張や変更を容易にし、安定した開発と運用を支える基盤となります。依存関係の管理を通じて、柔軟かつ持続可能なアーキテクチャを実現しましょう。

Discussion