一
一群盲人被带到一头大象面前,让他们摸摸大象像什么。一个瞎子摸到了大象的腿,说大象像一棵树;另一个瞎子摸到了大象的耳朵,说大象像一个扇子;第三个摸着大象的身体,说它像一堵墙;第四个瞎子则拽着大象的尾巴说,它分明像一根绳子…
盲人摸象
大象是一个不可分割的完整组件,但对外提供不同的服务。不同类型的客户使用不同的服务,完全无需知道其它服务的存在,也无需知道背后是谁再为自己提供服务。
而这种方式,无疑降低了客户与服务提供者之间的耦合:
- 一方面,每个服务背后的具体提供者都可以自由替换,而不用担心客户代码受到影响(但要遵从里氏替换原则,即要遵从客户与服务之间的契约);
- 另一方面:作为服务提供者——大象——不会强迫客户依赖它无需依赖的东西,而只依赖自己特定需要的服务接口;
- 同时,由于你没有把整只大象呈现在客户面前,这就约束了客户随意使用大象提供的其它接口所带来的不必要的耦合。
如果回到具体程序设计上,一种可能的实现则会如下:
在这个版本的实现方式中,所有接口的具体实现都集中在类里。当接口足够多,实现足够复杂时,这毫无疑问会形成上帝类。
不过,这貌似没有太大问题。毕竟,大象本来就是个大块头。
三
组件化的设计方式,某种程度也是一种多角色对象设计方式。
我们每个人在生活中都扮演不止一个角色:
- 在孩子面前,我们是父母;
- 在父母面前,我们是子女;
- 职场上,在上司面前,我们是下属;
- 在下属面前,你是上司…
不同角色,要求履行的职责也不同:
- 作为父母:我们要给孩子讲故事,陪他们玩游戏,哄它们睡觉;
- 作为子女:我们则要孝敬父母,听取他们的人生建议;
- 作为下属:在老板面前,我们需要听从其工作安排;
- 作为上司:需要安排下属工作,并进行培养和激励;
- …
所有这些角色,或者有所关联,或者风马牛不相及,但却可以很好的集中于一个人身上。
四
每一个角色,都有其存在的上下文(),或称为环境。
比如:你不会回到家里去扮演上司的角色;也不会在公司环境下扮演父母的角色。
对某个人来说,如果一个上下文并不存在,或者已经脱离了某个上下文,那么对他而言,对应的角色也就不会或不再存在。比如,如果一个人还没有孩子,那他就不会扮演父母这个角色;一旦一个人离开职场,那他就无需再承担与职场有关的角色。
类似的,一个人也会由于上下文的变化而承担他过去无需承担的责任。比如,当有了孩子之后,他就要开始承担作为父母的责任;而一旦被提拔为管理者,就需要开始承担上司的职责。
因而,每个人需要承担的角色,都在随着环境的变化而变化。
对于软件开发而言,环境对应的是,对应的是需求。
需求的变化,对于很多系统而言是很频繁的。这就意味着我们之前在一个类里实现所有角色相关代码所得到的上帝类,在频繁变化的需求面前,由于角色变更而导致的变化也是很频繁的。
五
在频繁的变化面前,上帝类的修改,往往并不是简单的增加或删除某个角色相关代码那么简单。
我们已经知道,的主要作用是为了模块化,通过将关联紧密的元素放到一个类中,然后通过封装手段将易于变化的细节隐藏起来,只暴露更为抽象,更为稳定的接口,从而降低模块间耦合。最终达到让软件在变化面前,局部化影响,容易修改的目的。
与之相反的是,毫无边界控制的全局数据访问,从而造成大面积无规则的对于实现细节的依赖。这样的做法,在初次实现时,一定是最快速简单的。但同时也是在变化面前最为脆弱的。
而类,在OOPL里,是不可再分割的最小模块。而在类的内部,没有边界访问控制,对于类内部的一切实现细节的访问均是自由的。因而,在一个类内部,所有的成员变量都相当于全局变量,所有的函数,都相当于全局函数。而在类内部,没有任何强制手段可以阻止对这些“全局变量”和“全局函数”的自由访问。
如果一个类很小,职责单一,那么内部的高耦合所造成的影响就会很小(所谓高内聚,正是要把关联紧密的事物放在一起,从而将变化带来的影响控制在类内部)。
但高内聚的另外一面是:只有关联紧密的事才应该被放在一起。对于一个多重职责的上帝类,内部的各个元素之间的关联紧密程度几乎可以肯定是不一致的。
在这种情况下,没有任何边界访问控制的类内部,就会很容易导致本不该有的高耦合。
如果还是觉得难以理解,就不妨想象一下,把整个系统都放到单个类里,这样的设计会导致怎样的耦合度。
六
2003年,伴随着出版了《领域驱动设计》, 很快发表了一篇文章《Anemic Domain Model》(《贫血领域模型》 ),对那些只有数据,没有有价值行为的所谓领域对象进行了强烈的批评。
这篇文章引起了 区很大的反响。但却并没有阻挡住 区依然在大量使用贫血模型的脚步,,而不是Domain Object,被当作表达业务逻辑的核心场所。
但这也不全是这些团队的错。其根本原因在于,当大家尝试使用充血模型时,发现在易于变化的业务逻辑面前,那些领域类很容易就变成了上帝类,然后随着业务的变化不断修改。完全无法达到局部化影响的效果。大家都是要解决问题的,不能为了充血而充血不是/p>
因而,很多团队在实践时,继续纠结的披着面向对象的外衣,行着面向过程之实。
七
在单一类里实现所有角色所得到的上帝类,被称作水平上帝类(或横向上帝类)。
水平上帝类带来的问题,除了像所有上帝类一样,造成了不必要的高耦合之外,还会导致难以复用的问题。
现在我们定义几个不同的,用来表现不同类型的人。通过这些,可以实例化一个个不同的对象:具体的人。
首先是A类型人的实现:
下面是B类型人的实现:
对于和,他们都扮演了和的角色,并且这两个角色的实现方式也完全相同。同时,他们也各自扮演了对方不具备的角色:扮演了,而则扮演了。
这就造成了这两个类之间是有部分重复代码的。
但这的重复根本难不倒我们,将两者重合的角色代码提取到一个基类中即可。
然后,让两个类都从此它继承:
到目前为止,一切都好。此时我们再增加一个新的类型,让角色的复用关系更加复杂。如下:
此时,再想通过单根继承来解决复用问题,将会变成一个不可能完成的任务。
八
单根继承的最大问题在于:只能解决单个变化方向的问题,对于多个变化方向无能为力。
比如,在下面的关系中,基类存在两个抽象函数:和。这代表两个不同的变化方向。如果和各自存在两种不同的实现方式,则会存在种不同的组合关系。
两个变化方向
由此可以看出,当存在多个变化方向时,使用单根继承来消除重复,不仅会造成大量的仅仅为消除重复存在的中间类(比如本例子中的和),却最终依然无法彻底消除重复。
因而,为了解决我们之前所述的多角色对象的重复问题,我们必须另辟蹊径。
九
七巧板,是大家熟知的一种其源自中国的古老智力游戏。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!