Java设计模式——迪米特法则

来源:互联网 发布:淘宝不能货到付款吗 编辑:程序博客网 时间:2024/05/16 10:12

迪米特法则又叫作最少知识原则,就是说,一个对象应当对其他对象要有尽可能少的了解。

一、狭义的迪米特法则

如果两个类不必彼此直接通信,那么这两个类就不应该发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法时,可以通过第三者转发这个调用。

1.朋友圈与陌生人

如下图所示,“某人”与一个“朋友”组成自己的朋友圈,两个人都需要与一个圈外的“陌生人”发生相互作用。


“朋友”与“陌生人”若是朋友,组成“朋友”的朋友圈如下图所示。


相比较之下,“某人”其实并不需要与陌生人直接发生相互作用,但是“朋友”更需要与“陌生人”发生相互作用。这时候,迪米特法则建议“某人”不要直接与“陌生人”发生相互作用,而是通过“朋友”与之直接发生相互作用,如下图所示。


这时候,“朋友”实际上起到了将“某人”对“陌生人”的调用转发给“陌生人”的作用。这种传递叫作调用转发(Call Forwarding)。所谓调用转发,需要隐藏“陌生人”的存在,使得“某人”仅知道“朋友”,而不知道“陌生人”。换言之,“某人”会认为他调用的这个方法是“朋友”的方法。

2、朋友圈的确定

”某人”的朋友圈是如何确定的?以下的条件成为朋友条件:

  • 当前对象本身(this)。
  • 以参量形式传入到当前对象方法中的对象。
  • 当前对象的实例变量直接引用的对象。
  • 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友。
  • 当前对象所创建的对象。

任何一个对象,如果满足上述条件之一,就是当前对象的“朋友”,否则就是“陌生人”。

3、不满足迪米特法则的系统

系统由三个类组成:分别是Someone,Friend和Stranger。其中Someone与Friend是朋友,而Friend与Stranger是朋友。系统的结构如下图所示。


从上面的类图可以看出,Friend持有一个Stranger对象的引用,所以Friend与Stranger是朋友。Friend类的源代码。

public class Friend {private Stranger stranger=new Stranger();public void operation2(){//code}public Stranger provide(){return stranger;}}

Someone的源代码

public class Someone {/** * 调用Stranger的方法 * @param friend */public void operation1(Friend friend){Stranger stranger=friend.provide();stranger.operation3();}}

可以看出,在这种情况下,Someone与Stranger是朋友关系。Someone的operation1()方法接受Friend为参量,显然根据“朋友”的定义,Friend是Someone的朋友。其中Friend的provide()方法会提供自己所创建的Stranger的实例。显然,Someone的operation1()方法不满足迪米特法则。因为这个方法引用了Stranger对象,而Stranger对象不应该是Someone的朋友。

4、使用迪米特法则就行代码重构

可以使用迪米特法则对上面的例子进行改造,改造的做法是调用转发。改造后的类图如下图所示。


从上面的类图可以看出,与改造前相比,Someone与Stranger之间的联系已经没有了。Someone不需要知道Stranger的存在就可以做同样的事情。

Someone的源代码。

public class Someone {/** * 通过Friend转发方法,调用Stranger的方法 * @param friend */public void operation1(Friend friend){friend.forward();}}

从Someone的源代码可以看出,Someone通过调用自己的朋友Friend对象的forward()方法做到了原来需要调用Stranger对象才能够做到的事情。

Friend的源代码。

public class Friend {private Stranger stranger=new Stranger();public void operation2(){//code}/** * 调用转发方法 */public void forward(){stranger.operation3();}}

原来Friend类的forward()方法所做的就是以前Someone要做的事情:使用了Stranger的operation3()方法。这种forward()方法叫做转发方法。

由于使用了调用转发,使得调用的细节被隐藏在Friend的内部,从而使Someone与Stranger之间的直接联系被省略掉了。这样一来,使得系统内部的耦合度降低。在系统的某一个类需要修改时,仅仅会直接影响到这个类的“朋友”们,而不会直接影响到其余部分。

5、狭义迪米特法则的缺点

遵循狭义的迪米特法则会产生一个明显的缺点:会在系统里制造出大量的小方法,散落在系统的各个角落。这些方法仅仅是传递间接的调用,因此与系统的商务逻辑无关。当设计师试图从一张类图看出总体的架构时,这些小的方法会造成迷惑和困扰。

遵循狭义的迪米特法则会使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。

6、与依赖倒转原则互补使用,克服狭义迪米特法则的缺点

为了克服狭义迪米特法则的缺点,可以使用依赖倒转原则,引入一个抽象类型引用“抽象陌生人”对象,使“某人”依赖于“抽象陌生人”。换言之,就是将“抽象陌生人”变成朋友。

如下图所示。


“某人”现在与一个抽象角色建立了朋友关系,这样做的好处是“朋友”可以随时将具体“陌生人”换掉。只有新的具体“陌生人”具有相同的抽象类型,那么“某人”就无法区分他们。这就允许“陌生人”的具体实现可以独立于“某人”而变化。如下图所示。


可以引入一个抽象AbstractStranger,让Someone依赖于这个抽象角色,从而使Someone与Stranger的具体实现脱耦。如下图所示。


AbstractStranger由一个Java接口实现,源代码如下。

public interface AbstractStranger {abstract void operation3();}

Stranger实现了这个接口,源代码如下。

public class Stranger implements AbstractStranger{public void operation3(){//code}}

Someone依赖于抽象类型AbstractStranger,源代码如下。

public class Someone {public void operation1(Friend friend){AbstractStranger stranger=friend.provide();stranger.operation3();}}

Friend提供的也是抽象类型AbstractStranger,源代码如下。

public class Friend {private AbstractStranger stranger=new Stranger();public void operation2(){//code}public AbstractStranger provide(){return stranger;}}

二、广义的迪米特法则

迪米特法则所谈论的,就是对对象之间的信息流量、流向以及信息的影响的控制

在软件系统中,一个模块设计得好不好最主要、最重要的标志,就是该模块在多大程度上将自己的内部数据和其他与实现有关的细节隐藏起来。一个设计得好的模块可以将它所有实现的细节隐藏起来,彻底的将提供给外界的API和自己的实现分隔开来。这样,模块和模块之间就可以仅仅通过彼此的API相互通信,而不会理会模块内部的工作细节。这一概念就是“信息的隐藏”,或者叫作“封装”,是软件设计的基本教义之一

信息的隐藏非常重要的原因在于,它可以使各个子系统之间脱耦,从而允许它们独立的被开发、优化、使用、阅读以及修改。这种脱耦化可以有效的加快系统的开发过程,因为可以独立地同时开发各个模块。它可以使维护过程变得容易,因为所有的模块都容易读懂,特别是不必担心对其他模块的影响。

迪米特法则的主要用意是控制信息的过载。将迪米特法则运用到系统设计中时,要注意下面的几点。

(1)      在类的划分上,应当创建有弱耦合的类。类之间的耦合越弱,就越有利于复用。一个处于弱耦合中类一旦被修改,不会对有关系的类造成波及。

(2)      在类的结构设计上,每一个类都应当尽量降低成员的访问权限(Accessibility)。换言之,一个类包装好各自的private状态。一个类不应当public自己的属性,而应当提供取值和赋值的方法让外界访问自己的属性。

(3)      在类设计上,只要有可能,一个类应当设计成不变类。

(4)      在对其他类的引用上,一个对象对其对象的引用应当降到最低。

1、广义的迪米特法则在类的设计上的体现

优先考虑将一个类设置成不变类

Java语言的API提供了很多的不变类,比如:String,BigInteger,BigDecimal等封装类都是不变类。

一个对象与外界的通信大体可分两种,一种是改变这个对象的状态,另一种是不改变这个对象的状态。当设计任何一个类的时候,都首先考虑这个类的状态是否需要改变。即便一个类必须是可变类,在给他的属性设置赋值方法的时候,也要保持吝啬的态度。除非真的需要,否则不要为一个属性设置赋值方法。

尽量降低一个类的访问权限

在满足一个系统对这个类的需求的同时,应当尽量降低这个类的访问权限(Accessibility)。对于顶级的类来说,只有两个可能性的访问等级。

  • package-private:这是默认的访问权限。如果一个类是package-private的,那么它就只能从当前库访问。
  • public:如果一个类是public的,那么这个类从当前库和其他库都可以访问。

一个具有package-private访问权限的好处是,一旦这个类发生修改,那么受到影响的客户端必定都在这个库内部。

尽量降低成员的访问权限

类的成员包括属性、方法、嵌套类和嵌套接口等,一个类的成员可以有四种不同的访问权限。

  • private:这个成员只可能从当前顶级类的内部访问。
  • package-private:这个成员可以被当前库中的任何一个类访问,这是默认访问权限。
  • protected:如果一个成员是protected的,那么当前库中的任何一个类都可以访问它,而且在任何库中的这个类的子类也都可以访问它。
  • public:此成员可以从任何地方被访问。

取代C Struct

Point类是一个类似于C Struct 的Java类,这个类被叫作退化的类,因为没有提供数据的封装。

一个类似于C Struct的Java类源代码。

public class Point {public int x;public int y;}

这个类的设计是错误的,因为这个类没有提供给自己演化的空间。一个好的设计应该提供适当的访问方法,包括取值方法和赋值方法。

public class Point {private int x;private int y;public Point(int x, int y) {super();this.x = x;this.y = y;}public int getX() {return x;}public void setX(int x) {this.x = x;}public int getY() {return y;}public void setY(int y) {this.y = y;}}

2、广义的迪米特法则在代码层次上的体现

限制局域变量的有效范围

在需要一个变量的时候才声明它,可以有效地限制局域变量的有效范围。一个变量如果仅仅在块的内部使用的话,就应当将这个变量在程序块的内部使用它的地方声明,而不是放到块的外部或者块的开头声明。这样做的好处有两个:

(1)      程序可读性比较好。

(2)      如果一个变量是在需要它的程序块的外部声明的,那么当这个块还没有被执行时,这个变量就已经被分配了内存;而且在这个程序块已经执行完毕后,这个变量所占据的内存空间还没有释放,这显然是不好的。如果局域变量都是在马上就要使用的时候才声明,就可以避免这种情况。

0 0
原创粉丝点击