面向对象编程方法的本质,其实就是一种如何将大量的代码与数据进行管理的归档方法。
每一个类,其实就像一个文档盒子,把一些相类似的东西,可以归类放在同一个盒子里(class)
比如,猫与老虎都属于猫科动物。
这样反向查阅时,可以快速找到相应的内容。
代码与普通文档不同的是,代码之间是有相互调用的。
于是我们把那些相互调用比较频繁的,通常又把它们一起放到更大的柜子里。(package或namespace)
然而,在归档的时候会发现很多细节问题。
比如A文档与B文档,只是多了两个属性,这些东西是否应该归在一起?
因为他们的区别很小,所以通常会把它们放在一起,并且把它们划成不同的类的同时,还要要保持它们之间的联系,然后标注成继承关系。
这样可以通过基类找出衍生类,也可以衍生类找出基类。
举例来说,比如一只骡子,算马还是算驴?
细分还有“马骡”与“驴骡”,要说马骡属于马,驴骡属于驴,这种管理起来似乎也不太好,显得马骡与驴骡变成了两种不同的动物,然而实际上它们的差异也没有那么大。
那怎么办呢,可以进行多重继承,它既可以继承于马,又可以继承于驴,这样就好了。
到此听起来都很完美,但是其实已经踩入了一个大坑。
因为面向对象整理归档在实践中,大家发现,尽量黑盒封装,在相互调用之间,尽量暴露最少的方法或属性,而且这个粒度拆的越细越好,这样可以更加灵活。
最好就是一个类,就它两三个方法,两三个属性,这样理解起来简洁,认知负担小。
然后多重继承的灾难就来了,复杂情况下一个普通的类,可能要继承几十个类。
但是你没有办法保证类之间的成员,不发生任何冲突。
比如马和驴,继承了马与驴的骡类,马与驴都能奔跑,这个奔跑是继承马的还是驴的?
因此这里的继承就需要选择了,指定选择实现谁的,这个显然会带来一些冲突。
当然一些语言里面已经解决了这个问题,比如元对象,动态类型之类的,但多线程环境下这些很容易遇到诡异的现象。
不过,更根本的问题不在这里。
更麻烦的是,骡子是不可生育的,两个类进行合成以后他失去的生育属性。
这就尴尬了,继承一般只能添加或修改成员,而不能删除。
因为如果能删除之类的属性的话就违反继承的概念了。
因为你大可以把一只兔子耳朵有羽毛全部去掉,然后能给它插上翅膀,加个尖嘴,变成老鹰。
这样无法保证大家不会搞得一团乱。
因此,最好的方式需要在马或驴的基类,实现一个是否可生育的属性,然后一路继承。
一个子类的实现要影响到爷爷类,这个肯定是有问题的。
问题出在哪里?一开始我们是想分门别类地把所有代码归档,通过抽象来进行简化管理,结果并没实现消除。
马驴骡的表达,如果改用组合关系表达,就会显得简单。
在常见的面向语言中,使用了一个抽象概念,接口。
接口设计的其实很简单,它不是为了继承而来,而是为了组合而设计。
任何一个类,它都由一个或多个接口组合而成,对于接口函数的具体的实现,是在实现类中,自己负责的。
原因在于所继承的基类方法与属性都是不可控的,所以要把继承的依赖,改成一种持有,这样就能消除多重继承带来的问题。
比如前面的马或驴,可以叫马和驴的特征抽象合成一个叫骡子接口,然后实现马骡和驴骡。
在实现中可以创建马和驴的对象,然后调他们相应的属性与函数,从而实现归档。
设计模式里面的不少方法,本质几乎都是为了变相解决多重继承的问题。
构造函数,对于组合关系来说,其实不是一个很好的设计。
因为构造函数的本质,其实本质就是一个可以直接返回自己对象的函数。
而把复杂的构建放在构造函数里,很容易在继承中导致各种灾难。
如何构建过程中,尴尬的发现不满足创建条件,怎么办?
一个下属,如果去办事,办到一半才发现这事办不了,那这个问题一定是出在领导身上,而不是下属身上的,因为这种问题应该一开始就避免。
构造函数无法合理传递返回出错信息,只是为了new的时候,自动调用运行的那么一下,实在没有必要。
因此,在设计模式中,通常会采用factory或builder的方式来创建对象。
到这里便会发现,已经开始尝试各种绕开类的继承了。
这样,就好比一块石头,你可以把它凿成你想要的样子拿来用。
然后我们知道builder负责造零件,factory是负责出产品的,假如我们把他们两个融合起来。
要是切成大量足够细的接口与实现,直接将它们进行组合,岂不要好得多。
但是面向对象的语言,来实现这些的时候,会非常的笨拙。
很显然,那干脆,为什么我们不一步到位呢?
万物皆是组合而成。
于是trait,是接口,impl就是实现,struct,就是属性。
无论任何情况下,只要将它们进行组装就好了。
Rust天然就是这样,因此不要构造函数也很天然。
这样并非没有缺点,虽然看来灵活了,但是代码组织管理上的负担并没有消除。
大量的trait,还缺乏一种更加有效的组织管理方法。
很显然面向对象的编程的设计,是从代码级别实现组织管理,在过去的不少大型软件中已经得到了很好的实践。
并且面向对象的编程语言,因为它的规约性极强,可以让IDE也很强大,对于大型代码的管理非常方便。
如何将这两个优势进行结合,应该是未来语言的发展方向。
当我们创造一个概念时,这个概念必然会以某种形式反过来束缚我们。
为构造函数赋予特殊的语法地位,并不总是一种画蛇添足。在不同的场合下,专用的构造函数,和语法地位与普通函数没有任何差别的工厂函数,可能有着不同的优劣对比。
先来看工厂函数优于构造函数的方面:
可见其中很多场合是现实中不可避免的,所以实际编程中,工厂函数一定会被用到,哪怕语法中已经提供了专门的构造函数也是一样。于是用户时刻面临两种选择,需要仔细思考每个具体地方应该使用哪种,心智的开销、需求变化或单纯误用之后被迫重构的麻烦,都会不断引起反思。事实上,既然其中一种所能涵盖的用场确实是另一种的超集,所以,最终产生是否工厂函数其实可以包办一切、构造函数是否只是多余累赘的念头也是顺理成章。很难说,那些干脆没有构造函数语法概念的新生代语言完全没有受到这种观念的影响。
那么,这种观念是否总是正确呢?不妨就回来接着看下反面,构造函数优于工厂函数的方面:
以上最后一条或可视作小众场合,不做过多计较。事实上,最大的着眼点依然在于封装性。构造函数对对象创建的一切入口,从“法制”而非“人治”的层面,有着严密、完备、无所遗漏的接管,并严格杜绝一切产生非一致的非法对象状态的可能性。比起“创建对象”的作用本身,这种“完备”才是其真正意义所在。在笨重臃肿的软件架构日益受到诟病、乃至连OO的“政权合法性”都日益受到挑战的今天,或许强调这种名词并不容易引起共鸣。但实际上,封装性真的只是OO的一部分吗?不妨设想,如果完全扔掉封装性,则OO会变成怎样?——答案是,其实不会怎样,封装性其实是OO中最可有可无的一部分。有诸多动态语言,事实上就是几乎没有一点封装性,但表达能力上并没有受到任何影响,照样OO得欢。在它们里,可能构造函数才是真正的画蛇添足,模仿残余,完全去掉也没有关系。比起这点小小侧面,不如说它们欠缺的是整个的“法制”系统,所以才会项目一大就不免在“人制”方面如临大敌,各种规范限制工具辅助严阵以待不敢怠慢,一时爽火葬场的段子广为流传。这也是对本质的一种揭示,说到底,封装性是一种“减法”,并非为软件建模提供什么功能和方便,而正是以提供“不便”的方式,来对人员过度自由的行为进行约束,从而一方面提高软件质量,另一方面提升代码的可读性和可理解性,从而使语言的工程能力得到飞跃。极端一点说的话,弱类型语言才是自由度更大的,一块内存一时可以当整数、一时可以当字符串、再一时还能当指针,按说功能只会是强类型语言的超集。但大规模用起来会是什么感觉想必也显而易见。然而这也并不是在对某一类语言进行彻底否定,因为世上存在各种各样的领域,没有一把锤子能够普遍适应,天下也不尽是大型工程,在需要轻快敏捷的地方非要套用一大堆严谨规则也是自找麻烦。所以每种语言也有自己的选择,针对预定的问题域被打造成了特定的样子。如何认清自己所面临的问题域,选择最合适的工具,从而避免削足适履的麻烦,发挥最大的应有效用,才是语言的使用者需要始终思考的主题。