白话设计——深入了解LOD

从类关系说起一文中,我们谈到了几种类与类之间的关系,在此来深入一下对象与对象之间的通信问题.为什么要深入对象与对象之间的通信呢,其根本在于,系统中不会存在唯一的对象,不同的对象势必要相互进行交流.


初学者的问题

在开始介绍迪米特法则之前,先让我们会到初学编程的时代。

在我们刚开始学习编程的时候,通常会将所有的方法都声明为公有(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层以上的会使得系统过分的冗余和并切会委托层次过多而导致开发人员无法正确的理解流程,产生风险的可能会大大提高.

时间: 2024-08-08 11:36:37

白话设计——深入了解LOD的相关文章

引擎设计跟踪 地形LOD的改进

虽然地形已经有LOD和形变(morphing)来进行LOD的渐变,从而避免bump, 但是如果LOD级别过多,远处的高山就会严重丢失细节,比如变成尖尖的凸起,非常丑陋,这是morphing无法解决的. 解决一种方式是使用强制的固定LOD (fixed LOD),如果一个block的高度(maxH)或者高度比率(dH/dX)超过一定阈值,就使用固定的高细节的LOD. 例如LOD 0为最高细节,aab为block的包围盒,那么: 1 if(aab.getSize().y / aab.getSize(

白话设计——从类关系说起

温故知新,最近更多的是研究和开发各种类库,对设计的是有些感触.以前在大学的时候,虽然知道,但是总归是欠缺经验的,现在,我尝试用最通俗易懂的方式说出来. 所谓的设计不正是采用恰当的方式组织雷类关系么?因此谈设计我认为首先要从类之间的关系开始说起. 在java开发中,有很多时候我们是在不断的处理类与类之间关系,其中这六种关系是:依赖.关联.聚合.组合.继承.实现,他们的耦合度依次增强,其在UML的表示如下: 下面我们分别对这几种关系进行说明. 依赖 在实际生活中我们做任何一件事情几乎都需要借助其他物

白话设计——浅谈DIP和IOC

追本溯源,不断的回顾基础对我而言是种不错的方式,每次重新回顾这些点往往收获很大.以前,受个人所限,觉得这些理论毫指导价值价值,过于相信实践的的力量,导致自己进步缓慢.其实有些时候,实践更需要站在理论巨人的肩膀,这会让我们少走很多的弯路. 当然具体因人而异. 开发之困 实际开发中最常遇到的问题是类A直接依赖类B.当我们希望将类A修改为依赖类C时,就必须要通过修改类A来实现.这种 情况下类A作为高层的业务模块,负责复杂的业务模块,而类B和类C是底层模块,负责基本的原子操作.实际工程中类A作为业务模块

第2章 面向对象的设计原则(SOLID):5_迪米特法则

5. 迪米特法则(Law of Demeter,LoD) 5.1 定义 (1)应尽量减少其他对象之间的交互,对象只和自己的朋友交谈,即对其他依赖的类越少越好(不要和太多的类发生关系). (2)尽量不要让类和类之间建立直接的关系,这样可减少类与类之间的依赖,降低类之间的耦合. (3)一个类应对自己需要耦合的类知道得最少,只知道其public方法,其内部如何复杂自己没有关系,也叫最少知识原则(Least Knowledge Principle,LKP). 5.2 迪米特法则:核心要义就是类间解耦.低

设计模式—— 五:迪米特原则

目录 什么是迪米特原则? 迪米特法则的含义 1. 只和朋友交流 不遵循迪米特法则的定义 遵循迪米特法则的定义 2. 朋友间也是有距离的 不遵循迪米特原则的设计 遵循迪米特原则的设计 3. 是自己的就是自己的 4. 谨慎使用Serializable @ 什么是迪米特原则? 迪米特法则来自于1987年美国东北大学(Northeastern University)一个名为"Demeter"的研究项目.迪米特法则又称为最少知识原则(LeastKnowledge Principle, LKP),

学习设计模式 - 六大基本原则之迪米特法则

设计模式总共有六大基本原则,统称为SOLID (稳定)原则,分别是S-单一职责原则(Single Responsibility Principle), O-开闭原则(Open closed Principle),L-里氏替换原则(Liskov Substitution Principle),L-迪米特法则(Law of Demeter),I-接口隔离原则(Interface Segregation Principle),D-依赖倒置原则(Dependence Invension Principl

提升UI设计工作效率的4个技巧

如何提高UI 设计的速度?在这里分享一些我观察到的常见问题和改善方式.当然,需要注意的地方何其多,本文先着重聊一下观念和沟通的部分. 身为ui设计师的你,应该要?? 了解工程实作的基本原理 业界 NG 率:接近 100% 许多人认为创造力和逻辑是左右半脑分开管辖的.设计师和工程师使用的是不同部分的能力,所以大家各安其份做好自己的工作就好--我负责光鲜亮丽地画图,工程师就负责用超大声的同刻键盘在黑色的屏幕上编写外星文. 但事实上这种刻板印象除了阻碍你进步以外可说是一点用都没有(而且不见得是正确的)

面向对象设计原则

七大原则:开闭原则.里氏代换原则.依赖倒转原则.合成/聚合复用原则.迪米特法则.接口隔离原则,单一职责原则. 开闭原则是面向对象的可复用的基石.其他六种原则是手段和工具. 各规则详细(本部分为转载) http://kb.cnblogs.com/page/214010/ 正如牛顿三大定律在经典力学中的位置一样,“开-闭”原则(Open-Closed Principle)是面向对象的可复用设计(Object Oriented Design或OOD)的基石.其他设计原则(里氏代换原则.依赖倒转原则.合

引擎设计跟踪(九.14.2 final) Inverse Kinematics: CCD 在Blade中的应用

因为工作忙, 好久没有记笔记了, 但是有时候发现还得翻以前的笔记去看, 所以还是尽量记下来备忘. 关于IK, 读了一些paper, 觉得之前翻译的那篇, welman的paper (http://graphics.ucsd.edu/courses/cse169_w04/welman.pdf  摘译:http://www.cnblogs.com/crazii/p/4662199.html) 非常有用, 入门必读. 入门了以后就可以结合工程来拓展了. 先贴一下CCD里面一个关节的分析: 当Pic的方