C++关于循环依赖的问题

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都能够独立测试。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值