C++ 设计模式:桥接模式 (V)
最编程
2024-04-08 20:19:09
...
1、定义与动机
- 桥模式定义:将抽象部分(业务功能)与实现部分(平台实现)分离,使他们可以独立地变化
-
引入动机:
- 由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个维度的变化
- 如何应对这种“多维度的变化”?如何利用面向对象技术来使用类型可以轻松地沿着两个乃至多个方向变化,二部引入额外的复杂度?
2、案例分析
假设现在存在这样一个需求:
- 有一个消息发送器Messager,里面有七个方法
- 大致需要完成登录、发送文本消息、发送图片消息、播放声音、建立连接等
- 同时由于存在不同的终端设备:例如手机、电脑、pad等,他们的这些实现方式肯定会存在一定的不同
- 并且对于同一种设备存在不同的功能组合问题:例如发送图片、消息之前播放声音等。
2.1、首先设计一个抽象接口
根据上面所提出的需求可以写出如下的接口代码,函数都是纯虚函数和虚函数组成
class Messager{
public:
virtual void Login() = 0;
virtual void SendMessage() = 0;
virtual void SendPicture() = 0;
virtual void PlaySound() = 0;
virtual void DrawShape() = 0;
virtual void WriteText() = 0;
virtual void Connect() = 0;
virtual ~Messager(){
}
};
2.2、针对不同的终端平台编码设计
- 例如针对PC、手机…平台设计它们的是如何发送文本、画图、播放声音、建立网络通信连接的代码
- 但是这些Base类依然是一个抽象基类,因为存在一些纯虚函数没有实现,因此其本身也是一个抽象类
// PC平台实现(抽象类),没有完全实现完毕所有的纯虚函数
class PCMessagerBase: public Messager{
public:
virtual void PlaySound(){
// *****
}
virtual void DrawShape(){
// *****
}
virtual void WriteText(){
// *****
}
virtual void Connect(){
// *****
}
}
// Mobile平台实现(抽象类),没有完全实现完毕所有的纯虚函数
class MobileMessagerBase: public Messager{
public:
virtual void PlaySound(){
// *****
}
virtual void DrawShape(){
// *****
}
virtual void WriteText(){
// *****
}
virtual void Connect(){
// *****
}
}
2.3、针对同平台不同量级的实现
- PCMessagerLite:轻量级实现,该实现类只完成最基本的功能,普通登录、发送图片文本消息
- PCMessagerPerfect:完美级实现,在执行这些操作之前可以辅助带一些播放声音等额外的功能
class PCMessagerLite: public PCMessagerBase{
public:
virtual void Login(){
PCMessagerBase::Connect();
// ******
}
virtual void SendMessage(){
PCMessagerBase::WriteText();
// ******
}
virtual void SendPicture(){
PCMessagerBase::DrawShape();
// ******
}
};
class PCMessagerPerfect: public PCMessagerBase{
public:
virtual void Login(){
PCMessagerBase::PlaySound();
PCMessagerBase::Connect();
// ******
}
virtual void SendMessage(){
PCMessagerBase::PlaySound();
PCMessagerBase::WriteText();
// ******
}
virtual void SendPicture(){
PCMessagerBase::PlaySound();
PCMessagerBase::DrawShape();
// ******
}
};
/*
MobileMessagerLite、MobileMessagerPerfect的实现如上
略过
*/
2.4、分析
-
很明显针对上面的代码一眼就能看出:存在大量的代码重复PCMessagerLite和MobileMessagerLite的代码区别完全就在于类名不同、继承的Base类不同。
-
这一问题很好解决:首先把继承变成组合,把继承的Base类组合到Lite和Perfect类中
-
此时只是子类不同:由于子类都来自同一个基类Messager,那么可以使用多态,进行成员声明类型的改变。
-
最后在使用时动态的传入所需要的不同平台的实现即可。
-
-
这样实现会导致一个类的急剧膨胀,假设只有1个Messager的接口、n个Base类、那么不同量级的实现是n * m的,总体是一个 1 + n + n * m + …
-
这一点和装饰器模式很像,不过装饰器模式是一个阶乘的爆炸,但这里的工作量也不低
3、桥模式
针对上面所发现的弊端进行改进,首先可以进行的改进
3.1、改进(一)
- 将继承改成组合的方式,可以很容易的写出PCMessagerLite、MobileMessagerLite的实现代码
class PCMessagerLite{
private:
PCMessagerBase* pcMessagerBase;
public:
virtual void Login(){
pcMessagerBase->Connect();
// ******
}
virtual void SendMessage(){
pcMessagerBase->WriteText();
// ******
}
virtual void SendPicture(){
pcMessagerBase->DrawShape();
// ******
}
};
class MobileMessagerLite{
private:
MobileMessagerBase* mobileMessagerBase;
public:
virtual void Login(){
mobileMessagerBase->Connect();
// ******
}
virtual void SendMessage(){
mobileMessagerBase->WriteText();
// ******
}
virtual void SendPicture(){
mobileMessagerBase->DrawShape();
// ******
}
};
3.2、改进(二)
- 然后其实可以将PCMessagerLite、MobileMessagerLite提炼成一个代码,这两份代码核心不点在于各自做组合的对象不同
- 然而恰巧不巧的事:它们所组合的对象都来自同一个基类,因此可以提炼成MessagerLite代码
class MessagerLite{
private:
Messager* messager;
public:
MessagerLite(Messager *msg): messager(msg){
}
virtual void Login(){
messager->Connect();
// ******
}
virtual void SendMessage(){
messager->WriteText();
// ******
}
virtual void SendPicture(){
messager->DrawShape();
// ******
}
};
-
但是写完这个代码会有一个问题:基类PCMessagerBase和基类MobileMessagerBase是抽象类(没有完全实现所有的纯虚函数),而抽象类是无法实例化对象的,因此这个代码是有问题的,无法过编译。
-
仔细分析问题核心所在点:
- 一路下来,我们只在最后不同量级的代码编写实现Messager抽象类的login、SendMessage、SendPicture三个纯虚函数
- 之前的那些Base基类并没有实现这三个方法才导致它们依然是一个抽象类
- 所以问题的核心点:Messager抽象类的接口方法太多,官方一点的术语:职责不够单一(违背单一职责原则)
3.3、完美改进(三)
- 为了解决单一职责问题,可以将Messager抽象接口的拆成两个抽象接口
- Base类继承实现一个接口
- 不同量级实现一个接口,然后组合Base类的指针(这里动态传入Base类不同的指针)
class Messager{
public:
virtual void Login() = 0;
virtual void SendMessage() = 0;
virtual void SendPicture() = 0;
virtual ~Messager(){
}
};
class MessagerImpl{
virtual void PlaySound() = 0;
virtual void DrawShape() = 0;
virtual void WriteText() = 0;
virtual void Connect() = 0;
};
// PC平台实现
class PCMessagerBase: public MessagerImpl{
public:
virtual void PlaySound(){
// *****
}
virtual void DrawShape(){
// *****
}
virtual void WriteText(){
// *****
}
virtual void Connect(){
// *****
}
}
// Mobile平台实现
class MobileMessagerBase: public MessagerImpl{
public:
virtual void PlaySound(){
// *****
}
virtual void DrawShape(){
// *****
}
virtual void WriteText(){
// *****
}
virtual void Connect(){
// *****
}
}
class MessagerLite: public Messager{ // 实现抽象类
private:
MessagerImpl* messagerImpl; // 组合对象
public:
MessagerLite(MessagerImpl *msgi): messagerImpl(msgi){
}
virtual void Login(){
messager->Connect();
// ******
}
virtual void SendMessage(){
messager->WriteText();
// ******
}
virtual void SendPicture(){
messager->DrawShape();
// ******
}
};
class MessagerPerfect: public Messager{ // 实现抽象类
private:
MessagerImpl* messagerImpl; // 组合对象
public:
MessagerPerfect(MessagerImpl *msgi): messagerImpl(msgi){
}
virtual void Login(){
MobileMessagerBase::PlaySound();
MobileMessagerBase::Connect();
// ******
}
virtual void SendMessage(){
MobileMessagerBase::PlaySound();
MobileMessagerBase::WriteText();
// ******
}
virtual void SendPicture(){
MobileMessagerBase::PlaySound();
MobileMessagerBase::DrawShape();
// ******
}
};
- 这样就能完成所有的功能:其核心点在于使用继承 + 组合的模式取代单一的继承方式来实现所有的功能
- 采用桥模式的设计方式将类的膨胀改写成:1 + n + m的形式,整整比普通模式少了至少一个数量级的代码
4、总结
- Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,即“子类化”它们。
- Bridge模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类存在多个变化的原因和方向),复用性比较差。Bridge模式是比多级车工方案更好的解决方案。
- Bridge模式的应用一般在“两个非常强的变化维度”,有时一个类也可以有多余两个的变化维度,这时可以使用Bridge的扩展模式。
推荐阅读
-
.NET框架设计和实施说明6.5 - ASP.NET Core应用程序的多种运行模式V - Kestrel的其他说明
-
一万字设计模式:抽象工厂模式--"V我50,一键生成家庭水桶
-
C++ 设计模式 (1) - 工厂模式
-
C++ 设计模式:桥接模式 (V)
-
组合模式(Composite)的 C++ 设计模式
-
适配器模式:桥接不兼容的接口-结论
-
设计模式 (6):桥接模式
-
首页宽带光猫转换桥接模式教程-Kiyun博客
-
Ubuntu 20.04 物理机 QEMU-KVM + 虚拟管理器 在桥接模式下创建虚拟机
-
对话NGC蔡岩:从机制创新到价值沉淀,解析DeFi产品开发逻辑 |链捕手 - 真正的DeFi产品首先要有足够的安全性和稳定性,如果能在此基础上有一些功能创新,那就非常好了。像 Uniswap 这样逐渐成为 DeFi 基础架构的产品,可遇而不可求。 链式捕手:固定利率协议之前关注度比较高,但观察下来发现,大部分协议还是类似于传统金融CDO(抵押债务凭证)的玩法,风险系数很高,您如何理解这块业务的价值和风险? 蔡岩:确实有些定息协议类似CDO玩法,背后绑定一个债券,但并不是所有的定息协议都是这样的玩法,像这种CDO玩法的主要代表项目是88mph,背后绑定的是Aave、Compoud这样的借贷协议,在此基础上做定息和浮息债券;像APWine,背后同样是Aave,它会发行期货收益代币来锁定你的收益;Notional本身是做借贷市场的,在此基础上做定息协议。 非 CDO 的玩法,比如 Horizon,更像是一个利率撮合器,背后需要用户通过拍卖产生更合适的目标收益率;像 Saffron、BarnBridge 等是通过风险分级来定义不同的收益率。总的来说,创新还是挺多的。 价值层面是创新和想象力,因为在传统金融领域,比如银行做固定收益证券,或者评级机构给风险分级,这些业务都非常大,利润也很丰厚。而 DeFi 的对口业务给了类似业务很大的想象空间。尤其是固定利率协议的成熟产品不多,尝试各种微创新是很有意义的。 风险程度还是要具体到不同的玩法,比如,在 Aave、Compoud 等借贷协议的固定利率协议背后,如果这些借贷协议受到攻击,与之绑定的固定利率协议也会受损。 同样,如果自己做借贷市场,可能更需要更强的开发能力。再有,如果该程序的机制或参数设计不当,同样会导致协议运行不稳定,并可能造成大量用户清盘。 总的来说,风险在于固定利率协议的设计,这是一个非常复杂的过程,需要不断地尝试和出错。 链式捕捉器:刚刚提到背后是Aave/Compound的固定费率协议风险较大,您认为Aave最大的不确定性和创新点分在哪里? 蔡岩:其实爱钱进一直被认为是走在行业前列的项目,他们的迭代速度非常快,比如率先尝试闪贷、推出新的经济激励模式、推出目前业内首个安全模块、尝试L2解决方案等等。 而在主要的借贷业务上,他们又十分谨慎,比如在抵押率、清算系数等风险参数的设计上相对于其他借贷协议较为保守,并不会存在为了吸引更多借贷资金而降低风险的要求。 与许多 DeFi 项目一样,即使 Aave 进行了多次审计,也无法保证不存在漏洞。前段时间,Aave 刚进入 V2 阶段时,白帽黑客就指出了某个漏洞。 之前的创新点可能是闪电借贷,这是当时业内独一无二的新产品功能,也为 Aave 带来了不少收益。当然,也有人批评闪电贷只能方便黑客实现资金效益的最大化,但工具本身并没有错,未来闪电贷肯定会有更多的应用场景。 其次是安全模块的设计,这有点像项目本身的储备金库,保障项目的安全性,这也是爱维开创的先河。说实话,目前大多数项目都没有做到代币模式的良性或正向运营,也做不到像Aave一样的安全模块,这是一个不小的门槛。 Chaincatcher从某种程度上来说,挖矿模式是DeFi财富效应的根本支撑,但Aave的CEO却说挖矿机制带来的动力是不可持续的,您怎么看这个观点? 蔡岩:"挖矿机制 "不可能失效,因为它是一种激励机制,或者说是项目冷启动的一种方式。但流动性开采亚博体育手机客户端不会一直高涨。比如去年11月的流行性挖矿高APY持续了一两个月就崩盘了,导致DeFi市场大幅回调。 Aave、Uniswap、Synthetix等项目真正爆发进入市值前15名也是在今年2月,我更倾向于这是头部DeFi长期价值的体现。虽然大家都喜欢抢高APY的矿机,但我个人很少参与挖矿,所以我并不觉得流动性挖矿是DeFi的基本面支撑。