目录
概述
依赖倒置原则(Dependency Inversion Principle, DIP)是罗伯特·C·马丁(Robert C. Martin)提出的一个面向对象设计原则。主要强调的是高层次模块不应该依赖于低层次模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则可以分为两个部分来理解:
-
高层次模块不应该依赖于低层次模块:
- 高层次模块通常是系统中更接近业务逻辑的部分,而低层次模块则是实现具体功能的细节。
- 为了保持高层模块的灵活性和可维护性,它们应当依赖于抽象接口或类,而不是具体的实现细节。
-
抽象不应该依赖于细节:
- 抽象是指定义好的接口或者基类,它们描述了行为的一般形式。
- 细节指的是具体的实现,这些实现应该依赖于抽象提供的接口或基类来进行定义。
遵循依赖倒置原则可以带来以下好处:
- 降低耦合度:通过依赖于抽象而非具体实现,可以减少模块间的耦合程度。
- 提高系统的灵活性:当需要改变某个模块的具体实现时,只要新的实现仍然符合原有的抽象接口,就不需要修改使用该模块的其他部分。
- 易于扩展:增加新的实现方式只需实现已有的抽象接口,不需要改动现有代码。
一个典型的实现方法是使用接口或抽象类作为依赖的基础,然后让具体实现去继承或实现这些抽象。这样,当需要更改或扩展功能时,只需要添加新的实现而无需修改现有的高层模块。
意图
面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
代码 示例
步骤 1: 定义抽象层
定义一个抽象层——Logger
接口:
// Logger.java
public interface Logger {
void log(String message);
}
步骤 2: 实现具体细节
为Logger
接口创建具体的实现类。这里有两个实现:ConsoleLogger
和FileLogger
。
ConsoleLogger
// ConsoleLogger.java
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Console Logger: " + message);
}
}
FileLogger
// FileLogger.java
import java.io.FileWriter;
import java.io.IOException;
public class FileLogger implements Logger {
private String fileName = "log.txt";
@Override
public void log(String message) {
try (FileWriter writer = new FileWriter(fileName, true)) {
writer.write("File Logger: " + message + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
步骤 3: 创建使用抽象的模块
创建一个UserService
类,使用Logger
接口进行日志记录。
// UserService.java
public class UserService {
private final Logger logger;
// 通过构造函数注入Logger实例
public UserService(Logger logger) {
this.logger = logger;
}
public void createUser(String username) {
// 模拟创建用户的过程
logger.log("Creating user: " + username);
System.out.println("User created: " + username);
}
}
步骤 4: 使用具体实现
在主程序中使用UserService
类,并传递不同的日志实现。
// Main.java
public class Main {
public static void main(String[] args) {
// 使用ConsoleLogger
Logger consoleLogger = new ConsoleLogger();
UserService userServiceWithConsoleLogger = new UserService(consoleLogger);
userServiceWithConsoleLogger.createUser("Alice");
// 使用FileLogger
Logger fileLogger = new FileLogger();
UserService userServiceWithFileLogger = new UserService(fileLogger);
userServiceWithFileLogger.createUser("Bob");
}
}
原理解析
在这个例子中,遵循依赖倒置原则的两个方面:
-
高层次模块不依赖于低层次模块:
UserService
类是一个高层次模块,依赖于抽象接口Logger
。ConsoleLogger
和FileLogger
是具体的实现细节,实现Logger
接口。
-
抽象不依赖于细节:
Logger
接口定义日志记录的行为,没有涉及到任何具体的实现细节。- 具体的日志记录器(
ConsoleLogger
和FileLogger
)依赖于Logger
接口并实现了它的方法。
优点
- 解耦:通过依赖于抽象,可以轻松地更换或添加新的日志记录器实现,而不需要修改
UserService
类。 - 易于测试:由于
UserService
类依赖于抽象可以在单元测试中使用模拟(mocks)或存根(stubs)来代替实际的日志记录器。