5. 设计模式
先分析一下设计模式在项目流程中的作用,一个正常的公司项目大体流程一般为如下:
业务需求
从项目的产生,无论是甲方还是乙方,军工还是民用,项目的来源都不是凭空捏造的,必定有其遇到的问题。一般产品经理会跟客户沟通拟定项目的需求。甲方相对于专业的开发人员来说,通常为不懂技术或了解不多,一般产品经理会抽象甲方的业务逻辑形成文稿。最终和家甲方商讨形成《需求说明书》移交项目经理进行项目评估。
项目评估
通常项目经理针对《需求说明书》针对公司现有的人员和资源进行项目评估,其中还有项目架构师的参与(通产项目经理和架构师为同一人),最终确定项目的大体方向,比如效率高采用什么框架和语言实现,涉及的人员和时间周期,最终产生《项目计划》《技术服务合同》等。同时和甲方签订合同。
需求分析
技术层面的需求分析,按照《项目计划》项目经理、技术经理、架构师(通常三个角色为一人)相关技术人员同时展开项目架构相关会议,从整体架构和各个模块推导形成完整的敲定《项目方案》,通常为多个实现方案,取其最优方案作为优先执行。这里涉及软件的整体框架和模块组合,其中运用大量的软件设计模式进行相关性结构保证软件开发原则,例如:OA软件系统开发设计九大原则。
开发产品
最为广大搬砖工最关心的一环,上面的需求分析不会对每个class的关系和结构进行细分,一个好的程序员应该具备的素质,模块任务分配到手,首先需要抽象业务需求,我这个模块是干嘛的,在整个系统中有什么作用。思考完这些问题,你会发现一些隐藏的坑,可能载现目前体会不到,随着代码的不断堆积,你会发现代码越来越难敲,有时候甚至想推掉重新来过,载这里亲身体会,特别是存在界面和数据业务上,如果没有采用MVC模式,你的数据传递和界面没有分离,那么基本告别迭代。有人说大公司模块细分你学不到什么东西,可能我没有多大的体会,只有自己愿不愿意去跟深层次的了解场景罢了。多想想你所涉及的模块内的场景,有些东西是在需求分析中所不能体现的,比如你的同事用了原型模式,让自己的代码可拆卸组合,而你没有分离,假设存在V2版本,谁的开发效率会更高,同事准时下班,而你还在苦逼的重构代码,还有运用合理的设计模式将会对代码质量有飞跃式提升,例如界面N个贴图Qlable,那么工厂模式就用起来啊,你在你的QWidget中定义一堆Qlabel,明眼人一看代码:“代码真菜”。
交付产品
交付产品可能遇到的问题,甲方:“你这个界面可不可以加个类似某音的商城跳转按钮?就加个按钮而已没那么难吧”。你:“阿玛尼”。如果你是国际大厂没的说了签新合同,可惜,有多少人进了大厂?设计模式让你的代码解耦,更容易更改。
后期维护
有多少人看自己一年前的代码都不忍心看?何况后期维护很有可能不只一年后期维护也牵扯代码查找与更改,这便是体现设计模式的优越性。
项目交接
设计模式相当于业内的代码潜规则,好的代码体现在移交项目注释和设计模式上面,你所写的代码就是你的脸,接手的工程师看到代码言简意赅,设计模式可以减少写代码的行数,通过潜规则更清楚表述自己的代码,看不懂我的代码?That is impossible!你怕是要去学学软件设计模式了。
业务终止
到这里一般都是项目完全脱离。
综上所诉,软件设计模式带来的好处就是,让你的代码有前瞻性,有限的程度上符合日益变更的需求,少些废代码,理解容易(业内潜规则),甩锅与装逼(这当然是说笑了)!!!
5.1. 创建型模式
工厂模式(Factory Pattern)
创建者模式:这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
工厂模式(Factory Pattern) 我的第一个项目就是实现贴图的QListWidget其中不免含有item中元素的各种操作,比如单击事件双击和事件移动到上面会出现气泡什么的,下面以QQ群成员列表为例
这里看到成员2816个人,每人为每一项也就存在2816个项,每一项有三个元素,分别头像QLable,名称QLabel ,身份QLabel,不妨把Item作为工厂生产的产品,那么我们通过QString(头像路径),QString(成员名称),QString(身份标示)为一组生成不同产品(这里的不同指的是界面显示,而非class类的不同),下面我用伪代码说明。
typedef struct ItemMess
{
QString ImgPath;
QString name;
QString idImgPath;
}
QVector<ItemMess> MessContainer=
{
{"Path::一去二三里的头像","西安-一去二三里","Path::群主的图片"};
{"Path::北京-青春的头像","北京-青春","Path::管理员的图片"};
{"Path::上海-叶海的头像","上海-叶海","Path::管理员的图片"};
......;
} //这里定义一些初始化变量,然而在现实的需求中,肯定是来源数据库查询或者文件。
class Factory
{
public:
Factory(){}
QItemList generateItem(Vector<ItemMess> _itemMess)
{
QItemList itemlist;
foreach( auto var:_itemMess)
{
QItem pointer = new QItem() //构造新项
pointer.setHeadImg(QLabel::loadImg(var.ImgPath)); //设置新项的头像图片
pointer.setName(QLabel::loadText(var.name)); //设置新项的昵称
pointer.setIdImg( QLabel::loadImg(var.name)); //设置新项的身份图片
itemlist.append(pointer); //把新项添加到列表中
}
}
}
int main()
{
Factory f;
QListWidget w;
w.setItemList( f.generateItem(MessContainer));
w.show();
return 0;
}
上面 QItemList Factory::generateItem(Vector _itemMess) 把输入数据作为当前QItemList 生成的依赖信息,生成最终的产品QItemList 然后返回数据项设置到QListWidget。其中Factory 扮演的角色如下:
抽象工厂模式(Abstract Factory Pattern)
开始剖析抽象工厂模式,不得不提到实际项目中肯定会遇到的几点矛盾: 1.刚开始框架铺开,实现代码的前期,会遇到class实现的功能不明确。 2.实现代码存在多种泛化。 3.既定接口实现class,通常用在C++插件上(常说的面向接口编程)
实现抽象工厂的难点:使用场景有限,需要考虑的class兼容性,纯虚函数限制,继承抽象工厂模式的class tree都需要实现相关的方法(无论是继承树的哪一层级都需要重新实现),推荐如果出现第3种矛盾可以采用,如果想要更加深入的了解抽象工厂模式,建议查看我的另外一篇文章QtPlugin(C++跨平台插件开发),特别是代码的多种泛化,千万千万最好别用它,不然你会绞死在自己未定义的错误上面(当然)。当然有更好的使用抽象工厂模式也欢迎一起探讨,对于IT新人可能不太容易理解抽象工厂模式,于是斟酌了一下,就多写一些话,如果看不太明白的话这这个设计模式可以直接跳过,同时抽象工厂模式的不同场景,所以很多存在相似的地方,说明得详细一点也无伤大雅。
接口抽象
#ifndef QABSTRACTBUTTON_H
#define QABSTRACTBUTTON_H
#include <QtGui/qicon.h>
#include <QtGui/qkeysequence.h>
#include <QtWidgets/qwidget.h>
QT_BEGIN_NAMESPACE
class QButtonGroup;
class QAbstractButtonPrivate;
class Q_WIDGETS_EXPORT QAbstractButton : public QWidget
{
Q_OBJECT
more .....
protected:
void paintEvent(QPaintEvent *e) Q_DECL_OVERRIDE = 0;
virtual bool hitButton(const QPoint &pos) const;
virtual void checkStateSet();
virtual void nextCheckState();
more .....
};
QT_END_NAMESPACE
#endif // QABSTRACTBUTTON_H
直接上Qt自带的QAbstractButton文件可能稍微好理解一些,所有泛化的button class都继承QAbstractButton。相信在Qt官方组编写Button怎么实现不同类型的按钮也思考了这些问题,但是尽量会思考得完整一些。因为抽象嘛,到时候继承抽象类得实现一堆代码,能够略过抽象落到实现方法上是最好的。
上面是抽象类抽象方法的实现结构图,按照道理来说,我们并不知到我们要实现什么button的界面绘制效果(这里接触过Qt的同僚应该知道,点击了不同类型button在界面上的显示是不一样的),我们预留一个(当然可以多个)接口作为不同button自己的内部实现paintEvent()。这就是抽象到实例化方法,而QAbstractButton就是一个抽象工厂。
面向接口编程 前段时间有一个伙计我现在实现了实例化的class,要怎么去用上抽象工厂模式呢,我的回答是知道实例没必要再反着推抽象了。如果是为了统一接口反过来用过面向接口编程这种抽象工厂也是没有错的。面向接口编程为了切合实际,我们首先先假定一个场景。场景如下: 我们想要做一款摄像头识别文字然后显示到任意的显示设备上。为了扩展摄像头的不同型号和和显示设备的不同型号,首先我们肯定过不会先去实现摄像头和显示设备的内部处理采集到的数据流代码。
enum DeviceType //设备类型枚举标记
{
DT_Camera = 0,
DT_Display
};
class AbstractDevice //通常接口会是叫做 class InterfaceDevice 这里定义成抽象工厂的名称实则是一样的
{
public:
virtual bool open() = 0;
virtual bool close() = 0;
virtual QByteArray readAll() = 0;
virtual DeviceType getDeviceType() = 0;
QString deviceName(){return m_StrDeviceName;}
private:
QString m_StrDeviceName; //不一定抽象工厂里面不能定义其他实际变量,因为经常继承可以会用到
};
/*定义了一个抽象的设备类型,假设我们不知道我们的设备到底是什么样的设备(包括摄像头和显示设备),而枚举类型的标记中没有
DT_Camera = 0,
DT_Display
整个DeviceType都为空,
*/
class AbstractCamera:public AbstractDevice //抽象摄像头类型
{
public:
DeviceType getDeviceType(){return DeviceType::DT_Camera;} //标记设备为摄像头
private:
};
class AbstractDisplay:public AbstractDevice
{
public:
DeviceType getDeviceType(){return DeviceType::DT_Display;}
};
class Camera_1080P:public AbstractCamera //最终实现1080p的摄像头
{
public:
Camera_1080P(){}
bool open(){}
bool close(){}
QByteArray readAll(){}
};
class Display_LED:public AbstractDevice //最终是实现LED的显示屏
{
public:
Display_LED(){}
bool open(){}
bool close(){}
QByteArray readAll(){}
};
为了加深映像我代码中用两次抽象class继承,你会发现载上面的class中除了Camera_1080P和Display_LED我都没有实现构造函数,因为接口的编写都是抽象的, 不允许实际的构造。而继承到最终的实际设备中,我们实现了所有的抽象方法,从而产生了置顶而下的构造。
对外的接口:
bool AbstractDevice::open();
bool AbstractDevice::close();
QByteArray AbstractDevice::readAll();
DeviceType AbstractDevice::getDeviceType();
QString AbstractDevice::deviceName();
那么疑问来了,我为什么采用了这种方式去抽象有继承实现?不得不提到的一个子类可以转父类型,然后通过父类的接口进行子类的调用,这也称作代码的闭包,相信都有所耳闻微服务框架,那么C++有没有呢?肯定是有的。那就是插件机制,通过把每一个class都封装成dll,然后通过顶层预留接口进行操作dll中的代码。调用流程图如下:
屏蔽编译器和C++的实现机制,上层结构如果用底层的汇编来杠那就没意思了,更为详细的dll封装移步QtPlugin(C++跨平台插件开发)或者百度CTK框架,你会学习到C++的微服务框架。
单例模式(Singleton Pattern)
这个模式应该是广大同僚用的最多的。通过屏蔽对外的构造函数实现,场景不由分说,作用于当前程序只能存在一个class实例,经常用在管理器(批量的new/delete class)之上。
懒汉单例
// ClassManager.h
class ClassManager: public QObject
{
public :
static const ClassManager* getInstance();
void ClassRegister(const QObject * pointer); //其他公有供调用的方法
private:
ClassManager(){}
~ClassManager(){}
static ClassManager* This;
}
//ClassManager.cpp
#include "ClassManager.h"
static ClassManager* ClassManager::This = nullptr;//在调用getInstance()时构造
static const ClassManager* getInstance()
{
if(!This) This = new ClassManager;
else return This;
}
void ClassRegister(const QObject * pointer)
{
//你的一些操作
}
懒汉单例可以用在Widget类型的组件,因为new Widget class 需要在 Application之后
饿汉单例
// ClassManager.h
class ClassManager: public QObject
{
public :
static const ClassManager* getInstance();
void ClassRegister(const QObject * pointer); //其他公有供调用的方法
private:
ClassManager(){}
~ClassManager(){}
static ClassManager* This;
}
//ClassManager.cpp
#include "ClassManager.h"
static ClassManager* ClassManager::This = new ClassManager(); //直接构造
static const ClassManager* getInstance()
{
if(!This) This = new ClassManager
else return This
}
void ClassRegister(const QObject * pointer)
{
//你的一些操作
}
懒汉和饿汉只是进不进行class构造而已。
建造者模式(Builder Pattern)
看了N多教程解释这个专业名词,感觉举例子都太抽象了,比如什么肯德基套餐,实际代码呢,就是实单一的class 然后通过实例化形成一个包含多单一class的复合class,在Qt中存在这样的例子,比如界面 QComBobox ,为什么点击后会出现下拉列表?这个下拉列表是一个QListWidget,而把一或者多个对象组合成一个复合对象的过程,叫做建造过程,把这种方法论叫做建造者模式。所有的设计模式都是方法论!!! 在项目中建造者模式用的比较多,我有一个自定义实现的QFileSelectBox组件类似CMake-GUI 的FileSelect具有编辑时检索本地文件并且补全到下拉列表的功能,代码在Github上(这里不方便贴代码,因为代码实在是太多了)这里是传送门,建议认真学习设计模式的看官能够下载然后跑一跑,认真的分析代码的实现。
下面我实现最终的代码,拥有注释部分是建造者模式中构造的单一Class
class FileSelectBox : public QWidget
{
Q_OBJECT
public:
enum FileSelectType
{
SELECT_ALL = 0,
SELECT_DIRS = 1,
SELECT_FILES = 2,
};
explicit FileSelectBox(QWidget *parent = nullptr);
FileSelectBox(FileSelectType type,QWidget *parent = nullptr);
~FileSelectBox();
const QLineEdit* lineEdit();
const QPushButton* pushButton();
const QTableView* tableView();
const QDialog* fileDialog();
void setLineEditText(QString Url);
void setPushButtonText(QString DisplayTest);
private:
FileSelectLine* m_pLineEdit = nullptr; //本质上是一个继承QLineEdit重构类型,为了实现的一些Signed
selectPopList* m_pTableView = nullptr; //本质上是一个QTableView,作为下拉列表的显示项
QPushButton* m_pPushButton = nullptr; //本质上是一个QPushButton,作为点击的按钮
QStandardItemModel* m_pItemModel = nullptr; //Item作为填充QTableView的数据项
FileSelectType m_eSelectFileType = SELECT_ALL;
QFileDialog* m_pFileDialog = nullptr;
int m_iItemWidth,m_iItemHeight = 30;
QPoint m_MovePos;
QPoint m_AfterPos;
virtual void resizeEvent(QResizeEvent* event);
virtual void keyPressEvent(QKeyEvent *event);
virtual void moveEvent(QMoveEvent* event);
virtual void paintEvent(QPaintEvent* event);
virtual void showEvent(QShowEvent *event);
virtual void hideEvent(QHideEvent *event);
virtual void closeEvent(QCloseEvent *event);
virtual void focusInEvent(QFocusEvent *event);
virtual void focusOutEvent(QFocusEvent *event);
void initWidget();
private slots:
void showPopList(QStringList strList);
void selectFile(QString path);
void showSelectFileDialog();
void hidePopList();
void enterKeyAddText();
signals:
public slots:
};
最后所有的类组合成为 FileSelectBox。这便是建造者模式
原型模式(Prototype Pattern)
原型模式重点在于重载 operator = (),实现拷贝构造实现快速生成一个当前类的副本目标,什么?没听过?那就放弃看这篇文章。先去看看C++的基础。 经常用在class拷贝,貌似好像没有怎么实现过,因为都是传Class地址。这个模式实现的可能就只有Qt中的QString class还有带有拷贝构造的容器类型,有其他的class或者实现的场景欢迎在评论区留言。 题外话:拷贝构造会生成新的class对象,如果在class传递中默认使用了class的拷贝构造(例如QString),那么程序将会在调用函数时溢出。虽然是题外话,但是仍然是应该打星号的重点。
5.2. 结构型模式
结构型模式:这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
适配器模式(Adapter Pattern)
假定一个场景:我们都知道QWidget类树族中Hide()和show()是隐藏和显示QWidget,基于这个方法做了一个QWidget界面显示和隐藏的管理类,原先的界面管理器结构,如下:
上面的结构可以控制所有Widget子类Hide和show。现在因为业务需求,引入了新的界面管理器(假设来自其他GUI界面框架)。 要求实现兼容两套界面管理器,如下(conceal:隐藏,spread:展示):
思维,构造QWidget的两个可兼容新管理类的接口函数
QWidget::conceal()
QWidget::spread()
但是我们总不能去更改QWidget源代码对吧,那么
QWidget::conceal() => Interface::conceal()
QWidget::spread() => Interface::::spread()
继承实现
class Interface:public QWidget
{
public:
explicit interface(QWidget* parent = nullptr):QWidget(parent){}
~interface(){}
void conceal(){hide();}
void spread(){show();}
};
我所构造的 Interface 就是一个适配器,适配的 QWidget 类,作为 InterfaceManaegr 和 QWidgetManager 中间媒介。 我通过 Interface 类型转换到父类型 QWidget 注册到 QWidgetManager 以供 hide() 和 show() 的调用,我也可以通过Interface注册到InterfaceManager 以供 conceal() 和 spread() 的调用。 你想要所有继承QWidget 的类要在 InterfaceManager中能够调用 conceal() 和 spread() ,你得更改继承QWidget为Interface。并且单根多重继承会提示。
重定义类套壳实现
class Interface
{
public:
Interface(QWidget* instance = nullptr)
{
m_pAdapterWidget = instance;
}
~Interface(){}
void setAdapterWidget(QWidget* instance){m_pAdapterWidget = instance;}
QWidget * adapterWidget(){return m_pAdapterWidget;}
void conceal(){m_pAdapterWidget->hide();}
void spread(){m_pAdapterWidget->show();}
void hide(){m_pAdapterWidget->hide();}
void show(){m_pAdapterWidget->show();}
private:
QWidget * m_pAdapterWidget = nullptr;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Interface intfc(new QWidget);
intfc.spread();
return a.exec();
}
通过Interface(QWidget*) 转换成新的 class; 套壳实现 Interface 只能通过调用 QWidget * adapterWidget() 获取私有的 m_pAdapterWidget 设置到QWidgetManager, 而直接兼容新的InterfaceManager。
多重继承的套壳实现
class Interface
{
public:
Interface(QWidget* instance = nullptr,QWidget* parent = nullptr)
{
if(parent) instance->setParent(parent);
if(instance) m_pAdapterWidget = instance;
}
~Interface(){}
void setAdapterWidget(QWidget* instance){m_pAdapterWidget = instance;}
QWidget * adapterWidget(){return m_pAdapterWidget;}
void conceal(){if(m_pAdapterWidget) m_pAdapterWidget->hide();}
void spread(){if(m_pAdapterWidget) m_pAdapterWidget->show();}
private:
QWidget * m_pAdapterWidget = nullptr;
};
class myButton :public Interface,public QWidget
{
public:
myButton(QWidget* parent = nullptr):
QWidget(parent),
Interface(nullptr,parent)
{
setAdapterWidget(this);
}
~myButton(){}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
myButton button;
button.spread();
return a.exec();
}
个人认为多重构继承的套壳实现看起来有鸡肋。 三种构造的方式的糙话体现:
继承实现:我爸爸原来是个两脚的插头(QWidget),发现我不是亲生的,我爸爸居然成了我爷爷(QWidget),并且(QWidget)告诉我的新爸爸有三角插头(Interface)同时是爷爷的儿子,于是我成了三角插头,我成功的通过意外继承插上了三孔插板; 套壳实现:我现在是个两脚插头(QWidget),我有个朋友他是个三角插头(Interface),我通过我的朋友插上三孔插板; 多重构继承的套壳实现:我的爸爸一个两脚的插头,我的妈妈是个三角插头,好了我现在继承了父辈的所有传统,我顺理成章的能够插三孔插板; 根据上面的解析,我觉得最能让人接受的是套壳实现,你别管我爸爸妈妈爷爷奶奶,我就找一个朋友帮我插三孔插板。
桥接模式(Bridge Pattern)
上面讲了适配器模式,可能会有些疑问 QWidgetManager 和 InterfaceManager 是干嘛的?桥接模式整好分拣一下这一块,顺带,整好最近开发的项目中用到了此模式。
过滤器模式(Filter、Criteria Pattern)
这里将会提到QSS和QSS选择器,在QtEvent中也运用了此设计模式进行事件分类与执行。