🔖

Javaの依存関係の逆転の原則 (DIP)

2023/05/01に公開

Javaの依存関係の逆転の原則 (DIP)

はじめに

Javaの依存関係の逆転の原則 (Dependency Inversion Principle, DIP)は、オブジェクト指向プログラミングにおいて重要な原則の一つです。DIPは、コードの変更をより簡単に行えるようにするため、高位モジュールと低位モジュールの間の依存関係を逆転させることを目的としています。この原則を理解することで、より柔軟で拡張性の高いプログラムを作ることができます。以下の記事はchatGPTによる出力を参考に整形することで作成した記事になります。

用語の定義

高位モジュール (High-level module)

高位モジュールは、一般的にビジネスロジックを実装しているクラスやモジュールです。高位モジュールは、他の低位モジュールを呼び出すことがありますが、それらの詳細については知りません。

低位モジュール (Low-level module)

低位モジュールは、一般的にデータベースや外部APIなどのインフラストラクチャに関連するクラスやモジュールです。低位モジュールは、高位モジュールから呼び出されますが、それらがどのように構築されているかについては知りません。

抽象 (Abstraction)

抽象とは、オブジェクトのインターフェイスを定義することです。抽象を使用することで、実装の詳細を知らずにオブジェクトを操作できます。

具象 (Concretion)

具象は、抽象の実際の実装です。具象は、抽象のインターフェースを満たす必要があります。

依存関係 (Dependency)

依存関係とは、あるオブジェクトが他のオブジェクトに依存する関係のことです。オブジェクトが他のオブジェクトに依存する場合、そのオブジェクトは、その他のオブジェクトが変更された場合に影響を受ける可能性があります。

逆転 (Inversion)

逆転とは、従来の依存関係を逆転させることを意味します。従来の依存関係では、低位モジュールが高位モジュールに依存していましたが、逆転

public class UserServiceImpl implements UserService {
    private UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public User getUserById(int id) {
        return userRepository.getUserById(id);
    }

    @Override
    public void saveUser(User user) {
        userRepository.saveUser(user);
    }
}

この実装では、UserServiceImplUserRepositoryに依存していますが、UserRepositoryの具体的な実装クラスには依存していません。代わりに、UserRepositoryのインタフェースを宣言した抽象クラスを注入することで、UserServiceImplUserRepositoryとの依存関係を逆転させることができます。

public interface UserRepository {
    User getUserById(int id);

    void saveUser(User user);
}

public class JdbcUserRepository implements UserRepository {
    @Override
    public User getUserById(int id) {
        // JDBCを使用してデータベースからユーザーを取得する
    }

    @Override
    public void saveUser(User user) {
        // JDBCを使用してユーザーをデータベースに保存する
    }
}

JdbcUserRepositoryUserRepositoryインタフェースを実装する具体的なクラスであり、UserServiceImplUserRepositoryインタフェースを注入することによってJdbcUserRepositoryとの依存関係を逆転させることができます。このようにすることで、UserServiceImplJdbcUserRepositoryの具体的な実装の詳細を知る必要がなくなり、UserRepositoryが提供するメソッドのみに依存することができます。

この例は非常に単純であるため、実際のアプリケーションでは、複数のモジュールやクラスが相互に依存し、それらを管理する必要があります。しかし、DIPを遵守することによって、アプリケーションの柔軟性と拡張性を向上させることができます。

以上が、Javaの依存関係の逆転の原則 (DIP)についての解説です。DIPを理解することは、より良い設計をする上で非常に重要なことであり、将来的に大規模なアプリケーションを開発するために必要なスキルの一つです。

OrderService クラスが OrderRepository クラスに依存しているため、 OrderRepository クラスが変更されると OrderService クラスに影響を与えます。しかし、 OrderService クラスは OrderRepository クラスを必要とするだけでなく、CustomerService クラスも必要としています。この場合、CustomerService クラスは OrderService クラスの高位モジュールであり、OrderRepository クラスは低位モジュールであると見なすことができます。高位モジュールが低位モジュールに依存している場合、依存関係は逆転しておらず、依存関係逆転の原則に違反しています。

依存関係を逆転させるために、 OrderService クラスは OrderRepository クラスを直接参照するのではなく、 OrderRepository インターフェースを介して参照する必要があります。これにより、 OrderRepository クラスの実装が変更されても、OrderService クラスの変更を必要としなくなります。同様に、 CustomerService クラスも CustomerRepository インターフェースを介して CustomerRepository クラスに依存する必要があります。これにより、 OrderService クラスと CustomerService クラスは、それぞれの高位モジュールであるため、依存関係は逆転しています。

以下は、依存関係逆転の原則に従って再設計された OrderService クラスと CustomerService クラスの例です。

public interface OrderRepository {
    void save(Order order);
}

public interface CustomerRepository {
    void save(Customer customer);
}

public class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;

    public OrderServiceImpl(OrderRepository orderRepository, CustomerRepository customerRepository) {
        this.orderRepository = orderRepository;
        this.customerRepository = customerRepository;
    }

    @Override
    public void placeOrder(Order order) {
        // do some order validation
        // ...

        // save order and customer info
        orderRepository.save(order);
        customerRepository.save(order.getCustomer());
    }
}

public class CustomerServiceImpl implements CustomerService {
    private final CustomerRepository customerRepository;

    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void createCustomer(Customer customer) {
        // do some customer validation
        // ...

        // save customer info
        customerRepository.save(customer);
    }
}

再設計された OrderServiceImpl クラスは、 OrderRepository インターフェースと CustomerRepository インターフェースをコンストラクタで

public interface Database {
    void save(String data);
}

ここで定義されたDatabaseインターフェースは、データベースにアクセスするための一般的なメソッドを定義しています。このインターフェースは、HighLevelClassが依存するものの1つであり、具体的な実装には依存していません。つまり、HighLevelClassは、Databaseインターフェースを通じてデータベースとやりとりすることができ、その具体的な実装に依存しないため、拡張性が高くなります。

次に、このDatabaseインターフェースを実装するクラスMySQLDatabaseを定義しましょう。

public class MySQLDatabase implements Database {
    public void save(String data) {
        System.out.println("Data saved to MySQL database: " + data);
    }
}

ここで、MySQLDatabaseクラスはDatabaseインターフェースを実装しています。saveメソッドは、Databaseインターフェースで定義されたシグネチャをそのまま実装しています。HighLevelClassは、Databaseインターフェースに依存しているため、MySQLDatabaseクラスの具体的な実装には依存していません。

最後に、HighLevelClassクラスを定義し、Databaseインターフェースを使用してデータベースにデータを保存するメソッドを実装します。

public class HighLevelClass {
    private Database database;
    
    public HighLevelClass(Database database) {
        this.database = database;
    }
    
    public void sendData(String data) {
        database.save(data);
    }
}

ここで、HighLevelClassクラスは、Databaseインターフェースに依存していますが、具体的な実装には依存していません。HighLevelClassクラスのコンストラクタで、Databaseインターフェースを実装したオブジェクトを渡すことができます。そして、sendDataメソッドは、そのDatabaseオブジェクトを使用してデータを保存することができます。

このように、DIPを適用することで、HighLevelClassクラスは、Databaseインターフェースを通じてデータベースとやりとりすることができ、具体的な実装には依存しないため、拡張性が高くなります。また、Databaseインターフェースの具体的な実装には、他のクラスから簡単に切り替えることができるため、柔軟性が高くな

もう一度、コードの修正を行います。まず、Notifier インターフェースの定義を以下のように変更します。

public interface Notifier {
    void sendNotification(String message);
}

Notifier インターフェースは、NotificationService クラスから依存されるため、NotificationService クラスは依存関係の上位にあり、Notifier インターフェースは依存関係の下位にあります。

次に、NotificationService クラスの定義を以下のように変更します。

public class NotificationService {
    private Notifier notifier;

    public NotificationService(Notifier notifier) {
        this.notifier = notifier;
    }

    public void sendNotification(String message) {
        notifier.sendNotification(message);
    }
}

このようにすることで、NotificationService クラスは Notifier インターフェースに依存しています。つまり、NotificationService クラスは Notifier インターフェースを実装したクラスのどれかを受け入れることができます。これによって、NotificationService クラスは、具体的な Notifier の実装に依存しなくなりました。

次に、EmailNotifier クラスを以下のように変更します。

public class EmailNotifier implements Notifier {
    public void sendNotification(String message) {
        // メールを送信する実装
        System.out.println("メールを送信しました:" + message);
    }
}

このようにすることで、EmailNotifier クラスは Notifier インターフェースを実装し、NotificationService クラスに依存されることができます。

同様に、SMSNotifier クラスを以下のように変更します。

public class SMSNotifier implements Notifier {
    public void sendNotification(String message) {
        // SMS を送信する実装
        System.out.println("SMS を送信しました:" + message);
    }
}

これで、NotificationService クラスは、Notifier インターフェースに依存するように変更され、EmailNotifierSMSNotifier クラスは、Notifier インターフェースを実装していることが保証されます。依存関係の逆転の原則により、上位の NotificationService クラスは下位の Notifier インターフェースに依存するようになりました。これにより、NotificationService クラスは、EmailNotifierSMSNotifier の実装の詳細について知る必要がなくなり、柔軟性と再利用性が向上しました。

まとめ

依存関係の逆

次に、クライアント側のコードを書いていきます。具体的には、ElectricPowerSwitchクラスを作成します。

public class ElectricPowerSwitch {
    public LightBulb lightBulb;
    public boolean on;

    public ElectricPowerSwitch(LightBulb lightBulb) {
        this.lightBulb = lightBulb;
        this.on = false;
    }

    public boolean isOn() {
        return this.on;
    }

    public void press() {
        boolean checkOn = isOn();
        if (checkOn) {
            lightBulb.turnOff();
            this.on = false;
        } else {
            lightBulb.turnOn();
            this.on = true;
        }
    }
}

このクラスは、LightBulbクラスを依存関係として持ちます。具体的には、ElectricPowerSwitchクラスのコンストラクタにLightBulbオブジェクトを渡しています。このように、ElectricPowerSwitchクラスはLightBulbクラスに依存しています。

しかし、この依存関係は、DIPを守っていません。つまり、ElectricPowerSwitchクラスはLightBulbクラスの具体的な実装に依存しています。そこで、DIPを守るようにElectricPowerSwitchクラスを修正します。具体的には、ElectricPowerSwitchクラスのコンストラクタにSwitchableオブジェクトを渡すように変更します。

public class ElectricPowerSwitch {
    public Switchable switchable;
    public boolean on;

    public ElectricPowerSwitch(Switchable switchable) {
        this.switchable = switchable;
        this.on = false;
    }

    public boolean isOn() {
        return this.on;
    }

    public void press() {
        boolean checkOn = isOn();
        if (checkOn) {
            switchable.turnOff();
            this.on = false;
        } else {
            switchable.turnOn();
            this.on = true;
        }
    }
}

ここで注目すべき点は、ElectricPowerSwitchクラスが依存しているオブジェクトが、LightBulbクラスからSwitchableインターフェースに変更されたことです。つまり、ElectricPowerSwitchクラスはSwitchableインターフェースに依存するようになりました。これにより、ElectricPowerSwitchクラスはSwitchableインターフェースに定義されたメソッドを使用することができるようになり、LightBulbクラスの具体的な実装に依存することなく、より柔軟なコードになりました。

以上が、JavaでDIPを実装するための具体的な例です。DIPを守ることにより、コードの柔軟性が向上し、メンテナン

public interface DBConnection {
    void connect();
}

public class MySQLConnection implements DBConnection {
    @Override
    public void connect() {
        System.out.println("Connected to MySQL database.");
    }
}

public class OracleConnection implements DBConnection {
    @Override
    public void connect() {
        System.out.println("Connected to Oracle database.");
    }
}

public class DatabaseManager {
    private DBConnection dbConnection;

    public DatabaseManager(DBConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    public void connectToDatabase() {
        this.dbConnection.connect();
    }
}

この例では、DBConnectionインタフェースは高位モジュールであり、データベースへの接続を抽象化しています。MySQLConnectionおよびOracleConnectionクラスは低位モジュールであり、実際のデータベースへの接続を実現しています。

DatabaseManagerクラスは高位モジュールであり、データベースに接続する責任を持ちます。しかし、このクラスはDBConnectionインタフェースに依存しており、具体的なデータベースに依存していません。これにより、DatabaseManagerクラスはデータベースへの接続方法を知る必要がありません。

例えば、DatabaseManagerクラスは次のように使用できます。

DBConnection dbConnection = new MySQLConnection();
DatabaseManager dbManager = new DatabaseManager(dbConnection);
dbManager.connectToDatabase();

このコードでは、DatabaseManagerクラスはMySQLConnectionクラスのインスタンスを使用してデータベースに接続します。しかし、DatabaseManagerクラスはMySQLConnectionクラスに依存していないため、必要に応じて別のデータベースに接続することができます。

例えば、OracleConnectionクラスを使用する場合は、次のようにコードを変更することができます。

DBConnection dbConnection = new OracleConnection();
DatabaseManager dbManager = new DatabaseManager(dbConnection);
dbManager.connectToDatabase();

このように、Dependency Inversion Principleは、高位モジュールが低位モジュールに直接依存することなく、抽象に依存することを推奨しています。これにより、コードの柔軟性、拡張性、保守性が向上します。

Discussion