C++设计模式 #6 桥模式(Bridge)

动机

  • 由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个变化的维度。

如何应对这种“多维度的变化”?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至多个方向变化,而不引入额外的复杂度

举个栗子

我们有一个发送消息的抽象基类

class Messager{
public:virtual void Login(string name, string password) = 0;virtual void SendMessage(string message) = 0;virtual void SendPicture(Image image) = 0;virtual void PlaySound() = 0;virtual void DrawShape() = 0;virtual void WriteText() = 0;virtual void Connect() = 0;virtual ~Messager() {}
};

在这种业务场景下,我们需要发送消息实现登录,发送文字,发送图片的功能。同时也可能有播放声音等等其他的功能需求。

针对不同的平台,我们需要不同的实现逻辑来完成基础需求。

class PCMessagerBase : public Messager{
public:virtual void PlaySound(){//PC平台的实现逻辑}virtual void DrawShape(){//PC平台的实现}virtual void WriteText(){//PC平台的实现}virtual void Connect(){//PC平台的实现}
};class MobileMessagerBase : public Messager{
public:virtual void PlaySound(){//Mobile平台的实现逻辑}virtual void DrawShape(){//Mobile平台的实现}virtual void WriteText(){//Mobile平台的实现}virtual void Connect(){//Mobile平台的实现}
};

针对具体的不同业务场景,我们需要有不同的逻辑。比如说,我们需要一种精简版的逻辑,同时需要一种完美版的逻辑,类似针对非会员和会员的不同处理😀

//业务逻辑
class PCMessagerLite : public PCMessagerBase{
public:virtual void Login(string name, string password){PCMessagerBase::Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message){PCMessagerBase::WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image){PCMessagerBase::DrawShape();    //发送图片前对图片的处理等等//...}
};class PCMessagerPerfect : public PCMessagerBase{
public:virtual void Login(string name, string password){PCMessagerBase::PlaySound();PCMessagerBase::Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message){PCMessagerBase::PlaySound();PCMessagerBase::WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image){PCMessagerBase::PlaySound();PCMessagerBase::DrawShape();    //发送图片前对图片的处理等等//...}
};

perfect版本在lite版本的基础上,可能有一些其他的行为,比如说播放一段音乐等等。

//业务逻辑
class MobileMessagerLite : public MobileMessagerBase{
public:virtual void Login(string name, string password){MobileMessagerBase::Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message){MobileMessagerBase::WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image){MobileMessagerBase::DrawShape();    //发送图片前对图片的处理等等//...}
};class MobileMessagerPerfect : public MobileMessagerBase{
public:virtual void Login(string name, string password){MobileMessagerBase::PlaySound();MobileMessagerBase::Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message){MobileMessagerBase::PlaySound();MobileMessagerBase::WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image){MobileMessagerBase::PlaySound();MobileMessagerBase::DrawShape();    //发送图片前对图片的处理等等//...}
};

在移动端平台上也是一样的。业务流程是一样的,可能有一些实现调用了MobileMessagerBase基类的方法。

存在的问题

现在存在的类的关系是这样的。这样的类中间存在的大量的重复代码,比如PCMessageLite和MobileMessageLite的Login函数中,逻辑明显是一样的,唯一的区别在于调用的基类的Connect函数不同。

这样明显是一种不好的设计。这与我们之前写过的装饰模式中的问题非常类似。C++设计模式 #5 装饰模式(Decorator)-CSDN博客

重构

如果参考装饰模式(Decorator)的方式,我们可以将代码重构成以下这种形式。

class MessagerLite{Messager* messager;		//在运行时 = new PCMessagerBase() 或者  MobileMessagerBase()
public:virtual void Login(string name, string password) {messager->Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message) {messager->WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image) {messager->DrawShape();    //发送图片前对图片的处理等等//...}
};class MessagerPerfect {Messager* messager;		//在运行时 = new PCMessagerBase() 或者  MobileMessagerBase()
public:virtual void Login(string name, string password) {messager->PlaySound();messager->Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message) {messager->PlaySound();messager->WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image) {messager->PlaySound();messager->DrawShape();    //发送图片前对图片的处理等等//...}
};

看起来这种方法是可行的,但是注意这种方法存在着致命的缺陷。

PCMessagerBase和MobileMessagerBase这两个类,只重载了Messager类中的两个三个方法,另外三个依然是纯虚函数。PCMessagerBase和MobileMessagerBase这两个类依然是纯虚基类,它们是不可以被实例化的,也就是说,我们在运行时是无法初始化MessagerLite的messager指针为PCMessagerBase的。

造成这种问题的原因是,Messager的这些函数放在一个类中,并不合适。我们将代码彻底重构成如下形式

class Messager {
protected:MessagerImp* messager;		//在运行时 = new PCMessagerBase() 或者  MobileMessagerBase()
public:virtual void Login(string name, string password) = 0;virtual void SendMessage(string message) = 0;virtual void SendPicture(Image image) = 0;virtual ~Messager() {}
};class MessagerImp {
public:virtual void PlaySound() = 0;virtual void DrawShape() = 0;virtual void WriteText() = 0;virtual void Connect() = 0;virtual ~MessagerImp() {}
};class PCMessagerBase : public MessagerImp {
public:virtual void PlaySound() {//PC平台的实现逻辑}virtual void DrawShape() {//PC平台的实现}virtual void WriteText() {//PC平台的实现}virtual void Connect() {//PC平台的实现}
};class MobileMessagerBase : public MessagerImp {
public:virtual void PlaySound() {//Mobile平台的实现逻辑}virtual void DrawShape() {//Mobile平台的实现}virtual void WriteText() {//Mobile平台的实现}virtual void Connect() {//Mobile平台的实现}
};//业务逻辑
class MessagerLite : public Messager{
public:virtual void Login(string name, string password) {messager->Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message) {messager->WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image) {messager->DrawShape();    //发送图片前对图片的处理等等//...}
};class MessagerPerfect : public Messager{
public:virtual void Login(string name, string password) {messager->PlaySound();messager->Connect();    //在登录前与服务器等等保持连接//...}virtual void SendMessage(string message) {messager->PlaySound();messager->WriteText();    //在发送消息前进行消息的输入//...}virtual void SendPicture(Image image) {messager->PlaySound();messager->DrawShape();    //发送图片前对图片的处理等等//...}
};

当前类的关系是这样的,我们成功的将业务上的扩展(MessagerLite/MessagerPerfect)与平台上的扩展(PCMessagerBase/MobileMessagerBase)两个方向上分开。用(n+m)数量的类,实现了(n*m)的功能。

模式定义

  • 将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。——《设计模式》GoF

同样红色的部分是稳定的,蓝色的部分是变化的。

体现在我们上面的代码中,就是Messager类与MessagerImp类中间搭了一座桥。使得业务功能与平台扩展两个方向上可以分开变化。

总结

  • 桥模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度变化。所谓抽象和实现沿着各自维度变化,即“子类化”
  • 桥模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性较差。桥模式是比多继承方案更好的解决办法。
  • 桥模式一般应用于“两个非常强的变化维度”,有时一个类有多于两个的维度变化,这时也可以使用桥模式的扩展模式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/442965.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c99什么意思_C语言中-是什么意思啊?

展开全部->是一个整体,它是用于指向结构体、C中的class等含有子数据的指针用e5a48de588b662616964757a686964616f31333366303130来取子数据。换种说法,如果我们在C语言中定义了一个结构体,然后申明一个指针指向这个结构体,那么…

mysql外键关联查询_MySQL外键约束和多表联查

一.创建外键#测试数据表# 教师表,主表CREATE TABLE teacher(id INT PRIMARY KEY AUTO_INCREMENT,name varchar(20),age INT)ENGINEInnoDB DEFAULT CHARSETUTF8;#测试数据:INSERT INTO teacher values(1,范冰冰,22),(2,周杰棍,30),(3,双杰伦,35),(4,梁朝伟…

mysql innodb id_MySQL InnoDB row_id边界溢出验证的方法步骤

背景跟同学聊到row_id一个边界问题,这里详细说明下。InnoDB表若没有定义主键,会使用系统的一个默认递增row_id (dict_sys->row_id)作为主键。每次插入一行加1,到达最大值循环复用。需要注意的是,虽然dict_sys->row_id 被定义…

mysql mysqld.sock_MySQL笔记-最简单的方法来解决找不到mysqld.sock文件的问题

首先,环境:ubuntu 14.04,采用apt-get的方式安装的,手动安装可能路径设置稍有区别。1、安装MySQL后,用命令行首次启动时发现找不到Mysqld.sock文件,提示:ERROR 2002 (HY000): Cant connect to local MySQL server throu…

plan explorer mysql_plan explorer支持oracle吗

展开全部1.SQL语句的执行62616964757a686964616fe58685e5aeb931333361326365计划使用EXPLAIN PLAN语句来确定Oracle数据库下指定SQL语句的执行计划,这个语句插入每一步执行计划的行描述到指定表中。你也可使用EXPLAIN PLAN语句作为SQL跟踪工具的一部分。EXPLAIN PLA…

python 列表转图结构_Python读取网络(图)边列表数据进而转化为邻接矩阵

import networkx as nxG nx.Graph()path ./edge_list.txtedge_list []node_set set() #集合的特性就是元素不会重复,互异的with open(path, r) as f:for line in f:cols line.strip().split( )y1int(cols[0])y2int(cols[1])node_set.add(y1)node_set.add(y2)ed…

r语言读写word_R语言:在word中插入ggplot

最近CRAN上新了一个叫eoffice的package,并且不时被各路大佬提起。这个包的功能刚好也符合我最近的需求,这次带各位先来试试水。包的官方介绍:1. Introduction​cran.r-project.org这次主要试试在word中用该包插入ggplot。既然要试&#xff0c…

hamburger组件_一个侧边栏导航组件实现思路

翻译:布兰作者:Adam Argyle来源:https://web.dev/building-a-sidenav-component/在这篇文章中,我想和大家分享我是如何为 web 原型化一个 Sidenav 组件的,这个组件是响应式的,有状态的,支持键盘…

centos php mysql 5.6 安装_centos7安装nginx、php5.5、mysql5.6

一、nginx1、安装yum install nginx2、启动systemctl start nginx关闭:systemctl stop nginx 重启:systemctl restart nginx 检查状态:systemctl status nginx3、测试浏览器直接访问http://ip,应该会看到以下界面:4、支持php打开/…

myeclipse怎么导入mysql驱动_myeclipse sql导入数据库驱动包

如何配置strutshibernate,基本使用方法不少童鞋在自学SSH框架的时候,难在创建第一个项目,如何搭建好这些框架,很多书上只是给出了代码但是没有教如何使用,所以在本次博客中将会图文结合来说一下如何使用struts结合hibe…

创建或更改表 tablename 失败_mysql 创建用户

一. 创建用户命令:CREATE USER usernamehost IDENTIFIED BY password;说明:username:你将创建的用户名host:指定该用户在哪个主机上可以登陆,如果是本地用户可用localhost,如果想让该用户可以从任意远程主机登陆&#…

sql2000 mysql 兼容_SQL Server2000如何恢复数据库

以里诺仓库管理软件(SQL网络版)为例,如果您因电脑操作系统重装,需要把以前备份的数据库恢复过来,请您按如下操作来。首先,您需要安装MS SQL Server2000。1. 以Windowns XP为例,SQL Server个人版安装完成后,…

java dfs_Java数据结构与算法 深搜(DFS)的简单使用(一)之排列组合

今天,我们来简单介绍一下深度优先搜索(DFS)的概念和使用。在百度词条中,对深搜的解释是这样的。百度词条中的解释由此,我们可知,深搜是广泛运用到 图 中的搜索方法之一。用深度优先搜索遍历图的基本思路是:(1)访问顶点…

java 线程执行结束_Java_如何等待子线程执行结束

本程序的数据有可能是如下:main thread work startsub thread start working.main thread work done.now waiting sub thread done.sub thread stop working.now all done.忽略标号, 当然输出也有可能是1和2调换位置了. 这个我们是无法控制的. 我们看下线程的join操作, 究竟干了…

mysql将时间轴转化为时间_MySQL日期计算及格式转换有关问题

mysql日期计算及格式转换问题2012-06-09 21:08 MySQL日期计算及格式转换问题做开发的时候经常会碰到以下几个问题使用mysql的内置函数将时间轴转成对应的日期方法一:使用from_unixtime(unix_timestamp)函数即可实现,如:SELECT FROM_UNIXTIME(…

java语言特点 字符串不变_面试必问:Java中String类型为什么设计成不可变的?

这几天在各大平台上都看到过这样一些帖子,全都是关于String类型对象不可变的问题,当然现在也是找工作的准备时期,因此花了一部分时间对其进行整理一下。想要完全了解String,在这里我们需要解决以下几个问题(1)什么是不可变对象&am…

java socket android_Android:这是一份很详细的Socket使用攻略

前言Socket的使用在 Android网络编程中非常重要今天我将带大家全面了解 Socket 及 其使用方法目录示意图1.网络基础阅读本文前,请先了解 关于计算机网络基础,如计算机体系结构、TCP、UDP等知识2. Socket定义即套接字,是应用层 与 TCP/IP 协议…

内构函数java_Android JNI参数传递

Java中调用native函数传递的参数是Java数据类型,到了JNI层需进行数据类型转换,基本数据类型是在前面加个j,如int——>jint,应用数据类型除了基本数据类型的数据、Class、String和Throwable外,其余所有Java对象的数据…

java 垃圾回收机制_Java的垃圾回收机制

前言在C语言中, 程序员必须小心谨慎的处理每一项内存分配, 且内存使用完后必须手动释放曾经占用的内存空间。当内存释放不够完全时, 即存在分配但永不释放的内存块, 就会引起"内存泄漏"问题。而在Java语言中, 它给了程序员一个美好的承诺: 程序员无需管理内存, 因为J…

java闹钟程序声音_跪求高手帮忙写一个JAVA手机闹钟程序 实现添加铃声和设置多闹钟...

展开全部import java.util.*;import java.awt.*;import java.applet.*;import java.text.*;public class AlarmClock extends Applet implements Runnable{Thread timernull; //创建线程timerImage clockp,gif1,gif2,clock6,clock7; //clockp:闹钟的外壳,闹铃和e68a…