C++关于循环依赖的问题:
循环情况:
class B;
class A {
public:
B b;
};
class B {
public:
A a;
};
若两个类之间存在循环依赖则在编译时会报错,原因是两个类中存在相互的调用,无法为两个类分配具体的空间。
即使已经进行了前向声明,但是A、B类完全依赖彼此,各自编译都需要彼此的完整定义,此时编译器是无法办到的。
1. 使用指针代替变量声明 (把完全依赖关系降级为不完全依赖)
因为指针类型就是四个字节,在编译时编译器知道A、B类所占内存空间的大小,故在编译时不会报错,唯一要求是各自调用函数的定义不要在类的声明体内,否则编译会报错。
class B;
class A {
public:
B *b;
};
class B {
public:
A *a;
};
2. 升级策略:既然A、B两个类相互包含说明A、B两个类的耦合度比较高,考虑抽象出父类,A,B作为派生类。
// *.h
class B;
class A {
public:
B *b;
int funA(); // funA中使用B,故此处无法得到B的完整定义,所以不能在声明体内完成定义;
int data_;
};
class B {
public:
A *a;
int funB(); // funB中使用A,故此处无法得到A的完整定义,所以不能在声明体内完成定义;
};
// *.cxx
int A::funA()
{
b = new B();
if(b) {
return b->funB(); // A的某个方法需要调用B的方法得到结果
}
return -1;
}
int B::funB()
{
a = new A();
if(a) {
return a->data_; // B的某个方法需要调用A的数据
}
return -1;
};
A和B就这样耦合在一起, 虽然编译能够通过,但如果要进行单元测试,可以想象一下,测A需要B是完整的,B又依赖A,那A到底要怎么测试,再严重一点,A、B分别在不同的组件中,那面这两个组件因为这种循环依赖,就很难完成独立的单元测试,项目组就会勉强的把这两个组件绑在一起测试,而且再也分不开了,这是很糟糕的事情。
class Base { // 像这种,A、B为了使用各自的数据和方法,即将数据和方法抽出作为父类;
public:
int fun() { return data_; };
protected:
int data_;
};
class A : public Base{
public:
int funA() { return fun(); };
};
class B : public Base{
public:
int funB() { return data_;};
};
// A和B不再耦合,也可以进行独立的单元测试
3. 升级策略: 再一种常用的升级方式就是借助第三类抽离耦合部分函数
// *.h
struct Helper {
static int doFunA();
static int doFunB();
};
class A {
public:
int funA();
int data_;
};
class B {
public:
int funB();
};
// *.cpp
int Helper::doFunA() {
A a;
return a.data_;
}
int Helper::doFunB(){
B b;
return b.funB();
}
int A::funA() {
return Helper::doFunB();
}
int B::funB(){
return Helper::doFunA();
}
通过中间者Helper的静态方法分离A、B的耦合部分,这种方式在类耦合,还有组件耦合上使用最多,同理,当两个组件相互耦合时,也可以考虑一个中间者组件辅助他们完成调度,这也就是中介者设计模式。
4. lambda表达式
函数的调度对于调用者来说,其实只关心需要传入的参数、能够返回的结果,至于是哪个对象,内部如何实现的,调用者是无感的。
c++标准库自从引入函数绑定,lambda表达式后,像A、B这种循环依赖的,就可以利用。
#include <functional>
using CallBack = std::function<int()>; // 声明一个callback函数类型
class B {
public:
int funB(CallBack callback);
};
class A { // A 依赖B, B完全独立
public:
int funA();
B b;
int data_;
};
int A::funA() {
auto callback = [&]() -> int{
return data_;
};
return b.funB(callback);
}
int B::funB(CallBack callback){
// 可以追加各种业务处理
return callback(); // 再回调到A的处理,因为callback为新的函数类型,B不再依赖A
}
这样A就单向依赖B, B为独立的,这也是常用的一种解耦方式。
5. 降低策略,将共享或可重用的代码移到更低一层的C类中,A、B去依赖C。
class C {
public:
int fun(int x, int y) { return x + y; };
int data_;
};
class A {
public:
int funA() {
C c;
return c.fun(1, 2);
};
};
class B {
public:
int funB() {
C c;
return c.data_;
};
};
这种一般都是在类设计的粒度不够细的时候发生,降级一个C就是进一步让代码框架粒度细化。
6. 抽象基类,就是分离接口和实现。
class Base {
public:
virtual int fun() = 0 ;
};
class BaseImpl : public Base {
int fun() override;
};
class A {
public:
int funA() { return b->fun(); }; // 只调用B的接口,即使B的实现类内依赖了A
Base *b;
int data_;
};
//*.cpp
int BaseImpl::fun() {
A a;
return a.data_;
}
这个例子可能不太优良,但大概是这个意思,A依赖B的接口,B的实现类在函数内依赖A, 这样A、B之间就不再耦合。
这样即使是单元测试时,A也只是依赖B的接口,至于B的实现就可以通过Mock的方式实现,让A、B、BaseImpl都能够独立测试。