zoupaper的个人博客分享 http://blog.sciencenet.cn/u/zoupaper

博文

设计模式与现代C++ 连载(一)

已有 3342 次阅读 2019-3-18 20:11 |系统分类:科研笔记

1 概述

本博客的目的是总结自己编写C++程序的一些经验教训,为同仁提供一些参考。设计模式的重要性就不必说了。在经典的《设计模式-可复用面向对象软件的基础》一书中,四位作者以GUI软件作为例子,用C++98标准阐述了23种设计模式。但是2011年后,现代C++标准做了很大的变革,编程范式完全升级了。这本书并没有同步到C++11标准。市面上关于现代C++标准的书有一些,但是基于现代C++标准全面阐述设计模式的,至少我目前还没有见到。我这篇博客主要针对我自己用过的一些模式,提出一些实现和想法,因此肯定是不全面的。本博客的代码例子都是我自己写的,一定存在考虑不周之处。

由于现代C++的影响,国内影响力的作者对于设计模式、面向对象的思想做了很强有力的反思。例如陈硕的著作《Linux多线程服务端编程》;孟岩的博客《function/bind的救赎》等。我个人认为,可以不把设计模式绝对化,只要是好的经验,都可以是设计模式。设计模式也并不一定是面向对象的。例如,STL是典型的模板-泛型设计,不是面向对象的,这个似乎也可以作为通用库的一种设计模式。

设计模式究竟有很大的必要性吗?初学者读《设计模式》这本书,经常难以读懂,实际上,设计模式本身没有任何技术复杂性,大家觉得难是因为不具备这样的思维方式。当你真正有大量的开发经验的时候,就知道设计模式的重要性了。你见过有的程序有几百个全局变量,把所有的功能模块都耦合在一起,让你重构时无从下手吗?你见过混乱的继承体系,把原作者都绕晕的吗?你见过一个cpp文件有几万行,一个函数就有几千行吗?见得多了,就知道设计模式的重要性了。

下面我尝试从生理学的角度解释设计模式的作用。人的大脑思考机制,有些像计算机缓存-内存的机制。人在快速思考中不能处理过多的对象,如果思考的对象很多很复杂,需要把长期记忆区中的信息调取到缓存区,把缓存区的暂时不用的东西冲掉。长期记忆区与缓存区的切换越频繁,工作效率就越下降,而且下降的幅度很大。考虑一个设计糟糕的程序,你加一个简单的功能,都要修改十几处代码,那么,每切换一次修改位置,大脑都要重新建立该位置的工作场景和缓存;如果程序是良好设计的,有好的设计模式,那么,增加一个新功能,只需要在一个地方增加代码。这两者的工作效率差别非常惊人。有的大公司开发一个软件,几十个人干了一两年都不理想;同样功能的软件,小公司几个人就开发出来,而且打败了大公司,这样的例子实在太多了。

还可以从建筑的角度来类比。事实上,软件的结构与建筑的结构非常相似,设计模式这个词汇最初也来源于建筑。建设一个建筑物需要很多工人,每个工人职责范围都是很有限的。你看不到哪个工人一会在地基工作,一会儿又跑到顶楼;一会儿搅拌混凝土,一会儿开吊车。但是在软件工程领域,真的很多项目就是这样做事情的呢!

 

2 回调函数与function/bind

大家都熟悉调用函数,但是对回调却通常不熟悉。其实这涉及一个思维方式的转换。C++之父在《C++程序设计原理与实践(2)(进阶篇)》有一张图经典的说明函数调用和函数回调的区别。

                                               

常规函数与回调函数的主要区别是:常规函数何时调用,是函数本身的实现者可以决定的。而回调函数,是函数本身的编写者无法决定何时调用。上图GUI的例子是很典型的,你在GUI程序中设计一个点按钮功能的函数,但是你却不能决定何时调用这个函数。你必须设计成回调函数,注册到系统里面,由用户点击调用。

C++11中,提供了function/bind机制,可以实现回调函数。把函数调用的行为绑定再转换成function对象,然后对function对象就可以进一步处理了。(包括可以传值到函数里面,暂时存储下来,等等)。这种机制带来了巨大的灵活性,相当于行为机制的抽象化。function/bind为设计模式中的行为模式,提供了一种更简化的实现方法。

在经典的《设计模式》一书中,反复强调:要多用组合,少用继承。因为继承是强耦合的,一旦设计确定,后面就不好修改了。尤其是你设计出庞大的、有几百个类的继承体系,突然发现这个设计有重大缺陷,或者新的需求用已有的继承体系很难处理,那需要很大的重构成本。

但是传统设计模式对于虚函数的继承依然无法完全避免,因为C++的运行时多态只有这种处理方法。但是function/bind提供一种新的选择,我们可以只有具体类,没有继承,照样可以实现运行时多态。下面给出简单的例子(每个人都可以轻松看懂),分别:

#include<iostream>

#include<memory>

using namespace std;

 

class Base

{

public:

       virtual     void method() = 0;

};

 

class Base_A:public Base

{

       public:

       void method()

       {

              cout << "method_A" << endl;

       }

};

 

void Do_somthing(shared_ptr<Base> base)

{

    base->method();

}

 

int main()

{

  shared_ptr<Base> base = make_shared<Base_A>();

  Do_somthing(base);

}

上面这是采取虚函数继承的方式。

#include<iostream>

#include<functional>

using namespace std;

 

class Base_A

{

       public:

       void method()

       {

              cout << "method_A" << endl;

       }

};

 

void Do_somthing(function<void()>f)

{

  f();

}

 

int main()

{

  Base_A base_;

  Do_somthing(bind(&Base_A::method,&base_));

}

这个是采取function/bind的方式。采取function/bind可以简化继承体系,降低功能模块的耦合度。

无论是哪种方式,关键点是Do_something函数与具体的实现解耦,以后如果新增了Base_B方法,无需修改Do_something函数。

 

 

 




https://blog.sciencenet.cn/blog-3316223-1168229.html

上一篇:矩阵思维---从潮流计算说起
下一篇:设计模式与现代C++ 连载(二)
收藏 IP: 218.94.96.*| 热度|

1 魏焱明

该博文允许注册用户评论 请点击登录 评论 (1 个评论)

数据加载中...
扫一扫,分享此博文

Archiver|手机版|科学网 ( 京ICP备07017567号-12 )

GMT+8, 2024-3-29 21:10

Powered by ScienceNet.cn

Copyright © 2007- 中国科学报社

返回顶部