百科问答小站 logo
百科问答小站 font logo



面向对象编程的弊端是什么? 第1页

  

user avatar   miloyip 网友的相关建议: 
      

我想从一个游戏程序员的角度探讨OOP的一个问题──性能。

现时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之内:

       struct Particle {     Vector3 position;     Vector4 velocity;     Vector4 color;     float age;     // ... };     

即使不使用多态,我们几乎不加思索地会使用这种数据布局方式。我们通常会以为,由于各个成员变量都紧凑地放置在一起,这种数据布局通常对缓存友好。然而,实际上,我们需要考虑数据的存取模式(access pattern)。

在OOP中,通过封装,一个类的各种功能会被实现为多个成员函数,而每个成员函数实际上可能只会存取少量的成员变量。这可能形式非常严重的问题,例如:

       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.

research.scee.net/files

[2] Hennessy, John L., and David A. Patterson. Computer architecture: a quantitative approach. Elsevier, 2012.

[3] COLLIN, “Culling the Battlefield”, GDC 2011.

dice.se/wp-content/uplo

user avatar   minmin.gong 网友的相关建议: 
      

从我个人来说,经常遇到的一个问题是oo和并行的冲突。oo要封装,并行要开放。oo要array of struct/class,并行要struct of array。以至于很多时候为了上CUDA等大规模并行,得把原有数据结构做大规模修改。

不过如果习惯了并行优先,那会把程序设计成数据和操作分离的模式。不那么oo但结构仍然清晰。


user avatar   s.invalid 网友的相关建议: 
      

弊端是,没有人还记得面向对象原本要解决的问题是什么。

1、面向对象原本要解决什么(或者说有什么优良特性)

似乎很简单,但实际又很不简单:面向对象三要素封装、继承、多态

警告:事实上,从业界如此总结出这面向对象三要素的一刹那开始,就已经开始犯错了!)。

封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,或者叫接口

有了封装,就可以明确区分内外,使得类实现者可以修改封装的东西而不影响部调用者;而外部调用者也可以知道自己不可以碰哪里。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。



继承+多态:继承和多态必须一起说。一旦割裂,就说明理解上已经误入歧途了。

先说继承:继承同时具有两种含义:其一是继承基类的方法,并做出自己的改变和/或扩展——号称解决了代码重用问题;其二是声明某个子类兼容于某基类(或者说,接口上完全兼容于基类),外部调用者可无需关注其差别(内部机制会自动把请求派发[dispatch]到合适的逻辑)。

再说多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。

很显然,多态实际上是依附于继承的两种含义的:“改变”和“扩展”本身就意味着必须有机制去自动选用你改变/扩展过的版本,故无多态,则两种含义就不可能实现。

所以,多态实质上是继承的实现细节;那么让多态与封装、继承这两个概念并列,显然是不符合逻辑的。不假思索的就把它们当作可并列概念使用的人,显然是从一开始就被误导了——正是这种误导,使得大多数人把注意力过多集中在多态这个战术层面的问题上,甚至达到近乎恶意利用的程度;同时却忽略了战略层面的问题,这就致使软件很容易被他们设计成一滩稀屎(后面会详细谈论这个)。


实践中,继承的第一种含义(实现继承)意义并不很大,甚至常常是有害的。因为它使得子类与基类出现强耦合。

继承的第二种含义非常重要。它又叫“接口继承”。

接口继承实质上是要求“做出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,可一视同仁的处理实现了特定接口的所有对象”——这在程序设计上,叫做归一化


归一化使得高层的外部使用者可以不加区分的处理所有接口兼容的对象集合——就好象linux的泛文件概念一样,所有东西都可以当文件处理,不必关心它是内存、磁盘、网络还是屏幕(当然,对底层设计者,当然也可以区分出“字符设备”和“块设备”,然后做出针对性的设计:细致到什么程度,视需求而定)。

归一化的实例:

a、一切对象都可以序列化/toString

b、一切UI对象都是个window,都可以响应窗口事件。

——必须注意,是一切(符合xx条件的)对象皆可以做什么,而不是“一切皆对象”。后者毫无意义(从信息论角度上说,一切皆xx蕴含的信息量为0)。


显然,归一化可以大大简化使用者的处理逻辑:

这和带兵打仗是类似的,班长需要知道每个战士的姓名/性格/特长,否则就不知道该派谁去对付对面山坡上的狙击手;而连长呢,只需知道自己手下哪个班/排擅长什么就行了,然后安排他们各自去守一段战线;到了师长/军长那里,他更关注战场形势的转变及预期……没有这种层层简化、而是必须直接指挥到每个人的话,累死军长都没法指挥哪怕只是一场形势明朗的冲突——光一个个打完电话就能把他累成哑巴。

反过来也对:军长压根就不应该去干涉某个步兵班里、几个大头兵之间的战术配合;这不仅耽误他行使身为军长的职责,也会干扰士兵们长久以来养成的默契。他的职责是让合适的部队在合适的时机出现在合适的战场,而不是一天到晚对着几个小兵指手画脚、弄的他们无所适从。

约束各单位履行各自的职责、禁止它们越级胡乱指挥,这就是封装

正是通过封装和归一化,我们才可以做到“如果一个师解决不了问题,那就再调两个师”“如果单凭陆军解决不了问题,那就让空军也过来”——这种灵活性显然是从良好的部队编制得来的。在软件设计里,我们叫它“通过合理模块化而灵活应对需求变更”。



软件设计同样。比如说,消息循环在派发消息时,只需知道所有UI对象都是个window,都可以响应窗口消息就足够了;它没必要知道每个UI对象究竟是什么(归一化)、也不应该关心这个UI对象的内部执行细节(封装)——该对象自己知道收到消息该怎么做;而且若它出了问题,只需修改该对象即可,不会影响外部。

合理划分功能层级、适时砍掉不必要的繁杂信息,一层层向上提供简洁却又完备的信息/接口,高层模块才不会被累死——KISS是最难也是最优的软件设计方法,没有之一。


可见,封装和归一化才是战略层面、生死攸关的问题。遵循它并不能保证你一定能打胜仗,但违反它你必定死的很难看。

但这两个问题太大、太难,并且不存在普适性答案。这就使得没有足够经验、缺乏认真思考的外行们根本无从置喙



前面提到过,人们错误的把多态这个战术技巧提到“封装和归一化”相同的战略层面上。这就致使本该谈论战略的设计工作被一群毫无实践经验、只会就着浅显的多态胡扯八道的战术家攻占和把持,进而使得“以战术代替战略”成为普遍现象——因为对他们来说,多态是既容易理解又容易玩出诸多花样的;而封装和归一化就太空泛又太复杂,对他们来说完全无从着手了。

所以,他们把一切精力都用在多态的滥用上,却从不谈封装和归一化:即使谈到了,也是作为多态的附庸出现的。

这种战术层面的空谈很容易、也很容易出彩,但并不解决问题——反而总是导致简单问题复杂化。

然而,对于如何解决问题,他们并不在行,也不在乎。因为他们没有能力在乎。

这就要命了。



总结:面向对象的好处实际就这么两点。

一是通过封装明确定义了何谓接口、何谓接口内部实现、何谓接口的外部调用者,使得大家各司其职,不得越界;

二是通过继承+多态这种内置机制,在语言的层面支持归一化的设计,并使得内行可以从代码本身看到这个设计——但,注意仅仅只是支持归一化的设计。不懂如何做出这种设计的外行仍然不可能从瞎胡闹的设计中得到任何好处。


显然,不用面向对象语言、不用class,一样可以做归一化的设计(如老掉牙的泛文件概念、游戏行业的一切皆精灵),一样可以封装(通过定义模块和接口),只是用面向对象语言可以直接用语言元素显式声明这些而已;

而用了面向对象语言,满篇都是class,并不等于就有了归一化的设计。甚至,因为被这些花哨的东西迷惑,反而更加不知道什么才是设计。


2、人们以为面向对象是什么、以及因此制造出的悲剧以及闹剧

误解一、面向对象语言支持用语言元素直接声明封装性和接口兼容性,所以用面向对象语言写出来的东西一定更清晰、易懂

事实上,既然class意味着声明了封装、继承意味着声明了接口兼容,那么错误的类设计显然就是错误的声明、盲目定义的类就是无意义的喋喋不休。而错误的声明比没有声明更糟;通篇毫无意义的喋喋不休还不如错误的声明

除非你真正做出了漂亮的设计,然后用面向对象的语法把这个设计声明出来——仅仅声明真正有设计、真正需要人们注意的地方,而不是到处瞎叫唤——否则不可能得到任何好处。

一切皆对象实质上是在鼓励堆砌毫无意义的喋喋不休,并且用这种战术层面都蠢的要命的喋喋不休来代替战略层面的考量。

大部分人——注意,不是个别人——甚至被这种无意义的喋喋不休搞出了神经质,以至于非要在喋喋不休中找出意义:没错,我说的就是设计模式驱动编程,以及

如此理解面向对象编程



误解二、面向对象三要素是封装、继承、多态,所以只要是面向对象语言写的程序,就一定“继承”了语言的这三个优良特性

事实上,如前所述,封装、继承、多态只是语言层面对良好设计的支持,并不能导向良好的设计。

如果你的设计做不出真正的封装性、不懂得何谓归一化,那它用什么写出来都是垃圾(不仅如此,因为你的低水平,“面向对象三要素”反而会误导你,使你更快、更远、更诡异的偏离目标)。



误解三、把软件写成面向对象的至少是无害的

要了解事实上是什么,需要先科普几个概念。


1、什么是真正的封装

——回答我,封装是不是等于“把不想让别人看到、以后可能修改的东西用private隐藏起来”?

显然不是

如果功能得不到满足、或者未曾预料到真正发生的需求变更,那么你怎么把一个成员变量/函数放到private里面的,将来就必须怎么把它挪出来。

你越瞎搞,越去搞某些华而不实的“灵活性”——比如某种设计模式——真正的需求来临时,你要动的地方就越多。

真正的封装是,经过深入的思考,做出良好的抽象,给出“完整且最小”的接口,并使得内部细节可以对外透明(注意:对外透明的意思是外部调用者可以顺利的得到自己想要的任何功能,完全意识不到内部细节的存在;而不是外部调用者为了完成某个功能、却被碍手碍脚的private声明弄得火冒三丈;最终只能通过怪异、复杂甚至奇葩的机制,才能更改他必须关注的细节——而且这种访问往往被实现的如此复杂,以至于稍不注意就会酿成大祸)。

一个设计,只有达到了这个高度,才能真正做到所谓的“封装性”,才能真正杜绝对内部细节的访问。

否则,生硬放进private里面的东西,最后还得生硬的被拖出来——当然,这种东西经常会被美化成“访问函数”之类渣渣(不是说访问函数是渣渣,而是说因为设计不良、不得不以访问函数之类玩意儿在封装上到处挖洞洞这种行为是渣渣)。



一个典型的例子,就是C++的new和过于灵活的内存使用方式之间的耦合。

这个耦合就导致了new[]/delete[]、placement new/placement delete之类怪异的东西:这些东西必须成对使用,怎么分配就必须怎么释放,任何错误搭配都可能导致程序崩溃——这是为了兼容C、以及得到更高执行效率的无奈之举;但,它更是“抽象层次过于复杂,以至于无法做出真正透明的设计”的典型案例:只能说,c++设计者是真正的大师,如此复杂的东西在他手里,才仅仅付出了如此之小的代价。

(更准确点说,是new/delete和c++的其它语言元素之间是非正交的;于是当同时使用这些语言元素时,就不可避免的出现了彼此扯淡的现象。即new/delete这个操作对其它语言元素非透明:在c++的设计里,是通过把new/delete分成两层,一是内存分配、二是在分配的内存上初始化,然后暴露这个分层细节,从而在最大程度上实现了封装——但比之其它真正能彼此透明的语言元素间的关系,new/delete显然过于复杂了)

这个案例,可以非常直观的说明“设计出真正对外透明的封装”究竟会有多难。


2、接口继承真正的好处是什么?是用了继承就显得比较高大上吗?

显然不是。

接口继承没有任何好处。它只是声明某些对象在某些场景下,可以用归一化的方式处理而已。

换句话说,如果不存在“需要不加区分的处理类似的一系列对象”的场合,那么继承不过是在装X罢了。




了解了如上两点,那么,很显然:

1、如果你没有做出好的抽象、甚至完全不知道需要做好的抽象就忙着去“封装”,那么你只是在“封”和“装”而已。

这种“封”和“装”的行为只会制造累赘和虚假的承诺;这些累赘以及必然会变卦的承诺,必然会为未来的维护带来更多的麻烦,甚至拖垮整个项目。

正是这种累赘和虚假的承诺的拖累,而不是为了应付“需求改变”所必需的“灵活性”,才是大多数面向对象项目代码量暴增的元凶。

2、没有真正的抓到一类事物(在当前应用场景下)的根本,就去设计继承结构,是必不会有所得的。

不仅如此,请注意我强调了在当前应用场景下

这是因为,分类是一个极其主观的东西,不存在普适的分类法

举例来说,我要研究种族歧视,那么必然以肤色分类;换到法医学,那就按死因分类;生物学呢,则搞门科目属种……

想象下,需求是“时尚女装”,你却按“窒息死亡/溺水死亡/中毒死亡之体征”来了个分类……你说后面这软件还能写吗?



类似的,我遇到过写游戏的却去纠结“武器装备该不该从游戏角色继承”的神人。你觉得呢?

事实上,游戏界真正的抽象方法之一是:一切都是个有位置能感受时间流逝的精灵;而某个“感受到时间流逝显示不同图片的对象”,其实就是游戏主角;而“当收到碰撞事件时,改变主角下一轮显示的图片组的”,就是游戏逻辑。


看看它和“武器装备该不该从游戏角色继承”能差多远。想想到得后来,以游戏角色为基类的方案会变成什么样子?为什么会这样?





最具重量级的炸弹则是:正方形是不是一个矩形?它该不该从矩形继承?如果可以从矩形继承,那么什么是正方形的长和宽?在这个设计里,如果我修改了正方形的长,那么这个正方形类还能不能叫正方形?它不应该自然转换成长方形吗?如果我有两个List,一个存长方形,一个存正方形,自动转换后的对象能否自动迁移到合适的list?什么语言能提供这种机制?如果不能,“一视同仁的处理某个容器中的所有元素”岂不变成了一句屁话?

造成这颗炸弹的根本原因是,面向对象中的“类”,和我们日常语言乃至数学语言中的“类”根本就不是一码事。

面向对象中的“类”,意思是“接口上兼容的一系列对象”,关注的只不过是接口的兼容性而已(可搜索 里氏代换);关键放在“可一视同仁的处理”上(学术上叫is-a)。

显然,这个定义完全是且只是为了应付归一化的需要。

这个定义经常和我们日常对话中提到的类概念上重合;但,如前所述,根本上却彻彻底底是八杆子打不着的两码事。

就着生活经验滥用“类”这个术语,甚至依靠这种粗浅认识去做设计,必然会导致出现各种各样的偏差。这种设计实质上就是在胡说八道。

就着这种胡说八道来写程序——有人觉得这种人能有好结果吗?

——但,几乎所有的面向对象语言、差不多所有的面向对象方法论,却就是在鼓励大家都这么做,完全没有意识到它们的理论基础有多么的不牢靠。

——如此作死,焉能不死?!


——你还敢说面向对象无害吗?

——在真正明白何谓封装、何谓归一化之前,每一次写下class,就在错误的道路上又多走了一步。

——设计真正需要关注的核心其实很简单,就是封装和归一化。一个项目开始的时候,“class”写的越早,就离这个核心越远

——过去鼓吹的各种面向对象方法论、甚至某些语言本身,恰恰正是在怂恿甚至逼迫开发者尽可能早、尽可能多的写class。


重复一遍:封装可(通过固定接口而)应付需求变更、归一化可简化(类的使用者的)设计:以上,就是面向对象最最基本的好处。

——其它一切,都不过是在这两个基础上的衍生而已。

换言之,如果得不到这两个基本好处,那么也就没有任何衍生好处——应付需求变更/简化设计并不是打打嘴炮就能做到的。


误解四、只有面向对象语言写的程序才是面向对象的。

事实上,unix系统提出泛文件概念时,面向对象语言根本就不存在;游戏界的精灵这个基础抽象,最初是用C甚至汇编写的;……。

面向对象其实是汲取以上各种成功设计的经验才提出来的。

所以,面向对象的设计,不必非要c++/java之类支持面向对象的语言才能实现;它们不过是在你做出了面向对象的设计之后,能让你写得更惬意一些罢了——但,如果一个项目无需或无法做出面向对象的设计,某些面向对象语言反而会让你很难受。

用面向对象语言写程序,和一个程序的设计是面向对象的,两者是八杆子打不着的两码事。纯C写的linux kernel事实上比c++/java之类语言搞出来的大多数项目更加面向对象——只是绝大部分人都自以为自己到处瞎写class的面条代码才是面向对象的正统、而死脑筋的linus搞的泛文件抽象不过是过程式思维搞出来的老古董。

——这个误解之深,甚至达到连wiki词条里面,都把OOP定义为“用支持面向对象的语言写程序”的程度。

——我们提及面向对象时,明明在谈论战略、谈论软件总体设计;但总有人把它歪曲成战术方面的、漫无目标却还自我感觉良好的、琐碎的投机。

——恐怕这也是没有人说泛文件设计思想是个骗局、而面向对象却被业界大牛们严厉抨击的根本原因了:真正的封装、归一化精髓被抛弃,浮于表面的、喋喋不休的class/设计模式却成了”正统“!

借用楼下PeytonCai朋友的链接:

名家吐槽:面向对象编程从骨子里就有问题

————————————————————————————

总结: 面向对象其实是对过去成功的设计经验的总结。但那些成功的设计,不是因为用了封装/归一化而成功,而是切合自己面对的问题,给出了恰到好处的设计

让一个初学者知道自己应该向封装/归一化这个方向前进,是好的;用一个面向对象的条条框框把他们框在里面、甚至使得他们以为写下class是完全无需思索的、真正应该追求的是设计模式,则是罪恶的——它实质上是把初学者的注意力从真正应该注意的封装、归一化方向引开,欺骗他们陷入“近乎恶意的全方位滥用多态”的泥潭。

事实上,class写的越随意,才越需要设计模式;就着错误的实现写得越多、特性用得越多,它就越发的死板,以至于必须更加多得多的特性、模式、甚至语法hack,才能勉强完成需求。

只有经过真正的深思熟虑,才有可能做到KISS。


到处鼓噪的面向对象编程的最大弊端,是把软件设计工作偷换概念,变成了“就着class及相关教条瞎胡闹,不管有没有好处先插一杠子”,甚至使得人们忘记去关注“抽象是否真正简化了面对的问题”——这是猥琐的投机,不是设计。

一言以蔽之:没有银弹。任何寄希望于靠着某种“高大上”的技术——无论是面向对象、数据驱动、消息驱动还是lambda、协程等等等等——就能一劳永逸的使得任何现实问题“迎刃而解”的企图都是注定要失败的,都不过是外行的意淫而已;靠意淫来做设计,不掉沟里才怪。

想要做出KISS的方案,就必须对面对的问题有透彻的了解,有足够的经验和能力,并经过深思熟虑,这才能做出简洁的抽象:至于最终的抽象是面向对象的、面向过程的还是数据驱动/消息驱动的,甚至是大杂烩的,那都无所谓。只要这个设计能做到最重要、也是最难的KISS,它就是个好设计。

在特定领域、特定场景下,的确有成功的经验、正确/合理的方向:技术无罪,但,没有银弹。


————————————————————————————————————————

2016.5.16:

嗯,这个是我很久很久以前在CU上发过的一系列帖子……

当时很多鼓吹“面向对象就是好来就是好的”就着一知半解胡搅蛮缠,这系列帖子是驳斥他们的。所以很多词句挖苦意味很浓,见谅。

再比如,传说中的面向对象本该大显神威的游戏领域——就说流行的WOW吧。

这个游戏有10个职业,10个种族,每个种族都有自己的几个特有种族天赋(这个种族天赋还可能根据职业有所不同,比如血精灵);每个职业有几十甚至上百种不同的技能/法术,这些技能有近战技能,有远程技能;有的技能会对敌方造成伤害或不良状态,有的技能能给己方队友加上好的状态或治疗队友;而且很多这类技能还会根据目标的状态切换不同的效果;有些技能是单体效果,有些技能是光环效果(又分为对敌方造成光环效果还是对己方两种,也可能两者兼备),而另一些技能是地图范围效果(如烈焰风暴是一个圆形区域;冰锥术是一个锥形区域;特别的,顺劈斩是在当前攻击目标旁边不超过5码的另一个敌对目标——某个boss的顺劈斩更强,它会从第一个目标传递几十个目标,总传递距离可以达到夸张的几百码;并且这个伤害也是各有特色的:战士的顺劈斩是每个目标伤害固定,有些boss的则是同时挨打的人越多伤害越低,但还有个变态boss却是被打的人越多伤害越高……);大多数技能还可以通过天赋雕文强化/改变的面目全非(比如插一个雕文,法师的火球就不会造成持续伤害但施法速度增加;点一个天赋,法师的冰冷减速效果就会降低对方受到的治疗效果;点某个天赋,盗贼的某些技能攻击就会延长自身提升攻击速度这个状态的持续时间,等等);还有很多技能是因为学习了某个专业或装备/持有某个物品而得到(比如,学了采药,就可以得到生命之血这个技能,每3分钟可用,能够在若干秒内回复你若干生命值——这个技能和采药技能等级挂钩,但很可能接下来的某个版本,就会再和玩家的生命上限值挂钩,以避免它像现在一样,被玩家斥为废柴技能);另外,不同等级的技能可能有施法时间甚至额外特效方面的差别;此外,每个技能会造成不同属性的伤害/效果(神圣、暗影、元素、物理等等),甚至一个技能同时造成多种类型伤害效果,更有冰火球这样根据目标抵抗力而智能选择更大杀伤效果类型的变态魔法……

最后,最最重要的是,这所有职业上千个技能(或许加上NPC特有的一些技能,数目会达到几千种)并不稳定,常常会因为某个技能或某些技能的组合过于强大/弱小而加以修改(比如加一个额外的负面状态如无敌/圣疗;甚至全面修改“抗性”“破甲”概念的定义)——玩过wow的都知道,这事几乎每个月都有发生。

好吧,你打算怎么设计这数千个技能/效果?
或者,你就这样把这些概念用class这个筐一装,然后到处开特例、特例都解决不了就搞23个模式使劲往一块粘,管他整体结构如何,淌哪算哪?

扯淡。


有个故事说的好:
有人送几个瞎子一条鱼,瞎子们高兴坏了,决定熬鱼汤喝。鱼汤熬好了,瞎子甲尝了一口,真鲜啊;瞎子乙赶紧也喝一口,太鲜了,太好喝了。几个瞎子一边喝一边赞美——忽然瞎子丙叫了起来:鱼跳我脚上了,它不在锅里!
众瞎子大惊:这鱼都没放到锅里,汤就鲜成这样了;要是放进锅里,还不得把我们都鲜死啊!

众面向对象原教旨主义者把事情搅得一团糟,同样也会大惊:天哪,用了面向对象都复杂成这样,这要不用面向对象,这软件就不能写了吧!


想想看,假如让那些面向对象原教旨主义者来设计,会出现什么情况:

定义一个基类叫技能;然后一个继承类叫法术技能,另一个叫物理技能;然后神圣法术从法术技能继承,疾病法术也从法术技能继承;由于圣骑士一个技能同时具备物理和法术两种效果,于是必须多重继承神圣法术和物理技能;多重继承太危险,于是不得不把神圣法术搞成接口类,引入接口继承甚至带实现的纯虚函数等等高端概念;然后,活该枪毙的暴雪设计师又想出了让某个技能同时对目标加上神圣持续伤害效果的奇怪点子——于是不得不再加个继承层次,使得神圣法术是神圣持续伤害法术的子集:仅立刻造成一次持续伤害的DOT(damage of time)技能……

那么,点一个天赋,一个技能就会有dot,否则就没有怎么办?

设计模式是灵丹妙药,不是吗 ^_^


等到把这所有几千个技能全部搞定,起码也是一个数万个类、几十层的恐怖继承树,并且会用完23个设计模式(甚至再发明几个新模式出来,我也不会感到奇怪),精巧复杂到没有任何人愿意去碰它。


但,请注意,天杀的暴雪设计师,在最开始的设计方案里规定DOT不能暴击;后来又添加约定说某某某职业的某个dot可以暴击;另一个职业的某个dot在点天赋后可暴击;至于死亡骑士,在他穿了T9套装中的其中四件装备时,他的某个瘟疫类型的dot可以暴击——但另一个瘟疫dot永远不能暴击。


嗯嗯嗯,太好解决了——这不就是策略模式吗?

好吧,你再填几十几百个类体系,然后把旧的几十层继承树中的数万个类一个个都策略化吧。反正不是我在维护……



哎呀不好,那个枪毙了几百次都还没死的暴雪设计师又出馊主意了,他要求:当死亡骑士点了邪恶系的某个天赋时,不光给他增加一个新的dot、并且在这个新dot的存在期间,还要保护他的两个dot性疾病和1个debuf性疾病不被驱散!


继续补充:在WLK里面,那个脑袋都被子弹打成筛子了的暴雪设计师又跳出来了,用他满是漏洞的脑子出了个该杀的主意:他要求添加载具概念,当玩家坐上载具时,临时删除他的所有技能,替换为载具的技能;或者当他坐在特定载具的特定位置时,防止他受到任何伤害、并且允许他释放自己的所有技能!
更该死的是,他要求,一些技能本来不允许在移动中施放;但现在,当玩家坐在载具上某个位置时,要临时允许他移动施法!

还有,为了平衡某个野外战场,他还要求,在某方人数较少时,临时根据提高他们的生命值和所有技能的攻击力和治疗能力——这个改变必须根据进入战场的人数实时进行;在一方连续在某个战场失败时,同样要给他们一定补偿!



嗯嗯,看看这些不断改变的刁钻需求吧,如果没有面向对象,没有以策略模式为首的28个设计模式(我有理由相信你们需要至少28个设计模式而不是23个)的英明领导,我们这些没接触过大项目、不懂面向对象的傻B们,就是哭的拿眼泪把长城溶解掉都没办法吧?——我当然知道搭建长城的材料极难溶与水。

可怜的瞎子,你们的鱼汤很鲜吧?




嗯,到这里,希望读者们也能停下来,好好思考一下,看看这个问题该如何解决。














想到了没有?

这里是答案,看看你的想法是否能不谋而合吧:

这个问题暴雪在Diablo 2时代已经完美解决了: 法术/技能数据库化


所谓数据库化,其实等同于表格化,例如这个随便杜撰出来的简化方案,是设计一个有如下字段的数据表格:

法术ID 动画效果 作用范围 作用类型 属性 特殊限制 强化类型 特殊设定


其中,特殊设定字段可以是一段LUA代码,可以在其中搜索、设置极其特殊的伤害类型,或者查询顺劈斩/治疗链等奇特技能的传递目标等等。

特殊限制字段设定法术的施法或/和生效条件,如驱散限定为只能作用于魔法性buf/debuf(根据职业不同,可能有进攻性驱散和防守性驱散之一,也可能同时具备——这就体现在可否驱散敌方/友方目标的debuf)



在这个方案下,释放一个法术/技能,就成为一种查表运算——找到此法术ID,找到它的作用类型和伤害属性,计算特殊设定(包括但不限于顺劈斩模式的判断、天赋加成和天赋效果、雕文加成和雕文效果等等)。

于是,到最后,整个法术体系被分为一组组的魔法buf/debuf、物理buf/debuf,这些buf/debuf会影响伤害公式中的某个因子或者造成伤害效果;而伤害效果又分为立即伤害/立即治疗和持续伤害/持续治疗;最后则是一套影响范围判定机制。


举例来说,骑士开圣盾,他同时得到一个buf和一个debuf。
buf是“无敌”,效果相当于设置伤害公式 a*(....) 前面的a因子为0(没有无敌时此因子为1),于是所有伤害无效。
debuf则是“自律”,因为他的圣盾、圣疗技能判断条件里都有“有自律debuf,则不允许使用”的设定,于是禁止他在短时间内再次使用这些无赖技能。

敌方法师对他释放寒冰箭,系统受理,但查询骑士状态,发现他处于无敌状态,返回大大的两个字“免疫”。

然后,有一个敌方牧师对他使用驱散,查询牧师的驱散术发现,在驱散术的可驱散列表里没有圣盾术,于是提示无法驱散或驱散了另外的可驱散(魔法)效果。
敌方牧师迅速反应过来,再次对他使用强力驱散;查询牧师强力驱散术,发现该牧师在不久前使用过强力驱散,提示无法施法。
等待3秒后,敌方牧师发现自己的强力驱散冷却(cool down),再次使用强力驱散,查询发现强力驱散可驱散圣盾术,于是成功移除骑士的无敌状态。

现在,敌方法师再次对他释放寒冰箭,骑士切换冰抗光环,系统查询骑士状态,发现冰抗光环,又查询法师穿透等级,和暴击等级,根据公式计算能否命中、能否造成全额伤害以及能否暴击;然后提取法师和骑士双方装备、天赋数据代入公式计算伤害加成、减免数据,最后给出骑士受到的伤害数字(包括部分抵抗了多少)。



在暴雪设计师的整理之下,如上种种最终构成了几个表格;只要查询并代入相应的数据,即可计算出伤害/治疗数值以及类型;特殊效果可以用存储在数据库中的LUA代码补充完成。

最终的设计效果就好像内嵌了一个解释器,这个解释器会根据法术ID解释执行数据库内部的相关内容。


这样一来,只要伤害公式、伤害/buf类型、动画效果等等就位,那么新增一个法术就只不过是在数据库中新增一条记录;让某个角色学会一个新法术,则只需在它的可使用法术列表里添加法术名称(或法术ID);释放法术是根据法术ID在数据库中提取动画;计算伤害是根据法术ID读取伤害公式,然后代入相关字段并求值。

而这一切,甚至可以通过内部实现的编辑器,用图形界面完成。


如何?无与伦比的扩展性和便利性,是吧?


这么一整套东西,核心代码很可能只有数千甚至数百行。这是因为看似复杂的光环、buf等等东西,其实都已经抽象到和其他法术同样的流程上来了。最终,所有这些全部归一为解释执行伤害公式、提取执行指定动画之类寥寥几个通用过程——这显然同样是封装和归一化思想结出的另一颗果实。但为什么你就是想不到封装和归一化还能这样用?很简单,因为你被那些只会就着浅显的多态喋喋不休的笨蛋彻底引偏方向了。

我并没有亲自实现过这个,所以不敢断定这玩意儿靠几百行代码真的就能全部实现;但根据我在其它项目上的经验,这套东西应该就是数百行代码就可以写出来的——但写出并调试好这数百行代码所需的时间可能是一个星期甚至一个月。

相比于不假思索的写下class所必然导致的庞大、复杂的类层次,以及扯来扯去蛋疼无比的复杂的设计模式大网,这玩意儿的实现、维护、修改、扩展的便利程度,显然不是一个量级的:前者可能数百人努力数年、弄出几百万行代码都不能正确实现需求,而且必然bug满天飞;而后者,一个人,个把月,千把行代码,完成。如果实现水平足够的话,写完就再不用碰代码,而是去写图形编辑工具了。之后,扩展、维护都不过是用自己实现的工具拖来拖去再改改属性、数值,然后点存盘写入数据库,完事。


所以说,万不可死板的傻抱着面向对象不放。你所面对的问题才是最重要的。

你必须随机应变给出合适的方案——至于最后的设计方案会是什么流派,那玩意儿根本无关紧要。拿出一个简单、有效、可靠的方案,比什么都重要。

最后,还是我在前文总结的那句话:

封装可(通过固定接口而)应付需求变更、归一化可简化(类的使用者的)设计:以上,就是面向对象最最基本的好处。其它一切,都不过是在这两个基础上的衍生而已。


换言之,如果得不到这两个基本好处,那么也就没有任何衍生好处——应付需求变更/简化设计并不是打打嘴炮就能做到的。

再强调一遍,应付需求变更/简化设计并不是空洞的宣传口号。

封装和归一化类似军队制度建设,目标是搞出一个标准化、立体、多变、高效的指挥体系,从而获得打大战、打硬战的能力,然后再去轻松碾压问题。此所谓战略。

而那些堆砌无用的所谓“设计模式”的家伙,其实是在每个零件表面粘上挂钩——据他们说,这样会增加灵活性、应对需求变更、简化设计:比如说你带了个包,就可以挂他们在飞轮上粘的那个勾子上。

但实际上,你永远不会把包挂飞轮上(但你还是不得不为那些”聪明绝顶“的家伙“为了避免飞轮上的钩子脱落、挂住其它零件、离心力太大破坏挂在上面的包”等等而衍生出的”杰出“设计买单)。

幸运的是,除了某些企业项目(或其他类似性质的项目),你并不会用到这么烂的东西。因为这些笨蛋到处乱粘的钩子会不可避免的导致整个项目变成黏糊糊的一团,从而在旷日持久的拖延后自杀。

这种做法,显然是和面向对象的初心——通过封装和归一化获得高效指挥体系——背道而驰,从而使得每个中了这种毒的家伙参与的项目不可避免的成为一滩稀屎。

所以,很遗憾,只有杀马特设计师才会这样做。真正的设计师压根不会在设计发动机时考虑“飞轮上挂包”这样的需求(这就叫“以不知所谓的战术投机代替战略布局”)。他会干净利落的在整车设计时加个后备箱。




请注意,这并不是个比喻。

如你所见,在”每个零件上粘上挂钩“这种事情实在太过疯狂,所以在其他行业连玩笑都不是,因为再傻的人都不会这么做。

然而在软件设计时……这种事情是如此多见,多见到面向对象的领军人物会推荐别人这样做(

如此理解面向对象编程

);多见到业内很多大佬都不得不站出来,怒斥”面向对象是个骗局“。

名家吐槽:面向对象编程从骨子里就有问题

“面向对象编程是一个极其糟糕的主意,只有硅谷里的人能干出这种事情。” — Edsger Dijkstra(图灵奖获得者)

Edsger W. Dijkstra

如此沉重的心智负担,这显然是面向对象的原罪。


user avatar   zhou-sai 网友的相关建议: 
      

初学时概念理解更加困难。

不知道别人是怎么理解的,我本科和研究生都上过面向对象编程语言的课程,大学学的是Java,研究生学的是C++,但是我完全不理解什么叫继承、封装。面试的时候我只好把这堆概念死记硬背下来去回答。

后来我是通过将C++ primer 以及java编程思想上面的代码都抄了一遍理解的。

所以我觉得面向对象编程的老师讲课真是个浮云。上课的时候一人带一个笔记本,抄代码运行就完事了。


user avatar   ruikocon 网友的相关建议: 
      

之前看过一篇很经典的文章《名词王国里的死刑》,原文在steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html


国内有人翻译过,我贴下内容,链接在名词王国里的死刑 - CSDN博客

==================================


Hello,world!今天我给大家讲一个关于Java魔鬼国王和他在全国范围内驱逐动词的故事。


注意:这个故事并没有什么圆满结局。如果你心灵脆弱或者吹毛求疵的话,这个故事不适合你。如果你易于动怒或喜欢在别人的博客上妄加评论, 那么请立即停止阅读。


在我们开始这个故事之前,先让我们熟悉一下背景:



溢出的垃圾



所有使用Java的人都喜欢“用例”,所以让我们以一个用例开始吧:倒垃圾。就像这样:“Johnny,快去倒垃圾,他都快溢出来了!”


如何表达倒垃圾这一活动呢?如果你是一个正常说英语的人,你可以粗略地用以下几句话来描述它

在水池下取出垃圾袋
带着垃圾袋去车库
把它扔到垃圾桶里
走回来
洗手
坐回沙发上
继续玩你的电视游戏(或者干其他的事)


即使你不用英语思考,你也会想象出一系列类似的动作。不考虑你选择的语言,或者采取的具体步骤,取决于你采取的行动,倒垃圾是一系列终止于垃圾在外面,你回到屋子里的动作。


我们的思想充斥着各种或勇敢的,或暴躁的,或激昂的动作。我们生活,我们呼吸,我们走路,我们谈话,我们笑,我们哭,我们希望,我们害怕,我们吃,我们喝,

我们走,我们听,我们倒垃圾。我们能自由地“做”和“行动”。假如我们只是石头,生活没准还算好,但是我们不会自由。因为我们可以“做”事,所以才会自由。


我们的生活也同时充斥着各种“名词”。我们吃“名词”(食物),我们从商店买“名词”(商品),我们坐在“名词”(凳子)上。
“名词”(石头)可能会忽然砸到你头上,在你的“名词”(头)上弄一个“名词”(大包)。名词即事物,想想没有了事物我们会怎样?但他们仅仅只是事物,比如:

意味着结束或者结束本身,或者一些贵重物品,或者我们周围经常看到的事物的名字。这是一座建筑,那是一个石头。任何一个小孩子都能指出名词,仅此而已。发生在名词身上的
“变化”才是最有趣的事情。


变化需要动作。动作是生活的调料。动作甚至给了调料以调料!毕竟除非你“吃”它,你是不会感到香这种味道的。名词也许无处不在,但是生活一直在变并一直有趣的功劳还是在于动词、


当然,除了名词和动词,我们还有形容词,介词,代词,冠词,连词,语气词,和许许多多其他让我们构造有趣语言的词汇。它们都在语言中扮演着自己的角色,而且每一个都很重要。 如果它们哪一个不存在了的话,那是挺遗憾的事情。


那么,如果有一天我们不再用动词了,你是不是感到很奇怪呢?


在下面我要给大家讲的故事里,这件事情真的发生了……



名词王国



在Java王国中,国王Java以铁腕统治着他的国家,而他子民的思考方式也并不和你我一样。在这里,你可以看到,名词是十分重要的并直接服从国王的命令。

名词是最重要的居民,它们身穿艳丽的服装显得高贵而优雅,而这些衣服是由形容词提供的。而形容词哪,也很满意它们的生活,当然,他们不可能像名词那么高贵,
不过相比于动词来讲却幸运得很多。


因为,动词在Java王国里的日子,相当,相当的糟糕。


奉国王Java的法令,动词是隶属于名词的,但他们不仅仅是宠物而已。或者说连宠物都不是,在整个国家,动词负担起所有的劳力工作。实际上,他们是王国的奴隶,至少是
农奴或者契约奴之类的。Java王国的居民对自己的生活还是比较满意的,他们从来没有想到会发生什么变化。


动词负责王国里的所有工作,但是仍然获取不到任何尊重,甚至都不允许单独出来。如果一个名词被发现在公共场合出现,它会立即被名词逮捕。


当然“逮捕”也是一个动词,他也从不被允许单独行动;你必须创造一个“动词逮捕着”来协助逮捕。但是“创造”和“协助”哪?这样的话,“创造者”和“协助者”也各自在这个工作上 伴随“创造”和“协助”起到了重要的作用。


国王Java,在他的上帝Sun(现在是Oracle了吧...[译者注])的指引下,时不时地威胁要将所有动词驱逐出王国。如果那一天到来了,他们当然需要至少一个动词来做 各种工作,而从国王残忍的幽默感上猜测,这个动词很可能就是“执行”。


动词“执行”(execute),和它的亲戚“运行”,“开始”,“走你”,“做”,“就这样做”或者相似的什么词可以通过找到合适的“执行者”来替代任何其他的动词。想等(wait)一下? Waiter.execute().刷(brush)牙(teeth)?ToothBrusher(myTeeth).go().扔(take out)垃圾(garbage)?TrashDisposalPlanExecutor.doIt()。没有任何 一个动词是安全的,一切动词都会被执行的名词而取代。


在这种精神更加泛滥的角落,名词已经把动词驱逐干净。不仔细看的话,你会觉得仍然有动词存在,比如耕种或倒茶壶,但是一旦仔细观察,真相便浮出了水面:名词可以随意

命名紧跟在它们后面的动词“执行”,而不改变自身的角色。所以,当你看到“耕地者”在“耕地”,“倒茶壶者”在“倒”或者说“注册管理者”在“注册”,你真正看到的是魔鬼国王Java的
“执行者”大军,只不过他们披着所有者的外衣而已。



在其他王国里的动词



在其他编程语言的王国中,倒垃圾是一件很直白的事情,和我们用英语表述的十分相似。在Java王国中,数据实体是名词而函数是动词,而在其他王国中却不然:王国的居民 是混在一起的,而且在能顺利完成工作的前提下,只要他们愿意,既可以是名词也可以是动词。


比如在附近的C的领域,JavaScript的地盘,Perl的地盘和Ruby的地盘,他们可能会把倒垃圾这件事分解成一系列的动作(或者叫做动词或者函数)。如果他们将这些

动作以适当的顺序应用于适当的事物(拿垃圾,把它带出去,扔到垃圾桶里等等),倒垃圾的任务就圆满成功了。在这个过程中根本不需要执行者或其他的伴随者这出现。


在这些王国里,真的没有必要创造这么多的包裹器来包裹动词。他们没有“垃圾倾倒策略”之类的名词,或者“垃圾倾倒地点定位者”来只是你倒垃圾的路径,也没有“倒完垃圾后的回调”来保证你

倒垃圾后回到自己的沙发上。他们只是写一些动词来操作名词,并创建一个主要的名词,例如,提出垃圾(take_out_garbage())并把一些需要做的子动作放在里面。


在这些王国中,当需求提升的时候,也通常有一种机制来生成比较重要的名词。如果这些精明的创造者创造出了一个全新的名词,比如房子,马车,或者耕起地来比人还快的机器, 他们会被给予一个统一的概念:类。而人们会给类一个名称,一个描述,一些状态和一些操作建议。


这些王国与Java的不同之处在于,动词是允许单独出现的,你没必要创造新的名词去束缚他们。


Java王国的人一种轻视的态度看待他们的邻居;而这也是程序诸王国的现状。



如果你挖个足够深的洞...



在世界的另一边,有一篇贫瘠的居住地。在那里,动词居民的地位十分之高。这就是函数式王国,包括Haskellia,Ocamlica,Schemeria和一些其它的国家。
因为附近的国家很少,他们几乎不与Java王国何其附近的国家有接触。也正因为这样,函数式诸国们相互轻视,并有事没事的时候打一仗以排遣寂寞。


在函数式王国里,名词和动词一般被看做同样等级的居民。但是,名词,对是名词,基本上整天无所事事。他们的出现在做事或者执行任务的时候并没有多大意义,因为

活跃的动词们基本把能做的事情都做了。这里也没有什么奇怪的法律说要创造各种“帮助者”来帮助动词做事,因此在这些王国中,名词的数量和实际上存在事物的数量是相同的。


这样做的结果是,动词在这片土地上为所欲为(请原谅我的用词)。如果你是一个外来人,你很容易产生名词(函数)是这里最重要的居民的印象。顺便提一句, 这也是为什么这里被叫做函数式诸国还不是事物诸国的原因。


在最为遥远的地方,远离函数式诸国,存在着一块传说中的土地,“Lamda the
Ultimate”(终极lamda?霸气~[译者注])。传说中在那里,没有名词,只有动词。
那里有事物,但事物由动词组成。如果传说不虚,甚至数字,那里最为流行的货币,也是动词!数字0被表示为lamda(),数字1是lamda(lamda()),2是lamda(lamda(lamda())),以此类推。


在这片神奇的土地上,一切事物,别管你是名词,动词,还是其他什么,都是由最基本的动词lamda组成的。


老实说,Java王国中幸福生活着的居民并没有意识到另外一个世界的存在。你能想象得知此事之后的文化震动么?他们可能会发明一个新的名词(比如“憎恶”)来表达自己新的感受。



Java王国中的居民真的快乐么?



你可能觉得Java王国中的生活有点奇怪,如果糟糕的话还效率还会变得十分低下。不过,你能从一个地方的童谣中看出他们的幸福程度,而Java王国的童谣,是一群古怪的诗。 比如,这里的儿童经常朗诵的寓言: (这就不翻了[译者注])

       For the lack of a nail,         throw new HorseshoeNailNotFoundException("no nails!");      For the lack of a horseshoe,         EquestrianDoctor.getLocalInstance().getHorseDispatcher().shoot();      For the lack of a horse,         RidersGuild.getRiderNotificationSubscriberList().getBroadcaster().run(           new BroadcastMessage(StableFactory.getNullHorseInstance()));      For the lack of a rider,         MessageDeliverySubsystem.getLogger().logDeliveryFailure(           MessageFactory.getAbstractMessageInstance(             new MessageMedium(MessageType.VERBAL),             new MessageTransport(MessageTransportType.MOUNTED_RIDER),             new MessageSessionDestination(BattleManager.getRoutingInfo(                                             BattleLocation.NEAREST))),           MessageFailureReasonCode.UNKNOWN_RIDER_FAILURE);      For the lack of a message,         ((BattleNotificationSender)           BattleResourceMediator.getMediatorInstance().getResource(             BattleParticipant.PROXY_PARTICIPANT,             BattleResource.BATTLE_NOTIFICATION_SENDER)).sendNotification(               ((BattleNotificationBuilder)                 (BattleResourceMediator.getMediatorInstance().getResource(                 BattleOrganizer.getBattleParticipant(Battle.Participant.GOOD_GUYS),                 BattleResource.BATTLE_NOTIFICATION_BUILDER))).buildNotification(                   BattleOrganizer.getBattleState(BattleResult.BATTLE_LOST),                   BattleManager.getChainOfCommand().getCommandChainNotifier()));      For the lack of a battle,         try {             synchronized(BattleInformationRouterLock.getLockInstance()) {               BattleInformationRouterLock.getLockInstance().wait();             }         } catch (InterruptedException ix) {           if (BattleSessionManager.getBattleStatus(                BattleResource.getLocalizedBattleResource(Locale.getDefault()),                BattleContext.createContext(                  Kingdom.getMasterBattleCoordinatorInstance(                    new TweedleBeetlePuddlePaddleBattle()).populate(                      RegionManager.getArmpitProvince(Armpit.LEFTMOST)))) ==               BattleStatus.LOST) {             if (LOGGER.isLoggable(Level.TOTALLY_SCREWED)) {               LOGGER.logScrewage(BattleLogger.createBattleLogMessage(                 BattleStatusFormatter.format(BattleStatus.LOST_WAR,                                              Locale.getDefault())));             }           }         }      For the lack of a war,         new ServiceExecutionJoinPoint(           DistributedQueryAnalyzer.forwardQueryResult(             NotificationSchemaManager.getAbstractSchemaMapper(               new PublishSubscribeNotificationSchema()).getSchemaProxy().                 executePublishSubscribeQueryPlan(                   NotificationSchema.ALERT,                   new NotificationSchemaPriority(SchemaPriority.MAX_PRIORITY),                   new PublisherMessage(MessageFactory.getAbstractMessage(                     MessageType.WRITTEN,                     new MessageTransport(MessageTransportType.WOUNDED_SURVIVOR),                     new MessageSessionDestination(                       DestinationManager.getNullDestinationForQueryPlan()))),                   DistributedWarMachine.getPartyRoleManager().getRegisteredParties(                     PartyRoleManager.PARTY_KING ||                     PartyRoleManager.PARTY_GENERAL ||                     PartyRoleManager.PARTY_AMBASSADOR)).getQueryResult(),             PriorityMessageDispatcher.getPriorityDispatchInstance())).           waitForService();      All for the lack of a horseshoe nail.     


直到今天,这仍然是美好的建议。


尽管在Java王国的叙述方式和本.富兰克林的原作大有不同,但是这里的居民觉得他们的重新编排还是有一种不同的魅力在里面。


而最大的魅力在于“架构”,是所有人都能看见的。架构被国王Java授予了之高无上的地位,因为,架构全部是由名词构成的。正如我们所知的,在Java王国,名词即事物,事物

的荣耀高于一切的动词。架构由无数事物构成:你可以看或触摸的事物,给你留下壮观印象的事物,用棍子刮擦发出美妙声音的事物。国王Java,十分喜欢刮擦的噪音;
每当他新换车夫的时候,踢轮子发出的美妙声音让他觉得很满意。不管上面的故事有什么瑕疵,“事物”总是不缺少的。


作为人类,我们的第一本能总是寻找由各宗物体构成的庇护。庇护越坚固,我们感觉越安全。在Java王国,有很过坚固的东西让居民们感到安心。他们感慨如此庞大的架构
建造之神奇并认为它是“最为坚固的设计”。而且每当结构变化时,他们就越坚信这点。接着,架构的力量也变得强的令人生畏以至于没有人认为可以摧毁他。


除了坚固的架构之外,在Java王国中的所有东西很有调理地组织着:你会发现任何名词都会呆在适当的地方。这里每个故事都有一个固定的模式:实例构造在故事的表述中占了主要的篇幅,

因为每个抽象都会有一个管理者(Manager),而且每个管理者都有一个run()方法。Java居民们觉得他们可以用这种模型表述任何事情。这是一种“名词计算”,只要你愿意,
它可以满足任何抽象,任何计算。你需要的仅仅是足够的名词,名词的构造器,获取器方法,和重要的execute()函数来实现你的计划。


Java王国的居民活的不仅仅是幸福,简直是迸发出强烈的自豪感。



StateManager.getConsiderationSetter("Noun Oriented Thinking", State.HARMFUL).run()



或者,正如外面的世界所说,“面向名词的思考是有害的”


面向对象的编程把名词放到首位,但是我们为什么非得把名词捧上神坛以至于让语句变的如此啰嗦哪?
为什么一种语句成分的低位非得高于另外一种?这并不是好像面向对象的编程突然使得动词的低位降低,正如我们认为的那样。
这是一种奇怪的认识的扭曲。正如我的朋友 Jacob Gabrielso一次说到,提倡面向对象的编程好比提倡面向裤子的穿衣方式。


Java的静态类型系统,像起他任何类似的语言一样,有着共同的问题。但是过分强调面向名词的编程思想给人带来很大的困扰。 任何类型系统都会要求你重新梳理思路来配合它,但是清除独立的动词看起来十分不合情理。


C++并没有这个问题。因为C++作为C语言的超集允许你定义单独的函数。 此外,C++提供了独立的命名空间的概念。Java的类承载了太多的内容:命名空间,用户自定义类型,句法委托机制,可见性和作用域机制,等等。


不要误解了我的意思。我并没有说C++“好”,我只是赞美它至少相比于Java来讲灵活的类型系统。C++出现问题会让听众抓狂并且想杀了你
(比如,意想不到的段错误和其他难以发现的隐患)。并且在C++你很难找到一个能描述你的想法的咒语。
但是它灵活地可表述的思想的范围却远远超出了Java。因为C++提供给你了动词,谁想用一个没有动词的语言说话哪?


类是Java中唯一提供的建模的工具。所以当一个新的想法出现在你脑海的时候,你不得不重塑它,包装它,甚至弄碎它直到它变成一个名词, 即使它开始是一个动作,过程,或者任何其他不是“物”的概念。


我似乎回到了8,9年前一帮搞Perl的家伙对我说的:“伙计,并不是所有的东西都是对象的。”


很奇怪,Java似乎是主流面向对象语言中唯一一个完全以名词为中心的语言。
在Python或者Ruby中,你不会找到AbstractProxyMediator,NotificationStrategyFactory或者其他类似的东西。

为什么在Java中它们满地都是?我敢打赌这是原因出在了动词的身上。Python,Ruby,JavaScript,Perl当然,还有所有的函数式编程语言允许你声明并传递函数而不用用类包装它。


很显然,动态类型语言的使用更容易;你可以仅仅传递一个引用给函数,函数可以用名字获取它,而函数的调用者仅仅用合适的参数调用函数并正确地使用返回的值就可以了。


但是很多静态类型的语言同样也有第一类的函数。 这包括固定类型的语言比如C和C++,还有类型自动推断的语言比如Haskell和ML。 这些语言仅仅需要一些语法来建立,传递和调用函数的内容就可以了。


Java没有理由不简单地添加第一类函数并最终实现一个成熟的,没有扭曲的可以让人自由运用动词来实现他们想法的世界。 实际上,有一个基于JVM叫做
The Nice programming language
的语言实现了一个非常类似Java的语法,并包含了一个非常具有表现力的实现了使用动词方式:独立函数。
而Java强制你用Callback,Ruunable或其他匿名接口来包装它为一个类以便于调用。


Sun公司甚至没有打破他们一切函数都必须被类拥有的信条。任何匿名的函数都会具有一个隐式的this指针指向定义它的类;问题解决了。


我不知道为什么Sun公司坚持Java矗立在名词王国。我怀疑这是低估了他们的民众;他们添加了泛型,一个更加复杂的概念,所以他们不再关心如何保持他们语法的简练。
并且添加动词并不是一件坏事,这是因为Java现今所建立的:为一个Java程序员提供工具让他们按自己的想法编程更有意义。


我真心希望Java能修复这个缺陷,以便我可以把垃圾带出去并回来玩游戏或者一切当时在做的事情。


user avatar   feng-dong 网友的相关建议: 
      

面向对象的弊端在于作为一种建模技术没有很好的定义自己的适用范围。面向对象脱胎的环境有两个重要因素,一是基于 WIMP (Window, Icon, Menu, Pointer) 的图形化界面,二是早期提供图形界面接口的机器缺乏代码级别之外的组件管理方式 (比如 Unix 的进程和 IPC)。

面向对象在 WIMP 的环境中是很必要也是很成功的。原因是 WIMP 环境需要重量的实现继承提供的重用,WIMP 的对象种类能很好的被单继承模拟,WIMP 的属性和类别容易区分。而面向对象扩展到 WIMP 之外的环境中就失败了:

  1. 实际世界是多纬度的,属性和类别不好区分。红苹果是 color 属性为 red 的苹果,还是 Apple 的子类?
  2. 实际世界的工具是用来完成任务的。而不是象 WIMP 那样构建一个虚拟的空间化界面。
  3. 《人月神话》指出,编写 reusable code 比编写普通 code 至少要多花三倍的工作量。而面向对象的模糊了代码的重用和使用。使被重用的代码的依赖复杂化。导致很多不适合被重用的代码被重用。编写代码时要过分考虑重用的可能性。
  4. 其它管理复杂度的机制越来越流行。

user avatar   ling-jian-94 网友的相关建议: 
      

容我黑一波Java!Java简直就是对OOP概念滥用的极致。其他语言里,OOP是为程序员服务;Java里,OOP是为程序员添堵。

对于程序员来说,最苦恼的事情就是数据和程序如何和谐共处的问题,这两者是密切相关的,数据的定义改了程序就要修改;反过来,程序的功能变了,数据也要修改。但是数据和程序的定义方法是两种不一样的语法,如果分开来写,谁也不知道哪段程序和哪段数据是相关的了。

C程序员表示脑容量足够,分开写就分开写

C++程序员把数据和程序合在一起写了一个class。做不到的时候他们还是用C程序员的办法。

Java程序员要更加高瞻远瞩一些,他们是这么看这个问题的:数据和程序应该享有相同的权利。

Java程序员把数据加了一大堆getter和setter,然后把程序写进了一个static class,于是他有了两个类。他对此感到很满意。在Java的世界里,数据和程序都是class,这代表了公平和民主。

后来Java程序员对有main函数的class非常不满,觉得这侵犯了其他class的平等权,于是他们发明了JavaBean。从此以后他们再也不知道自己的程序究竟有多少个入口了。

Java程序员对没有implements的class感到恐惧,这代表它不能充分的实现多态性,调用这个类的代码不能正确调用其他有相同功能、相同接口的类。于是他给每个class创建了一个接口一模一样的interface。实际上这个interface从此以后再也没有其他class实现过。每次修改接口的时候,还要两个文件一起修改。

Java程序员极端排斥使用Object类型的指针,这代表自己对这个类型一无所知,甚至无法区分这个类型是自己定义的class还是别人定义的class。后来他定义了一个叫做MyObject的interface,再让所有的interface继承这个interface,再让所有的class实现那些继承自MyObject的interface,确保所有的类的实例都可以转换成MyObject类型。他感觉好多了。

Java程序员想用一个接口抽象出自己所有类的生命周期特性,来实现究极的多态。这个接口有start和stop两个方法,文档规定返回true表示成功,返回false或者抛出异常表示失败。最后99%的类的实现中,这两个方法的实现都是:return true;

Java程序员对单个方法的实现超过10行感到非常不安,这代表自己的代码可重用性很差。于是他把一个3个参数的长方法拆成了4个子过程,每个子过程有10个以上的参数。后来他觉得这样很不OOP,于是他又创建了4个interface和4个class。

---------------------------------------------------------------------------------

(这个人没黑过瘾又修改了回答)

Java程序员喜欢思考哲学问题。他之前思考一个问题思考了一个星期,这个问题是:创建服务究竟应该是Server类的方法还是ServerManager类的方法。后来他决定,创建服务完整的流程应该是ServerManager的方法,但其中把服务注册到Server的过程应该是Server的方法。之后的一个星期他在考虑这个方法究竟应该是私有方法还是公有方法。

Java程序员发明了很多设计模式,用来把不OOP的问题转换成OOP的形式,只需要多写两倍的代码。

欢迎一起黑!




  

相关话题

  为什么在计算机科学领域及编程中不使用现代数学建立的符号体系进行操作? 
  python如何将变量名转化为同名字符串? 
  既然有 HTTP 请求,为什么还要用 RPC 调用? 
  穷人家的女孩应该转去计算机专业吗? 
  《黑客帝国》中的先知是人还是程序? 
  线性代数对于计算机专业的作用是什么呢? 
  编程大神打符号和数字的时候也是用标准指法吗? 
  计算机图形学是否已经进入瓶颈期? 
  当游戏设计师需要具备哪些基本素养? 
  C++ 无法取代 C 吗? 

前一个讨论
五千年前到现在气候发生了很大变化吗,具体是怎么变化的,据说古中国犀牛分布殷商时期犀牛所能到达的北界?
下一个讨论
如何看待 2018 年 5 月 9 日特斯拉又出车祸?





© 2024-12-22 - tinynew.org. All Rights Reserved.
© 2024-12-22 - tinynew.org. 保留所有权利