Javaの依存関係の逆転の原則 (DIP)
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);
}
}
この実装では、UserServiceImpl
がUserRepository
に依存していますが、UserRepository
の具体的な実装クラスには依存していません。代わりに、UserRepository
のインタフェースを宣言した抽象クラスを注入することで、UserServiceImpl
はUserRepository
との依存関係を逆転させることができます。
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を使用してユーザーをデータベースに保存する
}
}
JdbcUserRepository
はUserRepository
インタフェースを実装する具体的なクラスであり、UserServiceImpl
はUserRepository
インタフェースを注入することによってJdbcUserRepository
との依存関係を逆転させることができます。このようにすることで、UserServiceImpl
はJdbcUserRepository
の具体的な実装の詳細を知る必要がなくなり、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
インターフェースに依存するように変更され、EmailNotifier
と SMSNotifier
クラスは、Notifier
インターフェースを実装していることが保証されます。依存関係の逆転の原則により、上位の NotificationService
クラスは下位の Notifier
インターフェースに依存するようになりました。これにより、NotificationService
クラスは、EmailNotifier
と SMSNotifier
の実装の詳細について知る必要がなくなり、柔軟性と再利用性が向上しました。
まとめ
依存関係の逆
次に、クライアント側のコードを書いていきます。具体的には、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