从抽象谈面向对象与泛型编程 part.1

1. 前言

抽象是我们常用的思维过程。一系列事物通过大脑的提炼,归纳和综合,让我们可以从无序中找出有序,从一个个具体的问题中找出通用的解决方法。编程中的抽象是从面相对象的程序设计,后文称为OOP,开始才有了比较完善的语言支持。如今软件复杂程度越来越高,OOP一直都是解决复杂软件设计的重要方法。使用OOP可以屏蔽各个任务具体的细节,抽象出一个简化而统一的流程,从而降低复杂软件开发的难度。这是一个建模的过程,也是OOP能够成功解决问题的原因。但是OOP的抽象能力是有限的,OOP的滥用反而会使复杂度无控制地上升。鄙文会浅讨OOP抽象适用的场景,并会阐述为什么作者觉得everyting is object是一个错误的设计。这个系列的文章后面还会探讨另外一种抽象能力更强大的范式,泛型编程GP。鄙文不是引战OOP与GP孰优孰劣,而是以抽象为线索,浅讨如何编写出能应付更加复杂程序的方法。

2. 传统OOP中的抽象

OOP有一个经典的案例。鱼会游泳,鸟会飞行,而它们都是动物。于是用OOP就很自然的有了下面的代码:

许多文章和书籍对OOP都是这样教的。从animal继承以扩展新的动物类,如哺乳类。从bird或fish继承,以扩展出具体的物种。当然还有许多文章也拿这个例子作为OOP的反面教材。因为当我们要实现飞鱼的时候,飞鱼既可以游泳,也可以飞行,这个继承结构无所适从。那么究竟是哪里出了问题?

熟悉OOP的人会说,这里违反了里氏里氏替换原则(LSP)。飞鱼属于鱼类,但是我们抽象的鱼类无法满足要求。那我们给鱼增加一个fly接口:

这能解决飞鱼这一个情况,但现实中好像还有更多复杂的情况。例如天鹅属于鸟类,但是它既会走路,也会飞行,当然也会游泳。这个时候我们又不得不扩充鸟类的接口。问题似乎变复杂了,绝大部分的鱼并不会飞行,但是为了兼容飞鱼我们为鱼类添加了fly接口。其实,真正的问题在于,我们为了实现一个可以游泳的动物,根据有限的经验抽象出了鱼类,并把这个动物归为鱼类。这种抽象的方式的代价,就是给实现类与抽象类之间,增加了一个is_a的关系,这个关系是一个很强的依赖关系。

对于我们举的这个例子而言,这种错误很容易发现,这是得益于动物这个话题是人们所熟知的。但是现实的问题往往都是未充分了解的,所以我们的抽象会很容易地违反LSP。其次,仅仅使用接口的异同类对事物进行抽象和归属,似乎并不是一个很好的主意。于是OOP有了改良,对行为的抽象看起来要更合理一些。

3. 接口编程与行为抽象

来到了接口编程,我们不再为类别进行抽象了,而是抽象行为。例如,上面的例子,我们不再为动物区分哺乳类,鸟类和鱼类等,也不会为鱼类实现飞鱼或者鲈鱼等其他具体的物种。简单起见,假设动物的行为有飞行,行走和游泳,于是我们抽象出了这三个接口。

飞鱼既可以飞行也可以游泳,我们只要实现这两个行为,飞鱼的问题就很好地解决了:

如果语言不支持多重继承,可以使用组合的方式实现。目前的设计好像能应付所有的动物,甚至能推广得更远。例如,实现飞机也很容易:

现在我们要实现一个功能,让我们实现的飞鱼先飞行再游泳。在OOP中,对象的具体语义是不可知的,例如我们访问飞鱼的fly接口是通过flyable接口对象,对于swim同样是从swimmable对象访问,而我们并不知道flyable和swimmable是否是从飞鱼而来的。OOP在这里会带来运行期的额外开销。呃,这里好像碰到了点什么问题。我们如何保证flyable和swimmable两个接口对象,都来自于同一个飞鱼对象?

在我们继续往下解决这个问题之前,让我们在回想一下,为什么要抽象。抽象是为了屏蔽每个任务具体的细节,提炼出一个统一的处理流程。也就是说,统一的处理流程是我们抽象的目标。而现在的这个先飞行再游泳的功能,无法在我们现有抽象的模型中表达。问题已经很清楚了,是我们的抽象出了问题。这就是OOP在设计的时候一定要避免的误区,我们的抽象是为了得到统一的处理流程,而不是为了适应更广泛复杂的真实世界的模型而过度抽象,遵守KISS很重要。

对于这个例子,如果我们的需求只是希望在程序中,统一控制实体先飞行再游泳,应该这样抽象

如果我们的需求是想统一控制实体的fly和swin行为,并能够灵活地组合这些行为,可以这样抽象:

OOP的抽象的程度应该到此为止,如果问题更复杂可以适当使用设计模式解决,当然设计模式不是鄙文探讨的范围。坚守KISS原则是一件很难的事情。Everything is object,这个极具诱惑和理想主义的咒语,一直在不停地将我们拉入深渊。是的,一直有一条路通往那里。

4. 深渊,从这里开始

下面的设计,不应该归于OOP的范畴了。因为它们为了试图用统一的方法解决一切问题,而且都违背了KISS最基本的教义。首先,如果我们想要继续坚持之前的设计,又希望问题能够得到解决,本着接口统一的原则,我们可以像COM一样,提供一个接口查询的机制:

还有一个解决方案,就是使用发送消息:

以上两个方法很类似,就放在一起讨论。两个方法都引入了一个共同的公共基类,object_base. 对!Everything is object就从这里开始了。第一种方法提供了一个统一的接口查询机制,而后者提供了一个统一的发送消息机制。两者都需要全局唯一的ID来区分不同的接口或者消息。接口查询的方式会带来接口对象不安全强转,而发送消息会招致不安全的数据转换。就目前而言所引入的公共基类和全局ID的负载,还有不安全的代码都不是很严重的问题。例如后者,可以在对象语义完整的时候,创建代理来绑定操作,从而抵消不安全的代码。最严重的问题是,OOP在这里已经荡然无存了。没有了接口的约束,之前抽象的模型也毫无意义了,这于过程式编程毫无差别。如此抽象的结果,却是写出了等同之前毫无抽象的代码,这种设计难道不是一个悖论吗?

深渊也是有底部的,我们继续把查询接口与查询消息推广,其实就是现代语言都提供的反射。反射是一种内省机制,可以让我们查询对象的成员方法和成员数据。当然,与其这样使用反射,何不去使用一门动态语言?

5. 小结

OOP使用的接口抽象的方法,本质上是围绕接口进行的归类方法。这种方法可以做到统一调度流程,屏蔽具体的细节,但同时也许多代价:
1. 围绕接口进行抽象,只利用了语法约束,但丢弃了语义;
2. 归类引入了很强的is_a的依赖关系,而程序中的归类结果通常与现实中经验相差甚远;
3. 对于静态类型,有很明确的编译期的语言,OOP把负担都扔给了运行期;

OOP有着适中的抽象能力,用来对现实问题的简化模型进行抽象,而everything is object是OOP的滥用。

下一篇,我们将讨论静态强类型语言所擅长的泛型编程。GP具有比OOP强大许多的抽象能力,它比OOP更适合于抽象复杂的模型。我们会以最大公约数算法为线索,讨论GP在C++编程语言中的实践方法。

解耦利器function message bus

需求1

能把模版函数和一个key注册起来以便后面使用,能把参数不同的函数和一个key注册起来以便后面使用。
c++中没有这样的一个容器可以存放模版函数和参数类型不同的函数。

需求2

对象A和对象B相互调用,耦合性很强,如何消除这种耦合性;
对象A和对象B没有任何关系,但A希望用B里面的方法,但二者又不适合直接关联起来。

如果你碰到这两个需求中的任何一个,那么你就需要function message bus。

作用

1.作为一个万能的函数注册器,可以注册任意类型的函数(除了重载函数);

2.解耦对象之间的调用关系

例子

上面的例子展示了如何注册不同类型的函数,包括普通(模版)函数,lambda和成员函数,也支持函数返回值。

因为函数的调用者不必知道被调用者,二者都依赖于function_msg_bus,所以你就可以通过它来解耦对象之间的调用关系。

具体代码

github

purecpp社区将举办今年的C++大会(讲师报名已经开始)

大会目标

Modern C++开源社区purecpp将举办今年的C++大会,目前正在筹备当中。举办C++大会的目的是为了促进行业交流,推广和促进modern c++为企业提高生产力和竞争力。

purecpp社区创始人祁宇作为大会的技术出品人(他也将在今年的cppcon上做演讲),负责招募国内外的优秀讲师。这将是purecpp社区组织举办的一场高水平、国际化的C++大会,大会的形式和内容将会与cppcon类似但又具备中国特色,如果你不能亲自前往美国参加cppcon那么就不要错过这次在中国举行的C++大会。

时间和地点

2018-11月或2018-12月,晚点确定最终时间

深证,广州,珠海三地之一,晚点确定最终地点

赞助商

这是开源社区组织的C++大会,不以盈利为目的,因此需要寻求赞助商。有意成为赞助商的请邮件purecpp@163.com

白金赞助商 16w(仅限一名)
1. 赞助商logo和链接放到官网
2. 开幕和闭幕上致谢
3. 提供两个展位
4. 可以在主会场打两个广告条幅
5. 大会宣传册上放赞助商logo

黄金赞助商 8w
1. 赞助商logo和链接放到官网
2. 开幕和闭幕上致谢
3. 提供一个展位
4. 大会宣传册上放赞助商logo

白银赞助商 4w
1. 赞助商logo和链接放到官网
2. 开幕和闭幕上致谢
3. 大会宣传册上放赞助商logo

青铜赞助商 2w
1. 赞助商logo和链接放到官网
2. 开幕和闭幕上致谢

大会主题

  1. C++11/14/17/20
  2. C++ libraries and frameworks of general interest
  3. ISO standardization proposals
  4. Parallelism/multi-processing
  5. Concepts and generic programming
  6. Functional programming
  7. High performance computing
  8. Software development tools, techniques, and processes for C++
  9. Practical experiences using C++ in real-world applications
  10. Industry-specific perspectives: mobile and embedded systems, game development, high performance trading, scientific programming, robotics, etc.

讲师报名

不管你是什么学历、什么公司、什么国家,只要你有和C++有关的创新的idea,你就可以报名!
讲师报名需要填一个报名表,类似于cppcon,你需要提供演讲者的信息、演讲的摘要信息和主要内容。如果你有很有说服力的证明材料就更好了,证明材料是能证明你演讲内容的代码或之前的ppt。

讲师报名阶段两个月左右,报名阶段结束之后就是评审阶段。采取的是专家评审方式,由两名中国专家和两名外国专家对演讲内容进行评审,评审通过的讲师就可以参会了。无论评审通过与否我们都会告知报名者结果。

这次大会对讲师的质量会要求很高,竞争也会比较激烈,因此填一份好的讲师报名表很重要。

希望中国的C++高手和天才们赶紧报名成为讲师,中国的C++大会将因你而不同!

中国C++大会讲师报名表

cppcon2018将于九月在贝尔维尤举办

cppcon2018

由C++标准委员会组织的顶级C++技术大会cppcon2018将于9月在bellevue举办,C++之父Bjarne Stroustrup和C++标准委员会主席Herb Sutter将在大会上做主旨演讲。来自全球数十个国家的C++专家将在大会上做精彩的演讲,国际各大知名公司也将参会。

今年的cppcon竞争非常激烈,报名的演讲主题有两百多个,最终只有一百多个主题被接受。相信今年的cppcon将是一场C++技术盛宴。随着C++17的确定和C++20的到来,今年势必会涌现出更多的新思想和新技术,非常令人期待。

purecpp社区创始人祁宇也将在这次大会上做演讲,这是他连续两年参加cppcon,在此也希望有更多的来自中国的C++爱好者能在C++国际舞台上发出自己的声音。

purecpp社区将持续关注cppcon2018技术大会的最新资讯并及时与大家分享。

欢迎关注purecpp社区的微信公众号。
qrcode_for_gh_300922997283_430

再谈自动注册的工厂

关于自动注册的工厂,我之前写过了两篇文章,之前的一篇文章在这里。这个自动注册的工厂通过宏实现自动注册,同时也可以支持变参。

下面是通过宏实现自动注册的例子。

之前提到的自动注册的工厂

问题

通过宏实现的自动注册工厂的主要问题就是需要借助宏,宏有两个问题,第一个问题是不能调试,第二个问题是让代码变得晦涩。这些自动注册的宏散落在各处,也增加了维护负担。如果能不借助宏实现自动注册就可以消除这些问题了。

改进目标

之前的自动注册工厂还需要改进,改进的目标就是消除宏。

实现

实现代码很简单,主要思路是借助crtp和静态变量初始化顺序来实现的,因为静态变量的实例化是早于main函数的,因此我们可以利用静态变量实例化的时候实现自动注册。

创建的对象需要派生于AutoMsgFactory,并实现一个静态的id函数,这个id就是创建该类型对象需要的一个唯一的id。需要注意的一个细节是:在id函数中需要使用一下基类AutoMsgFactory中的静态变量registered_,用来保证静态变量实例化。

assert(registered_);

上面这行代码在这里有两个作用,第一个作用是调用了registered_保证当前类会被自动注册,第二个作用是避免重复注册,出现重复注册时会触发断言错误。

另外通过编译期限定派生类必须定义一个Id函数可以保证派生类不会忘记定义创建该类需要的唯一id,这个id是自定义的,可以是枚举类型也可以是整形。

总结

这个实现消除了宏,在使用上也比较方便,不过也有个缺点是需要调用一下静态变量。综合来看,个人感觉比通过宏实现自动注册更好。

更新 支持变参

feather以及ormpp linux依赖库的安装

Debian分支的linux下
1.安装mysql开发库 sudo apt-get install libmysqlclient-dev
2.安装postgresql开发库 sudo apt-get install libpq-dev
3.安装sqlite3开发库 sudo apt-get install libsqlite3-dev
4.安装uuid库 sudo apt-get install uuid
5.安装zlib库 sudo apt-get install zlib1g-dev
6.安装openssl库 sudo apt-get install libssl-dev

Centos下依赖安装 (建议centos7.0+)
1.安装mysql开发库 sudo yum install mysql-devel
2.安装postgresql开发库 sudo yum install postgresql-devel.x86_64
3.安装sqlite3开发库 sudo yum install sqlite-devel.x86_64
4.安装uuid库 sudo yum install libuuid-devel.x86_64
5.安装zlib库 sudo yum install zlib-devel.x86_64
6.安装openssl库 sudo yum install openssl-devel.x86_64

asio库的依赖安装
如果不想安装使用完整boost 可以只安装asio模块 这是官方下载地址http://think-async.com/Asio/AsioStandalone
如果使用的是ubuntu18.0系统的 可以直接通过命令安装最新的boost sudo apt-get install boost
boost安装方法如下
https://www.boost.org/下载最新的boost库
tar xvf 解压后 进入相应目录执行
1 ./bootstrap.sh
2 ./b2 –without-python
等待编译完成后 执行 sudo ./b2 install
成功执行以上步骤就完成了boost库的安装

如果只需要使用cinatra框架 只需要安装zlib ssl uuid boost.asio即可正常通过编译

编译期遍历std::array

需求

在编译期遍历一个std::array,并且保证遍历的索引为编译期常量。

实现代码:

如何实现函数参数过滤代码点评

前面提出了一个参数过滤的需求,社区的一些朋友给出了实现,在这里做一下点评。

需求

过滤传入的函数参数。假设传入了int, bool, double, bool, structA这几个参数,现在我需要把其中的bool参数去掉,只保留非bool的参数,因此输入的参数经过过滤之后就变成一个tuple<int, double, structA>了。

实现思路:

实现方法有两种(当然,也许还有更多的方法)。

第一种思路

展开变参的过程中忽略特定类型,其它类型的参数重新组成一个tuple,这种方法比较直接了当。社区的Jaly就是这样做的,下面是他的实现代码:

这个代码的实现简洁又灵活,代码很少,并且能支持任意类型的过滤,非常精彩。

除了上面的实现之外,还有一种实现方法,利用c++17的fold expression去过滤,这也是我的实现方法。具体代码如下:

这个实现相比Jaly的实现,代码更多,还不够灵活,不能对任意类型进行过滤。虽然用到了C++17的fold expression,但是没有Jaly的实现那么简洁灵活,而且还用到了运算符重载,存在运算符重载冲突的可能性。算是展示fold expression的一种用法吧。

社区的yiShuiHanFeng实现了过滤bool参数并调用过滤bool后的目标函数,下面是他的实现:

yiShuiHanFeng是通过函数递归调用来过滤bool的,但是他在过滤bool的同时又把需要的参数保留,通过不断把非bool的参数往后放来实现的,最终实现目标函数调用,中间不会生成中间变量,实现得非常精妙,很赞!

第二种思路

在编译期得到过滤之后的参数索引。展开参数类型,排除bool类型,并把非bool类型的参数索引保存到index_sequence,后面根据这个index_sequence就可以得到过滤后的参数。下面是fesil的实现:

这个实现也很棒,也能对任意类型进行过滤,实现用到了c++17的if constexpr,让代码变得更简单了,虽然相比第一种实现方法稍显复杂一点,但仍然是一个不错的思路。

总结

这个需求大家积极参与并且给出了精彩的实现,非常好。可以看到同一个需求却有着不同的实现思路和实现方法,体现了元编程灵活与精巧。

本次实现得最好的是Jaly,他的代码最短又灵活,其次是fesil,yiShuiHanFeng的参数移动也很精妙。

cinatra模板引擎使用

渲染一个简单的html模板

通过render_view这个接口 第一个参数是模板文件的相对路径 这样我们就可以给客户端返回一个html页面了

在当前模板中包含其他的模板文件
我们有一个test.html 和一个header.html 内容分别如下

服务器代码如下

这样我们就可以在业务中复用公用的模板文件 通过不同的数据去渲染想展示的内容

需要通过数据去渲染的页面
我们有一个data.html的文件 内容如下

如上使用 就能轻松的通过需要的数据渲染出一个页面

通常我们展示前端页面的时候都会需要对一个list的数据进行渲染 同样cinatra的模板也支持我们开发相关的业务
我们有一个list.html的文件,内容如下

我们可以在cinatra里面写上这么一个接口

是不是很轻松的就能完成我们的业务了

cinatra模板引擎同样支持if判断
我们有一个study.html的文件,内容如下

我们可以通过传递display=0或者1来看内容是否显示

以上列举了一些cinatra中常用的一些方法 更多的功能 大家可以在cinatra群里面一起交流学习