迪米特法则(Law of Demeter LoD)
Least Knowledge Principle
对其他对象有尽可能少的了了解
迪米特法则最初是用来作为面向对象的系统设计风格的一种法则,与1987年秋天由Ian Holland在美国东北大学为一个叫做迪米特(Demeter)的项目设计提出的,因此叫做迪米特法则[LIEB89][LIEB86].这条法则实际上是很多著名系统,比如火星登陆软件系统,木星的欧罗巴卫星轨道飞船的软件系统的指导设计原则.
没有任何一个其他的OO设计原则象迪米特法则这样有如此之多的表述方式,如下几种:
(1)只与你直接的朋友们通信(Only talk to your immediate friends)
(2)不要跟"陌生人"说话(Don't talk to strangers)
(3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些本单位密切相关的软件单位.
迪米特法则要求我们尽量减少类之间的直接交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果一个对象的某一个方法需要调用另一个对象的诸多方法,可以通过第三者转发这个调用,调用者对被调用者内部的情况知道的越少越好。
例01
小明在app上点外卖,他选择了一家披萨店,并向披萨店下了一份订单,披萨店接收了订单并开始为小明准备披萨,类图设计如下:
public class PizzaStore {
public void takeOrder() {
System.out.println("披萨店接收订单");
}
public void prepareSource() {
System.out.println("披萨店准备披萨制作原料");
}
public void cookPizza() {
System.out.println("披萨店烹制披萨");
}
public void packPizza() {
System.out.println("披萨店打包披萨");
}
public void deliveryPizza() {
System.out.println("披萨店配送披萨");
}
}
public class Customer {
public void orderPizza(PizzaStore pizzaStore){
pizzaStore.takeOrder();
pizzaStore.prepareSource();
pizzaStore.cookPizza();
pizzaStore.packPizza();
pizzaStore.deliveryPizza();
}
public static void main(String[] args){
Customer xiaoming = new Customer();
PizzaStore pizzaStore = new PizzaStore();
xiaoming.orderPizza(pizzaStore);
}
}
可以看到小明发起订单后调用披萨店的一个整个过程,包括接收订单、准备原材料、制作披萨、打包、配送等,这样写代码貌似没有问题,但是对于小明来说这些过程其实他根本就不需要关心,小明跟披萨店的交互其实很简单,就是我给你订单,你给我披萨就可以,可是现在小明知道了过多的披萨店的方法,这个就是违反迪米特法则的例子,我们按照迪米特法则进行修改,设计如下:
public class PizzaStore {
private void takeOrder() {
System.out.println("披萨店接收订单");
}
private void prepareSource() {
System.out.println("披萨店准备披萨制作原料");
}
private void cookPizza() {
System.out.println("披萨店烹制披萨");
}
private void packPizza() {
System.out.println("披萨店打包披萨");
}
private void deliveryPizza() {
System.out.println("披萨店配送披萨");
}
public void orderPizza() {
takeOrder();
prepareSource();
cookPizza();
packPizza();
deliveryPizza();
}
}
public class Customer {
public void orderPizza(PizzaStore pizzaStore){
pizzaStore.orderPizza();
}
public static void main(String[] args){
Customer xiaoming = new Customer();
PizzaStore pizzaStore = new PizzaStore();
xiaoming.orderPizza(pizzaStore);
}
}
执行结果跟前面一样,但是现在代码发生了一些变化,我们在PizzaStore类中新增了一个orderPizza()方法来封装所有的披萨定制流程,同时PizzaStore类中的其他方法都被声明为了private,对于小明来说他只需要调用PizzaStore的orderPizza()方法来预定披萨就可以了,不需要知道其内部的运转流程,并且现在实际上小明也无法调用PizzaStore的其他方法了,因为他看不到了。这就是迪米特法则所要求的尽量减少两个实体之间的交互。
这里我也可以使用另外一种方式来满足迪米特法则,那就是引入中间类,通过中间类来解耦调用者和被调用者,这里我们可以引入一个PizzaStoreProxy来解耦Customer和PizzaStore, 类图设计如下:
可以看到我们现在的Customer只跟PizzaStoreProxy进行依赖,而PizzaStoreProxy内部关联PizzaStore,相关代码:
public class PizzaStoreProxy {
private PizzaStore pizzaStore;
public PizzaStoreProxy(PizzaStore pizzaStore) {
this.pizzaStore = pizzaStore;
}
public void orderPizza() {
pizzaStore.takeOrder();
pizzaStore.prepareSource();
pizzaStore.cookPizza();
pizzaStore.packPizza();
pizzaStore.deliveryPizza();
}
}
public class Customer {
public void orderPizza(PizzaStoreProxy pizzaStoreProxy){
pizzaStoreProxy.orderPizza();
}
public static void main(String[] args){
Customer xiaoming = new Customer();
PizzaStore pizzaStore = new PizzaStore();
PizzaStoreProxy pizzaStoreProxy = new PizzaStoreProxy(pizzaStore);
xiaoming.orderPizza(pizzaStoreProxy);
}
}
可以看到通过引入一个代理类,小明现在只跟披萨店代理打交道,完全不关披萨店的事,也不知道披萨店的内部实现,实现了解耦。
这就是前面提到的结迪米特法则的第三个含义:知道的越少越好,知道的越多对你没有好处。
迪米特法则另外含义:只和直接的朋友交流(only talk to your immedate frineds)。这是什么意思呢,就是说一个对象在调用另一个对象的过程中,或者在依赖另一个对象的方法中,不应该出现被依赖对象以外的其他对象与依赖者产生交互影响。
例02
为了激励公司员工工作的积极性,老板准备给公司的员工发红包,当然发红包这种小事怎么可能劳驾老板亲自动手呢,于是老板找来部门负责人,让他来代为处理,类图设计如下:
public class Boss {
public void sendRedPacket(GroupLeader groupLeader) {
List<Employee> memberList = groupLeader.getMemberList();
for (Employee employee : memberList) {
groupLeader.giveRedPacket("100元大红包", employee);
}
}
}
public class GroupLeader {
private List<Employee> memberList;
public List<Employee> getMemberList() {
return memberList;
}
public void setMemberList(List<Employee> memberList) {
this.memberList = memberList;
}
public void giveRedPacket(String packet, Employee employee) {
employee.setRedPacket(packet);
}
}
public class Employee {
private String packet;
public void setRedPacket(String packet){
this.packet = packet;
}
}
修改后的代码中Boss类的sendRedPacket只跟groupLeader对象有关,移除了Employee对象的影响,降低了耦合性,而将发红包的逻辑移到了GroupLeader内部,现实当中也是如此Boss可能只需要给你传一句话即可,具体怎么执行Boss压根不关心,那是你要做的事。
好了,红包发完了,过了一段时间Boss想检查一下公司员工的工作积极性到底有没有提高,准备抽查一下出勤率,但是公司部门有几百号人抽查不过来,所以准备只抽查部门的某一个组的出勤情况。我们修改一下GroupLeader类并添加小组负责人对象:
public class GroupLeader {
private TeamLeader teamALeader;
private TeamLeader teamBLeader;
private TeamLeader teamCLeader;
public TeamLeader getTeamALeader() {
return teamALeader;
}
public void setTeamALeader(TeamLeader teamALeader) {
this.teamALeader = teamALeader;
}
public TeamLeader getTeamBLeader() {
return teamBLeader;
}
public void setTeamBLeader(TeamLeader teamBLeader) {
this.teamBLeader = teamBLeader;
}
public TeamLeader getTeamCLeader() {
return teamCLeader;
}
public void setTeamCLeader(TeamLeader teamCLeader) {
this.teamCLeader = teamCLeader;
}
}
我们的GroupLeader 现在并不直接管理员工了,GroupLeader的手下现在是TeamLeader,总共有三个组的组长分别负责teamA、teamB、teamC,并由三个TeamLeader分别管理各自组的员工。
好了,都闪开,Boss来点人了:
public class Boss {
public void count(GroupLeader groupLeader) {
//检查B组的人数
System.out.println(groupLeader.getTeamBLeader().getMemberList().size());
}
}
仔细想一下,这里有没有违法迪米特法则呢?
当然有违反,关键是在这一句代码groupLeader.getTeamBLeader().getMemberList().size(),这句代码实际上耦合了多个类,而迪米特告诉我们与直接的朋友交流,这里直接的朋友只有GroupLeader,其他都不是,然而这里的方法调用还返回了TeamLeader对象,这是迪米特法则不希望看到的,即你是我的朋友,但你的朋友不一定是我的朋友。我们应该怎么修改呢,很简单,在GroupLeader里面再封装一个方法:
public class GroupLeader {
.....
public int getTeamBMemberCount() {
return this.teamBLeader.getMemberCount();
}
}
同时在TeamLeader 中也添加统计数量的方法:
public class TeamLeader {
.....
public int getMemberCount() {
return memberList.size();
}
}
现在,Boss中的方法就只依赖一个对象了:
public class Boss {
public void count(GroupLeader groupLeader) {
//检查B组的人数
System.out.println(groupLeader.getTeamBMemberCount());
}
}
这个例子告诉我们应该尽量避免getA().getB()getC().getD()这种链式调用的的写法(返回同一个对象类型的除外),类与类的关系是建立在类之间的,不是方法间,一个方法尽量不要引入一个类中不存在的对象。
怎么才能很好的遵循迪米特法则表达的“与直接的朋友交流”呢,在一个对象的方法内,只调用属于以下范围的方法:
- 该对象本身
- 被当做方法参数传进来的对象
- 该方法内所创建或实例化的任何对象
- 该对象本身的任何成员变量对象
- 一个很好的遵循迪米特法则的示范代码:
public class Car {
Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start(Key key) {
Doors doors = new Doors();
boolean anthorized = key.turn(); //方法参数传入对象的方法
if (anthorized) {
engine.start(); //成员对象的方法
updateDisplay(); //对象本身的方法
doors.lock(); //本方法内实例的对象的方法
}
}
//更新显示
public void updateDisplay() {
}
}
迪米特法则体现的OO特征:封装。
迪米特法则的优点是可降低系统的耦合度,使类与类之间保持松耦合关系,松耦合的关系同时也提高了复用性。但也有缺点,有可能造成大量中转类或跳转次数的增加,增加系统的复杂性,跳转次数越多系统就越复杂从而越难以维护。所以在实际项目中要具体问题具体分析,在松耦合和可维护性之间做一个合理的取舍。