在从类关系说起一文中,我们谈到了几种类与类之间的关系,在此来深入一下对象与对象之间的通信问题.为什么要深入对象与对象之间的通信呢,其根本在于,系统中不会存在唯一的对象,不同的对象势必要相互进行交流.
初学者的问题
在开始介绍迪米特法则之前,先让我们会到初学编程的时代。
在我们刚开始学习编程的时候,通常会将所有的方法都声明为公有(public),但随着我们代码量的增加,我们都会遇到一个典型的问题:
在调用某个对象的方法时,我们发现编译器提示这个对象所有的方法,这意味着该对象处在不安全的状态。为什么这么说呢?如果我们将这个对象比作一个人,那么这个人在别人面前是赤裸的,没有任何隐私,这让别人有机会观察你的一切行为,并某刻致命一击。除此之外,这个完全暴露的人,也会让别人不知所措。
这显然不是我们想要的,因此我们需要某种机制来限制的对象信息的公开:哪些信息是可以公开的,哪些是不可以公开的,在java中,我们通过方法的权限来实现这一点,比如private修饰的方法只有对象自己内部可以调用,public修饰的方法是公开给其他对象的等。
现在,你可能已经明白,java的设计者为什么要“多此一举”的为方法设计权限了。那么有人会问,我该怎么确定哪个方法应该被设计成公有的,哪些又应该被设计成私有的呢?
当你心里有这个疑问的时候,说明你已经开始关注我们经常提到的面向对象编程的原则之一:封装,即如何划分对象的结构。
我们都知道对象的结构的可被划分为静态属性和动态属性,所谓的静态属性就是值对象固有的属性,比如任何一个生命体都有年龄,而动态属性也称为行为属性,指的是对象所表现出来的行为,比如袋鼠能跳,能呼吸等。而这静态属性和动态属性又可以细分为可公开的静态属性,可公开的动态属性等。也就是说,划分对象的结构实则就是确定某个对象的动态属性和静态属性,在此基础上再来确定属性是否可公开等。
不难发现,这个过程和我们的认知的思维过程很类似:大脑试图从各种各样的的物体中抽取特征。比如,我们看到猫,狗,仙人掌,为了能区分它们,我们的大脑会对这三者进行特征抽取,比如猫和狗都可以移动,有眼睛,会叫,有爪子,而仙人掌则是不可移动,有刺,不能叫等,通过这种特种抽取,我们能区分出动物和植物的区别。换言之,我们之所以能区分出不同的物体,都是因为我们的大脑已经默默的为我们做了特征抽取的工作,这个过程如果由我们主动去做就称之为抽象编程。
揭秘迪米特法则
迪米特法则(Law of demeter,缩写是LOD)要求:一个对象应该对其他对象保持最少了解, 通缩的讲就是一个类对自己依赖的类知道的越少越好,也就是对于被依赖的类,向外公开的方法应该尽可能的少。
迪米特法则还有一种解释:Only talk to your immediate friends,即只与直接朋友通信.
首先来解释编程中的朋友:两个对象之间的耦合关系称之为朋友,通常有依赖,关联,聚合和组成等.而直接朋友则通常表现为关联,聚合和组成关系,即两个对象之间联系更为紧密,通常以成员变量,方法的参数和返回值的形式出现.
那么为什么说是要与直接朋友通信呢?观察直接朋友出现的地方,我们发现在直接朋友出现的地方,大部分情况下可以接口或者父类来代替,可以增加灵活性.
(需要注意,在考虑这个问题的时候,我们只考虑新增的类,而忽视java为我们提供的基础类.)
不难发现,迪米特法则强调了一下两点:
- 第一要义:从被依赖者的角度来说:只暴露应该暴露的方法或者属性,即在编写相关的类的时候确定方法/属性的权限
- 第二要义:从依赖者的角度来说,只依赖应该依赖的对象
实例演示
先来解释第一点,我们使用计算机来说明,考虑计算机关机的过程.当我们按下计算机的关机按钮的时候,计算机会执行一些列的动作会被执行:比如保存当前未完成的任务,然后是关闭相关的服务,接着是关闭显示器,最后是关闭电源,这一系列的操作以此完成后,计算机才会正式被关闭。
我们来用简单的代码表示这个过程,在不考虑迪米特法则情况下,我们可能写出以下代码
//计算机类
public class Computer{
public void saveCurrentTask(){
//do something
}
public void closeService(){
//do something
}
public void closeScreen(){
//do something
}
public void closePower(){
//do something
}
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
public class Person{
private Computer c;
...
public void clickCloseButton(){
//现在你要开始关闭计算机了,正常来说你只需要调用close()方法即可,
//但是你发现Computer所有的方法都是公开的,该怎么关闭呢?于是你写下了以下关闭的流程:
c.saveCurrentTask();
c.closePower();
c.close();
//亦或是以下的操作:
c.closePower();
//还可能是以下的操作
c.close();
c.closePower();
}
}
发现上面的代码中的问题了没?观察clickCloseButton()方法,我们发现这个方法无法编写:c是一个完全暴露的对象,其方法是完全公开的,那么对于Person来说,当他想要执行关闭的时候,却发现不知道该怎么操作:该调用什么方法?靠运气猜么?如果Person的对象是个不按常理出牌的,那这个Computer的对象岂不是要被搞坏么?
迪米特法则第一要义
现在我们来看看迪米特法则的第一点:从被依赖者的角度,只应该暴露应该暴露的方法。那么这里的c对象应该哪些方法应该是被暴露的呢?很显然,对于Person来说,只需要关注计算机的关闭操作,而不关心计算机会如何处理这个关闭操作,因此只需要暴露close()
方法即可。
那么上述的代码应该被修改为:
//计算机类
public class Computer{
private void saveCurrentTask(){
//do something
}
private void closeService(){
//do something
}
private void closeScreen(){
//do something
}
private void closePower(){
//do something
}
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
public class Person{
private Computer c;
...
public void clickCloseButton(){
c.close();
}
}
看一下它的类图:
接下来,我们继续来看迪米特法则的第二层含义:从依赖者的角度来说,只依赖应该依赖的对象。
这句话令人有点困惑,什么叫做应该依赖的对象呢?
还是用上面“关闭计算机”的例子来说明:
准确的说,计算机包括操作系统和相关硬件,我们可以将其划分为System对象和Container对象。当我们关闭计算机的时候,本质上是向操作系统发出了关机指令,而实则我们只是按了一下关机按钮,也就是我们并没有依赖System的对象,而是依赖了Container。这里Container就是我们上面所说的直接朋友—只和直接朋友通信.
再来深入讨论一下这点:
only talk to your immedate friends
这句话只说明了要和直接朋友通信,但是我觉得这还不完整,我更愿意将其补充为:
make sure your friends,only talk to your immedate friends,don‘t speak to strangers.
大意是:确定你真正的朋友,并只和他们通信,并且不要和陌生人讲话.这样做有个很大的好处就是,能够简化对象与对象之间的通信,进而减轻依赖,提供更高的灵活性,当然也可以提供一定的安全性.
现在来想想现实世界中的这么一种情况:你是一个令人瞩目的公众人物,周围的每个人都知道你的名字,当你独自走在大街上的时候会是怎么样的一种场景?每个人都想要和你聊天!,和你交换信息!!接着,你发现自己已经寸步难行了.如果这时候你有一个经纪人,来帮你应对周围的人,而你就只和这个经纪人通信,这样就大大减轻了你的压力,不是么?此时,这个经济人就相当于你的直接朋友.
迪米特法则第二要义
现在,我们再回顾”关机计算机”这个操作,前面的代码只是遵从了第一要义,,现在我们结合第二要义来进行改进:System和Container相比,System并非Person的直接朋友,而Container才是(Person直接打交道的是Container).因此我们需要将原有的Computer拆分成System和Cotainer,然后使Person只与Container通信,因此代码修改为:
//System
public class System{
private void saveCurrentTask(){
//do something
}
private void closeService(){
//do something
}
private void closeScreen(){
//do something
}
private void closePower(){
//do something
}
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
//Contanier
public class Container{
private System mSystem;
public void sendCloseCommand(){
mSystem.close();
}
}
//Person
ublic class Person{
private Container c;
....
public void clickCloseButton(){
c.sendCloseCommand();
}
}
来看一下它的类图:
延伸的第二要义
在上文中,我们还提到,直接朋友出现的地方,我们可以采用其接口或者父类来代替.那么在这里,我们就可以为Container和System提供相应的接口
//System interface
public interface ISystem{
void close();
}
//System
public class System implements ISystem{
private void saveCurrentTask(){
//do something
}
private void closeService(){
//do something
}
private void closeScreen(){
//do something
}
private void closePower(){
//do something
}
@override
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
//IContainer interface
public interface IContainer{
void sendCloseCommand();
}
//Contanier
public class Container implements IContainer{
private System mSystem;
@override
public void sendCloseCommand(){
mSystem.close();
}
}
//Person
ublic class Person{
private IContainer c;
....
public void clickCloseButton(){
c.sendCloseCommand();
}
}
来看一下它的类图:
对比这两种方案,明显这种方案二的解耦程度更高,灵活大大增强.不难发现,这应用了我们前面提到的依赖倒置,即面向接口编程.
除此之外,我们发现随着不断的改进,类的数量也在不断的增加,从2个增加到5个,这意味着为了解耦和提高灵活性通常要编写的类的数量会翻倍.因此,你需要在这做一个权衡,切莫刻意为了追求设计,而导致整个系统非常的冗余,最终可能得不偿失.
总结
有人会觉得Container像是一个中介(代理).没错,我们确实可以称其为中介,但这并不能否认他是我们的直接朋友:在很多情况下,中介可以说是我们的一种代表,因此将其定义为直接朋友是没有任何问题的.比如,当你想要租房的时候,你可以找房屋中介,对方会按照你的标准为你寻找合适的住房.
但是问题来了:那么做一件事情需要多少中介呢?总不能是我委托一个中介A帮我找房子,但中介A又委托了中介B,中介B又委托了中介C….等等,如果真的是这样,那还不如我自己去找房子效率更高.在实际开发中,委托的层次要控制在6层以下,多余6层以上的会使得系统过分的冗余和并切会委托层次过多而导致开发人员无法正确的理解流程,产生风险的可能会大大提高.