现时C++能够说是支持OOP范式中最为经常使用及高性能的语言。虽然如此,在C++使用OOP的编程方式在一些场合未能提供最高性能。 [1]详细描述了这个观点,我在此尝试简单说明。注意:其余支持OOP的语言一般都会有本答案中说起的问题,C++只是一个合适的说明例子。
历史上,OOP大概是60年代出现,而C++诞生于70年代末。如今的硬件和当时的有很大差别,其中最大的问题是内存墙_百度百科。图1: 处理器和内存的性能提高比较,处理器的提高速度大幅高于内存[2]。
跟据Numbers Every Programmer Should Know By Year:图2:2014年计算机几种操做的潜伏期(latency)。
从这些数据,咱们能够看出,内存存取成为现代计算机性能的重要瓶颈。然而,这个问题在C++设计OOP编程范式的实现方式之初应该并未能考虑获得。现时的OOP编程有可能不缓存友好(cache friendly),致使有时候并不能发挥硬件最佳性能。如下描述一些箇中缘由。
1. 过分封装
使用OOP时,会把一些复杂的问题分拆抽象成较简单的独立对象,经过对象的互相调用去实现方案。可是,因为对象包含本身封装的数据,一个问题的数据集会被分散在不一样的内存区域。互相调用时极可能会出现数据的cache miss的状况。
2. 多态
在C++的通常的多态实现中,会使用到虚函数表。虚函数表是经过加入一次间接层来实现动态派送。但在调用的时候须要读取虚函数表,增长cache miss的可能性。基本上要支持动态派送,不管用虚函数表、函数指针都会造成这个问题,但若是类的数目极多,把函数指针若是和数据放在一块儿有时候可放缓问题。
3. 数据布局
虽然OOP自己并没有限制数据的布局方式,但基本上绝大部分OOP语言都是把成员变量连续包裹在一段内存中。甚至使用C去编程的时候,也一般会使用到OOP或Object-based的思考方式,把一些相关的数据放置于一个struct以内:html
struct Particle {
Vector3 position;
Vector4 velocity;
Vector4 color;
float age;
// ...
};
即便不使用多态,咱们几乎不加思索地会使用这种数据布局方式。咱们一般会觉得,因为各个成员变量都紧凑地放置在一块儿,这种数据布局一般对缓存友好。然而,实际上,咱们须要考虑数据的存取模式(access pattern)。
在OOP中,经过封装,一个类的各类功能会被实现为多个成员函数,而每一个成员函数实际上可能只会存取少许的成员变量。这可能形式很是严重的问题,例如:java
for (Particle* p = begin; p != end; ++p)
p->position += p->velocity * dt; // 或 p->SimulateMotion(dt);
在这种模式下,实阶上只存取了两个成员变量,但其余成员变量也会载入缓存形成浪费。固然,若是在迭代的时候能存取尽可能多的成员变量,这个问题可能并不存在,但其实是很困难的。
若是采用传统的OOP编程范式及实现方式,数据布局的问题几乎没有解决方案。因此在[1]里,做者提出,在某些状况下,应该放弃OOP方式,以数据的存取及布局为编程的考虑重中,称做面向数据编程(data-oriented programming, DOP)。
有关DOP的内容就不在此展开了,读者可参考[1],还有[3]做为实际应用例子。
[1] ALBRECHT, “Pitfalls of Object Oriented Programming”, GCAP Australia, 2009. http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf
[2] Hennessy, John L., and David A. Patterson. Computer architecture: a quantitative approach. Elsevier, 2012.
[3] COLLIN, “Culling the Battlefield”, GDC 2011. http://dice.se/wp-content/uploads/CullingTheBattlefield.pdflinux
**************************************************************************************************************************************************************c++
不忘初心,方得始终。
前面几个答案都说到了点子上:OOP最大的弊端,就是不少程序员已经忘记了OOP的初心,潜意识中把OOP教条主义化(如同对GOTO语句的禁忌通常),而不是着眼于OOP着力达到的、更本质的目标,如:
- 改善可读性
- 提高重用性
可是OOP最重要的目标,实际上是OCP,即「开闭原则」。这一点不少答案都没有提到。
程序员
遵循开闭原则设计出的模块具备两个主要特征:
(1)对于扩展是开放的(Open for extension)。这意味着模块的行为是能够扩展的。当应用的需求改变时,咱们能够对模块进行扩展,使其具备知足那些改变的新行为。也就是说,咱们能够改变模块的功能。
(2)对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,没必要改动模块的源代码或者二进制代码。模块的二进制可执行版本,不管是可连接的库、DLL或者.EXE文件,都无需改动。
这就是为何会有「用多态代替switch」的说法。在应该使用多态的地方使用switch,会致使:
1 - 违反「开放扩展」原则。假如别人的switch调用了你的代码,你的代码要扩展,就必须在别人的代码里,人工找出每个调用你代码的switch,而后把你新的case加进去。
2 - 违反「封闭修改」原则。这是说,被switch调用的逻辑可能会由于过于紧密的耦合,而没法在不碰switch的状况下进行修改。
可是OCP不是免费的。若是一个模块根本没有扩展的需求,没有多人协做的需求,花时间达成OCP又有什么意义呢?设计类关系的时候忘记了OOP的初心,可能就会写出不少没有帮助的类,白白浪费人力和运行效率。
因此,假如全部代码都是你一我的维护,没有什么扩展的需求,那么多用一些switch也何尝不可;假如你的代码是要被别人使用或者使用了别人的代码,OOP极可能就是你须要用到的工具。
除了OOP,Type class和Duck Typing都是能够帮助你达成OCP原则的工具。固然,若是你使用的语言是Java,这两种工具都不用想了。shell
******************************************************************************************************************************************************************编程
弊端是,没有人还记得面向对象本来要解决的问题是什么。
一、面向对象本来要解决什么(或者说有什么优良特性)
彷佛很简单,但实际又很不简单:面向对象三要素封装、继承、多态
(警告:事实上,从业界如此总结出这面向对象三要素的一刹那开始,就已经开始犯错了!)。
封装:封装的意义,在于明确标识出容许外部使用的全部成员函数和数据项,或者叫接口。
有了封装,就能够明确区分内外,使得类实现者能够修改封装内的东西而不影响外部调用者;而外部调用者也能够知道本身不能够碰哪里。这就提供一个良好的合做基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。
继承+多态:继承和多态必须一块儿说。一旦割裂,就说明理解上已经误入歧途了。
先说继承:继承同时具备两种含义:其一是继承基类的方法,并作出本身的扩展——号称解决了代码重用问题;其二是声明某个子类兼容于某基类(或者说,接口上彻底兼容于基类),外部调用者可无需关注其差异(内部机制会自动把请求派发[dispatch]到合适的逻辑)。
再说多态:基于对象所属类的不一样,外部对同一个方法的调用,实际执行的逻辑不一样。
很显然,多态其实是依附于继承的第二种含义的。让它与封装、继承这两个概念并列,是不符合逻辑的。不假思索的就把它们看成可并列概念使用的人,显然是从一开始就被误导了。
实践中,继承的第一种含义(实现继承)意义并不很大,甚至经常是有害的。由于它使得子类与基类出现强耦合。
继承的第二种含义很是重要。它又叫“接口继承”。
接口继承实质上是要求“作出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,可一视同仁的处理实现了特定接口的全部对象”——这在程序设计上,叫作归一化。
归一化使得外部使用者能够不加区分的处理全部接口兼容的对象集合——就好象linux的泛文件概念同样,全部东西均可以当文件处理,没必要关心它是内存、磁盘、网络仍是屏幕(固然,若是你须要,固然也能够区分出“字符设备”和“块设备”,而后作出针对性的设计:细致到什么程度,视需求而定)。
归一化的实例:
a、一切对象均可以序列化/toString
b、一切UI对象都是个window,均可以响应窗口事件。
——必须注意,是一切(符合xx条件的)对象皆能够作什么,而不是“一切皆对象”。后者毫无心义。
显然,归一化能够大大简化使用者的处理逻辑:这和带兵打仗是相似的,班长须要知道每一个战士的姓名/性格/特长,不然就不知道该派谁去对付对面山坡上的狙击手;而连长呢,只需知道本身手下哪一个班/排擅长什么就好了,而后安排他们各自去守一段战线;到了师长/军长那里,他更关注战场形势的转变及预期……没有这种层层简化、而是必须直接指挥到每一个人的话,累死军长都无法指挥哪怕只是一场形势明朗的冲突——光一个个打完电话就能把他累成哑吧。
软件设计一样。好比说,消息循环在派发消息时,只需知道全部UI对象都是个window,均可以响应窗口消息就足够了;它不必知道每一个UI对象到底是什么——该对象本身知道收到消息该怎么作。
合理划分功能层级、适时砍掉没必要要的繁杂信息,一层层向上提供简洁却又完备的信息/接口,高层模块才不会被累死——KISS是最难也是最优的软件设计方法,没有之一。
总结:面向对象的好处实际就这么两点。
一是经过封装明肯定义了何谓接口、何谓接口内部实现、何谓接口的外部调用者,使得你们各司其职,不得越界;
二是经过继承+多态这种内置机制,在语言的层面支持归一化的设计,并使得内行能够从代码自己看到这个设计——但,注意仅仅只是支持归一化的设计。不懂如何作出这种设计的外行仍然不可能从瞎胡闹的设计中获得任何好处。
显然,不用面向对象语言、不用class,同样能够作归一化的设计(如老掉牙的泛文件概念、游戏行业的一切皆精灵),同样能够封装(经过定义模块和接口),只是用面向对象语言能够直接用语言元素显式声明这些而已;
而用了面向对象语言,满篇都是class,并不等于就有了归一化的设计。甚至,由于被这些花哨的东西迷惑,反而更加不知道什么才是设计。
二、人们觉得面向对象是什么、以及所以制造出的悲剧以及闹剧
误解1、面向对象语言支持用语言元素直接声明封装性和接口兼容性,因此用面向对象语言写出来的东西必定更清晰、易懂。
事实上,既然class意味着声明了封装、继承意味着声明了接口兼容,那么错误的类设计显然就是错误的声明、盲目定义的类就是无心义的喋喋不休。而错误的声明比没有声明更糟;通篇毫无心义的喋喋不休还不如错误的声明。
除非你真正作出了漂亮的设计,而后用面向对象的语法把这个设计声明出来——仅仅声明真正有设计、真正须要人们注意的地方,而不是处处瞎叫唤——不然不可能获得任何好处。
一切皆对象实质上是在鼓励堆砌毫无心义的喋喋不休。大部分人——注意,不是个别人——甚至被这种无心义的喋喋不休搞出了神经质,以致于非要在喋喋不休中找出意义:没错,我说的就是设计模式驱动编程,以及如此理解面向对象编程。
误解2、面向对象三要素是封装、继承、多态,因此只要是面向对象语言写的程序,就必定“继承”了语言的这三个优良特性。
事实上,如前所述,封装、继承、多态只是语言层面对良好设计的支持,并不能导向良好的设计。
若是你的设计作不出真正的封装性、不懂得何谓归一化,那它用什么写出来都是垃圾。
误解3、把软件写成面向对象的至少是无害的。
要了解事实上是什么,须要先科普几个概念。
什么是真正的封装?
——回答我,封装是否是等于“把不想让别人看到、之后可能修改的东西用private隐藏起来”?
显然不是。
若是功能得不到知足、或者不曾预料到真正发生的需求变动,那么你怎么把一个成员变量/函数放到private里面的,未来就必须怎么把它挪出来。
你越瞎搞,越去搞某些华而不实的“灵活性”——好比某种设计模式——真正的需求来临时,你要动的地方就越多。
真正的封装是,通过深刻的思考,作出良好的抽象,给出“完整且最小”的接口,并使得内部细节能够对外透明(注意:对外透明的意思是,外部调用者能够顺利的获得本身想要的任何功能,彻底意识不到内部细节的存在;而不是外部调用者为了完成某个功能、却被碍手碍脚的private声明弄得火冒三丈;最终只能经过怪异、复杂甚至奇葩的机制,才能更改他必须关注的细节——并且这种访问每每被实现的如此复杂,以致于稍不注意就会酿成大祸)。
一个设计,只有达到了这个高度,才能真正作到所谓的“封装性”,才能真正杜绝对内部细节的访问。
不然,生硬放进private里面的东西,最后还得生硬的被拖出来——固然,这种东西常常会被美化成“访问函数”之类渣渣(不是说访问函数是渣渣,而是说由于设计不良、不得不以访问函数之类玩意儿在封装上处处挖洞洞这种行为是渣渣)。
一个典型的例子,就是C++的new和过于灵活的内存使用方式之间的耦合。
这个耦合就致使了new[]/delete[]、placement new/placement delete之类怪异的东西:这些东西必须成对使用,怎么分配就必须怎么释放,任何错误搭配均可能致使程序崩溃——这是为了兼容C、以及获得更高执行效率的无奈之举;但,它更是“抽象层次过于复杂,以致于没法作出真正透明的设计”的典型案例:只能说,c++设计者是真正的大师,如此复杂的东西在他手里,才仅仅付出了如此之小的代价。
(更准确点说,是new/delete和c++的其它语言元素之间是非正交的;因而当同时使用这些语言元素时,就不可避免的出现了彼此扯淡的现象。即new/delete这个操做对其它语言元素非透明:在c++的设计里,是经过把new/delete分红两层,一是内存分配、二是在分配的内存上初始化,而后暴露这个分层细节,从而在最大程度上实现了封装——但比之其它真正能彼此透明的语言元素间的关系,new/delete显然过于复杂了)
这个案例,能够很是直观的说明“设计出真正对外透明的封装”究竟会有多难。
接口继承真正的好处是什么?是用了继承就显得比较高大上吗?
显然不是。
接口继承没有任何好处。它只是声明某些对象在某些场景下,能够用归一化的方式处理而已。
换句话说,若是不存在“须要不加区分的处理相似的一系列对象”的场合,那么继承不过是在装X罢了。
封装可应付需求变动、归一化可简化(类的使用者的)设计:以上,就是面向对象最最基本的好处。
——其它一切,都不过是在这两个基础上的衍生而已。
换言之,若是得不到这两个基本好处,那么也就没有任何衍生好处——应付需求变动/简化设计并非打打嘴炮就能作到的。
了解了如上两点,那么,很显然:
一、若是你没有作出好的抽象、甚至彻底不知道须要作好的抽象就忙着去“封装”,那么你只是在“封”和“装”而已。
这种“封”和“装”的行为只会制造累赘和虚假的承诺;这些累赘以及必然会变卦的承诺,必然会为将来的维护带来更多的麻烦,甚至拖垮整个项目。
正是这种累赘和虚假的承诺的拖累,而不是所谓的为了应付“需求改变”所必需的“灵活性”,才是大多数面向对象项目代码量暴增的元凶。
二、没有真正的抓到一类事物(在当前应用场景下)的根本,就去设计继承结构,是必不会有所得的。
不只如此,请注意我强调了在当前应用场景下。
这是由于,分类是一个极其主观的东西,不存在普适的分类法。
举例来讲,我要研究种族歧视,那么必然以肤色分类;换到法医学,那就按死因分类;生物学呢,则搞门科目属种……
想象下,需求是“时尚女装”,你却按“窒息死亡/溺水死亡/中毒死亡之体征”来了个分类……你说后面这软件还能写吗?
相似的,我遇到过写游戏的却去纠结“武器装备该不应从游戏角色继承”的神人。你以为呢?
事实上,游戏界真正的抽象方法之一是:一切都是个有位置能感觉时间流逝的精灵;而某个“感觉到时间流逝显示不一样图片的对象”,其实就是游戏主角;而“当收到碰撞事件时,改变主角下一轮显示的图片组的”,就是游戏逻辑。
看看它和“武器装备该不应从游戏角色继承”能差多远。想一想到得后来,以游戏角色为基类的方案会变成什么样子?为何会这样?
最具重量级的炸弹则是:正方形是否是一个矩形?它该不应从矩形继承?若是能够从矩形继承,那么什么是正方形的长和宽?在这个设计里,若是我修改了正方形的长,那么这个正方形类还能不能叫正方形?它不该该天然转换成长方形吗?什么语言能提供这种机制?
形成这颗炸弹的根本缘由是,面向对象中的“类”,和咱们平常语言乃至数学语言中的“类”根本就不是一码事。
面向对象中的“类”,意思是“接口上兼容的一系列对象”,关注的只不过是接口的兼容性而已(可搜索 里氏代换);关键放在“可一视同仁的处理”上(学术上叫is-a)。
显然,这个定义彻底是且只是为了应付归一化的须要。
这个定义常常和咱们平常对话中提到的类概念上重合;但,如前所述,根本上却不折不扣是八杆子打不着的两码事。
就着生活经验滥用“类”这个术语,甚至依靠这种粗浅认识去作设计,必然会致使出现各类各样的误差。这种设计实质上就是在胡说八道。
就着这种胡说八道来写程序——有人以为这种人能有好结果吗?
——但,几乎全部的面向对象语言、差很少全部的面向对象方法论,却就是在鼓励你们都这么作,彻底没有意识到它们的理论基础有多么的不牢靠。
——如此做死,焉能不死?!
——你还敢说面向对象无害吗?
——在真正明白何谓封装、何谓归一化以前,每一次写下class,就在错误的道路上又多走了一步。
——设计真正须要关注的核心其实很简单,就是封装和归一化。一个项目开始的时候,“class”写的越早,就离这个核心越远。
——过去鼓吹的各类面向对象方法论、甚至某些语言自己,偏偏正是在怂恿甚至逼迫开发者尽量早、尽量多的写class。
误解4、只有面向对象语言写的程序才是面向对象的。
事实上,unix系统提出泛文件概念时,面向对象语言根本就不存在;游戏界的精灵这个基础抽象,最初是用C甚至汇编写的;……。
面向对象实际上是汲取以上各类成功设计的经验才提出来的。
因此,面向对象的设计,没必要非要c++/java之类支持面向对象的语言才能实现;它们不过是在你作出了面向对象的设计以后,能让你写得更惬意一些罢了——但,若是一个项目无需或没法作出面向对象的设计,某些面向对象语言反而会让你很难受。
用面向对象语言写程序,和一个程序的设计是面向对象的,二者是八杆子打不着的两码事。纯C写的linux kernel事实上比c++/java之类语言搞出来的大多数项目更加面向对象——只是绝大部分人都自觉得本身处处瞎写class的面条代码才是面向对象的正统、而死脑筋的linus搞的泛文件抽象不过是过程式思惟搞出来的老古董。
——这个误解之深,甚至达到连wiki词条里面,都把OOP定义为“用支持面向对象的语言写程序”的程度。
——恐怕这也是没有人说泛文件设计思想是个骗局、而面向对象却被业界大牛们严厉抨击的根本缘由了:真正的封装、归一化精髓被抛弃,浮于表面的、喋喋不休的class/设计模式却成了”正统“!
借用楼下PeytonCai朋友的连接:
名家吐槽:面向对象编程从骨子里就有问题
————————————————————————————
总结: 面向对象实际上是对过去成功的设计经验的总结。但那些成功的设计,不是由于用了封装/归一化而成功,而是切合本身面对的问题,给出了恰到好处的设计。
让一个初学者知道本身应该向封装/归一化这个方向前进,是好的;用一个面向对象的条条框框把他们框在里面、甚至使得他们觉得写下class是彻底无需思索的、真正应该追求的是设计模式,则是罪恶的。
事实上,class写的越随意,才越须要设计模式;就着错误的实现写得越多、特性用得越多,它就愈加的死板,以致于必须更加多得多的特性、模式、甚至语法hack,才能勉强完成需求。
只有通过真正的深思熟虑,才有可能作到KISS。
处处鼓噪的面向对象编程的最大弊端,是把软件设计工做偷换概念,变成了“就着class及相关教条瞎胡闹,无论有没有好处先插一杠子”,甚至使得人们忘记去关注“抽象是否真正简化了面对的问题”。
一言以蔽之:没有银弹。任何寄但愿于靠着某种“高大上”的技术——不管是面向对象、数据驱动、消息驱动仍是lambda、协程等等等等——就能一劳永逸的使得任何现实问题“迎刃而解”的企图都是注定要失败的,都不过是外行的意淫而已;靠意淫来作设计,不掉沟里才怪。
想要作出KISS的方案,就必须对面对的问题有透彻的了解,有足够的经验和能力,并通过深思熟虑,这才能作出简洁的抽象:至于最终的抽象是面向对象的、面向过程的仍是数据驱动/消息驱动的,甚至是大杂烩的,都是可能的。只要这个设计能作到最重要、也是最难的KISS,那它就是个好设计。
的确有成功的经验、正确/合理的方向:技术无罪,但,没有银弹。设计模式
references:缓存
http://www.zhihu.com/question/20275578网络