一、解耦
每一个开发者在学习编程后可能接触到的最多的一个名词就是解耦。它几乎贯穿整个设计、开发和重构以及在未来软件的整个生命周期内的维护和处理。什么是解耦,这里不说各种官方的定义,只从实践中描述一下。解耦就是尽大可能降低甚至消除软件各个模块间的关联和依赖。这个模块小到一个函数,大到一个库甚至框架等。你能想多大就是多大,反之能想多小就是多小。可以说解耦是一个原则性的指导,它是设计的基础的工作之一。
二、如何解耦
既然解耦是一个基础的工作,那么如何解耦呢?有没有什么可以借鉴的方法或者手段?一定是有的。
1、设计解耦
解耦可以从设计开始即从源头就展开。业务需求转化为开发的设计时,就尽量将业务需求间的联系统一化、单一化和减少或消除依赖化。合理的分层和模块划分以及良好的接口设计自然可以做到这一点。这也是大公司为什么要有架构师存在的必要。
2、重构解耦
解耦同样可以从重构即软件到达一个阶段后在对设计和代码有较深的理解后,再抽丝剥茧进行解耦。重构不是一个紧急的工作,所以在重构时可以仔细的分析相关的实际情况,把解耦的工作做的更好。
3、代码级别提解耦
可以利用各种设计原则、设计模式和接口分离等方法进行。比如前面反复提到的单一职责和迪米特法则。职责越单一,和外界交互或依赖的可能性越低;而迪米特法则就是“不要和陌生人说话”。又如经常提到的依赖注入和控制反转,更是经典的解耦的方法。
在设计模式中,工厂模式可以很好的将应用与类实现解耦。也可以引入反射框架,这样解耦会更彻底!
4、利用库(动态或静态库)来解耦
库解耦的优势在于,开发方已经实现了高内聚,而应用方因为无法看到内部的实现,只能和接口打交道,自然做到了低耦合。道理是这个道理,可要是接口设计不好,耦合度仍然会不小,结果就是库的优势被削减。当然库也有库的问题,比如“DLL HELL”。一般比较普遍的解决这类问题是使用好版本管理,这里不展开说明,有兴趣可以查看相关的知识。
5、框架解耦
这种解耦方式,更多用于分布式的方法,可以说解耦到了一个更高的层次抽象。这里举一个不优秀的例子,比如程序里有两个进(线)程需要不断的进行通信,除了经典的同步等方法外,也可以使用IPC(MQ,PIPES,SHARED MEMORY等)或者消息框架亦或者更强大的服务(包括微服务、Mesh等)。之所以说这个例子不优秀,是因为线程间通信开发者如果没有特别的需求不会麻烦到引入一个更复杂的框架来处理。可要是开发一个大型的分布式的程序呢?
三、解耦的层次
从上面的分析可以看到,其实解耦是可以划分成以下几个层次的:
1、设计层
从工作的开始和阶段性的结束,以设计开始,以重构反馈,就把解耦放到一个重要的位置
2、代码层
其实就是考验开发者的编程思想和编程水平以及对设计者的思想的一个融合过程
3、框架层
到这一层其实就是一种宏观上的控制,更倾向于一种服务的隔离解耦即抽象的解耦
所以解耦是一个全生命周期的过程,需要不断的在重构和迭代中不断的进行。
四、实际应用
下面看一个依赖注入的解耦:
#include <iostream>
#include <memory>
#include <string>
class IDataBase {
public:
virtual ~IDataBase() = default;
virtual void execSql(const std::string &strSql) = 0;
};
class MySqlDB : public IDataBase {
public:
void execSql(const std::string &strSql) override { std::cout << "MySqlDB run: " << strSql << std::endl; }
};
class OracleDB : public IDataBase {
public:
void execSql(const std::string &strSql) override { std::cout << "OracleDB run: " << strSql << std::endl; }
};
class DBControl {
private:
std::unique_ptr<IDataBase> dbc_;
public:
// Constructor injection
explicit DBControl(std::unique_ptr<IDataBase> dbc) : dbc_(std::move(dbc)) {}
void DoSomething(const std::string &sql) { dbc_->execSql(sql); }
};
int main() {
auto db = std::make_unique<MySqlDB>();
auto db1 = std::make_unique<OracleDB>();
DBControl db_con(std::move(db));
db_con.DoSomething("select * from test");
DBControl db_con1(std::move(db1));
db_con1.DoSomething("select * from test");
return 0;
}
注:注入的方法有很多种方式,此处使用的构造函数,还可以使用属性、函数和接口等。
至于使用反射可以看前面的反射相关示例代码,而框架层和库层的,这里就不举例了。
五、总结
前面提到过封装和相关设计的一些分析,在它们的实践的过程中,解耦其实就已经悄然的不请自来。面向对象开发中最重要一个方法是封装,可封装的优劣如何判定呢?其中一个重要的方法就是看封装的耦合度。解耦就是封装一开始就必须考虑的问题。
设计和开发实践是体现开发者解耦能力的一个验证器。而将解耦设计好与坏,可能在当下并不会产生多大的效果,但在二次开发和完善修改时,就会发现,解耦的重要性。人教人很难学会,事儿教人一次就会。一个道理!