编程的法则 依赖倒置原则 (Dependency Inversion Principle)包括如何实践
flyfish
2017-07-19
2024-07-18
在软件工程中,存在着层次结构,其中上层的业务逻辑依赖于下层的实现细节。如果是直接的依赖关系可能会导致问题,因为较低层级的变化会影响到高层级的稳定性。
这就是依赖倒置原则发挥作用的地方,在软件设计中引入抽象作为中间层,让上层和下层都依赖于这个抽象,而不是彼此直接依赖。
这意味着,无论是在业务逻辑层还是数据访问层,都应该通过接口或抽象类来进行交互,而非具体的实现。这样一来,当需要更改或替换低层的具体实现时,不会对上层产生影响,因为它们只关心抽象的行为,而不在乎具体是如何完成的。
此外,依赖倒置原则还强调,抽象应该是稳定的,不应依赖于具体的实现细节;反之,具体的实现应该依赖于抽象。这样,即使底层实现发生变化,只要抽象不变,整个系统就能保持稳定。
上面的话总结原则的核心思想是:
-
高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
-
抽象不应该依赖于细节。细节应该依赖于抽象。
高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
高层模块 (High Level Modules)通常包含应用程序的业务逻辑或策略。它们决定了应用程序如何运作以及如何实现主要功能。而低层模块 (Low Level Modules)通常是实现具体功能的组件,例如数据库访问、网络通信、文件读写等。
在传统设计中,高层模块直接依赖于低层模块。这种设计的问题在于,如果低层模块发生改变,高层模块也必须相应地修改,导致系统的耦合度过高,不易于维护和扩展。
依赖抽象 意味着在高层模块和低层模块之间引入一个抽象层,通常是接口或抽象类。这样,高层模块和低层模块都依赖于这个抽象层,而不是直接依赖彼此的具体实现。
举例说明
假设有一个应用程序,它需要从不同的数据源读取数据并进行处理。数据源可以是文件、数据库或网络。
-
高层模块 :负责处理数据的逻辑。
-
低层模块 :实现具体的数据读取操作。
#include <iostream>
#include <string>
// 抽象层
class DataReader {
public:
virtual std::string readData() = 0;
virtual ~DataReader() = default;
};
class Writer {
public:
virtual void write(const std::string& data) = 0;
virtual ~Writer() = default;
};
// 具体实现
class FileDataReader : public DataReader {
public:
std::string readData() override {
return "Data from file";
}
};
class DatabaseDataReader : public DataReader {
public:
std::string readData() override {
return "Data from database";
}
};
class NetworkDataReader : public DataReader {
public:
std::string readData() override {
return "Data from network";
}
};
class ConsoleWriter : public Writer {
public:
void write(const std::string& data) override {
std::cout << "Writing to console: " << data << std::endl;
}
};
class FileWriter : public Writer {
public:
void write(const std::string& data) override {
// 模拟写入文件
std::cout << "Writing to file: " << data << std::endl;
}
};
// 高层模块
class DataProcessor {
DataReader& reader;
Writer& writer;
public:
DataProcessor(DataReader& r, Writer& w) : reader(r), writer(w) {}
void process() {
std::string data = reader.readData();
writer.write(data);
}
};
// 调用示例
int main() {
FileDataReader fileReader;
DatabaseDataReader dbReader;
NetworkDataReader netReader;
ConsoleWriter consoleWriter;
FileWriter fileWriter;
DataProcessor fileProcessor(fileReader, consoleWriter);
DataProcessor dbProcessor(dbReader, fileWriter);
DataProcessor netProcessor(netReader, consoleWriter);
fileProcessor.process();
dbProcessor.process();
netProcessor.process();
return 0;
}
解释
-
抽象层 :
DataReader
和Writer
是两个抽象类,定义了readData
和write
方法。 -
具体实现 :
FileDataReader
、DatabaseDataReader
和NetworkDataReader
实现了DataReader
接口。ConsoleWriter
和FileWriter
实现了Writer
接口。 -
高层模块 :
DataProcessor
类依赖于DataReader
和Writer
接口,实现数据读取和写入的逻辑。 -
调用示例 :在
main
函数中,创建了不同的具体实现对象,并传递给DataProcessor
类,分别模拟了从文件、数据库和网络读取数据并写入控制台或文件。
输出:
Writing to console: Data from file
Writing to file: Data from database
Writing to console: Data from network
在这种设计中,DataProcessor
类依赖于抽象的 DataReader
接口,而不是具体的 FileDataReader
、DatabaseDataReader
或 NetworkDataReader
类。这样,如果我们需要添加新的数据源,只需要实现 DataReader
接口,而不需要修改 DataProcessor
类。
抽象不应该依赖于细节。细节应该依赖于抽象。
这条原则的意思是,抽象层应该保持独立,不受具体实现的影响。具体实现(低层模块)应该依赖于抽象层,而不是反过来。
如果抽象层依赖于具体实现,那么抽象层的改变将导致具体实现的改变,从而违反了依赖倒置原则。希望抽象层稳定,具体实现可以随时更换,而不影响抽象层和高层模块的代码。
举例说明
假设在设计一个日志系统,日志可以输出到控制台、文件或远程服务器。
#include <iostream>
#include <string>
// 抽象层
class ILogger {
public:
virtual void log(const std::string& message) = 0;
virtual ~ILogger() = default;
};
// 具体实现
class ConsoleLogger : public ILogger {
public:
void log(const std::string& message) override {
std::cout << "Console Log: " << message << std::endl;
}
};
class FileLogger : public ILogger {
public:
void log(const std::string& message) override {
// 模拟写入文件
std::cout << "File Log: " << message << std::endl;
}
};
class RemoteLogger : public ILogger {
public:
void log(const std::string& message) override {
// 模拟发送到远程服务器
std::cout << "Remote Log: " << message << std::endl;
}
};
// 高层模块
class Logger {
ILogger& logger;
public:
Logger(ILogger& l) : logger(l) {}
void log(const std::string& message) {
logger.log(message);
}
};
// 调用示例
int main() {
ConsoleLogger consoleLogger;
FileLogger fileLogger;
RemoteLogger remoteLogger;
Logger logger1(consoleLogger);
Logger logger2(fileLogger);
Logger logger3(remoteLogger);
logger1.log("This is a console log message.");
logger2.log("This is a file log message.");
logger3.log("This is a remote log message.");
return 0;
}
解释
-
抽象层 :
ILogger
是一个抽象类,定义了log
方法。 -
具体实现 :
ConsoleLogger
、FileLogger
和RemoteLogger
类实现了ILogger
接口,分别将日志输出到控制台、文件和远程服务器。 -
高层模块 :
Logger
类依赖于ILogger
接口,实现日志记录的逻辑。 -
调用示例 :在
main
函数中,创建了不同的具体实现对象,并传递给Logger
类,分别模拟了不同的日志记录方式。
输出:
Console Log: This is a console log message.
File Log: This is a file log message.
Remote Log: This is a remote log message.
在这种设计中,ILogger
是一个抽象接口,ConsoleLogger
、FileLogger
和 RemoteLogger
类实现了这个接口。Logger
类依赖于抽象的 ILogger
接口,而不是具体的实现。这样,如果需要添加新的日志输出方式,只需要实现 ILogger
接口,而不需要修改 Logger
类。