这部分内容比较抽象,枯燥,无聊,但是却十分重要!希望大家能够对这块重视起来,他可以帮助你编写出高质量,优秀的面向对象代码!!
20世纪90年代期间,一些软件研究人员和从业人员在一起研究一种基于对象概念的新方法。先前的计算机变成方法将数据和过程分开,但是许多分析员发现,这种分离是不自然的。因为这和人类认识自然界事物的方式是不一致的,人类认识一个事物的通常过程是这样的:首先看到这个东西,然后会观察这个东西外部是什么样的,里面有什么,它能干什么。这里的外部和里面有什么属于事物的属性,能干什么是事物的行为特征,很多时候这两个部分是不可分离的,至少人类的思维模式是这样的。因此人们在长期的实践和研究中提出了基于对象的编程方法学,这就是我们现在广为使用的面向对象的编程思想。
面向对象的编程思想是比较契合人类认知思维的,为了使得所有人都能够使用这个原则去对一些事物进行抽象,面向对象提出了一系列的原则,这些原则和基本方法可以帮助我们更好地进行对事物的描述。
1. 四个特征
面向对象和面向过程有很大的不同的,它有以下4个基本特征。
抽象性
计算机软件科学就是一门高度抽象的学科,编程语言作为一个重要的工具,抽象性是所有编程语言的共性。面向对象的抽象性和面向过程的抽象性是不一样的,面向对象的抽象方式,是从整体上对一个对象的数据或者属性,行为进行的抽象。忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。比如,我们要设计一个学生成绩管理系统,考察学生这个对象时,我们只关心他的班级、学号、成绩等,而不用去关心他的身高、体重这些信息。抽象包括两个方面,一是过程抽象,二是数据抽象。过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值只能通过使用这些操作修改和观察。
继承性
这个特性是每个抽象对象类之间的纵向关系描述,它是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。这也体现了大自然中一般与特殊的关系。继承性很好的解决了软件的可重用性问题。比如说,所有的Windows应用程序都有一个窗口,它们可以看作都是从一个窗口类派生出来的。但是有的应用程序用于文字处理,有的应用程序用于绘图,这是由于派生出了不同的子类,各个子类添加了不同的特性。
封装性
这个特性和抽象性有一些关联,对一个事物的抽象就是需要忽略一些不重要的细节,这些细节的忽略基本就是通过封装性来完成。封装性是面向对象的特征之一,是对象和类概念的主要特性。封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,即哪些特性对外部世界是可见的,哪些特性用于表示内部状态。在这个阶段定义对象的接口。通常,应禁止直接访问一个对象的实际表示,而应通过操作接口访问对象,这称为信息隐藏。事实上,信息隐藏是用户对封装性的认识,封装则为信息隐藏提供支持。封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
多态性
多态性是指允许不同类的对象对同一消息作出响应。比如同样的加法,把两个时间加在一起和把两个整数加在一起肯定完全不同。又比如,同样的选择编辑-粘贴操作,在字处理程序和绘图程序中有不同的效果。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。多态性允许一个对象对相似的消息进行不同的响应,从而做出不同的处理。多态性有两种形式:横向多态和纵向多态。横向多态通常是指一个类内部多个方法接口之间的多态,通常这个特性的实现就是通过同样的函数名,返回值,但是参数不一样的方式实现的。纵向多态通常是指类之间的泛华关系,通常是通过类的继承和override覆写完成的。
2. 六个关系
依赖
关联
继承泛化)
实现
组合
聚合
这六个关系在下面的7个原则中会有描述,请详见下一小结。
3. 七个原则
基本是5+2原则,5个是SOLID, 2个是CL
5个SOLID:
S: 单一职责原则(Single Responsibility Principle)
O: 开闭原则(Open Close Principle)
L: 里氏替换原则(Liskov Substitution Principle)
I: 接口隔离原则(Interface Segregation Principle)
D: 依赖倒置原则(Dependence Inversion Principle)
2个CL:
合成/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
迪米特法则(Law Of Demeter)
在进一步说明以上的7个原则之前,我们应当知道:这些原则只是对面向对象设计原则在不同角度,不同层次,不同方式的抽象描述而已,他们之间关系是相互支撑,相互补充,相互影响的。请不要孤立地看待某个原则!
开闭原则(Open Close Principle)
就像牛顿的三大定律是经典物理学的奠基石一样,开闭原则也是面向对象设计的奠基石,开闭原则通常简写为OCP。
那么什么是开闭原则呢?开闭原则的含义是这样的:一个软件实体应该对扩展开放,对修改关闭。这个说法最早是由Bertrand Meyer提出的,原文的内容如下:
Software entities should be open for extension, but closed for modification.
这样的说法似乎比较抽象,用好理解的话来说就是:在设计一个软件模块的时候,应该是这个模块可以在不被修改的情况下可以灵活地扩展软件功能。这听起来好像比较矛盾,怎么可能既不修改代码,又要添加软件的新功能呢?但是实际上面向对象的思想和设计原则努力的目标就是这个,并且现在也有很多模式可以做到这一点。比如说,软件热插拔技术就是一个典型的例子,人们可以随时编写一个可插拔模块,然后以某种方式在不修改原来软件代码的情况下直接添加新的功能,甚至你都不用停止软件的运行!!
所有的软件系统都有一个共同的性质,那就是它们都会随着时间的流逝而不停地发生着各种变化,即唯一不变的就是不停地改变。在软件系统面临新的需求的时候,系统的设计必须是稳定而又灵活的。我们不可能一旦有新的需求提出的时候,我们就需要大规模地去修改我们的代码,这样牵一发而动全身的方式在软件开发中成本实在是太高了。在现实生活中,假如某人感冒发烧了,他去看医生,医生肯定是针对感冒发烧的症状开出处方,不可能医生让他去查一下心脏或者脚哪里有没有毛病的。但是在软件世界中,这样的事情经常会发生。一个模块需要修改,另外一个完全不相干的模块也需要进行修改,这是完全不可理喻的,但是这确实是每天都会发生的事情。为什么呢?原因就是你的软件设计有问题!你的软件和开闭原则相违背。满足开闭原则的设计可以使得我们的软件有着无可比拟的优越性:
1. 通过扩展已有的软件系统,可以以极地的代价实现新的功能,以满足对软件新的需求
2. 已有的软件模块,特别是最重要的抽象层模块,不能再修改,这就使得变化中的软件系统有一定的稳定性和延续性。
总的来说,考虑开闭原则会使得你的软件系统以不变应万变!
怎么才能做到符合开闭原则
抽象化是关键
解决这个问题的关键就是在于抽象。在想java这样的纯面向对象的编程语言中,可以给系统定义出一个一劳永逸,不再修改的抽象设计,此设计允许有无穷无尽的行为在实现层被实现。在java语言中,可以给出一个或者多个抽象的java类或者interface,这些抽象的表述表明了一个类别应该拥有的能力和特征签名。这个抽象遇见了所有可能的扩展,在任何情况下实现层可以任意添加自己特有的一些功能,这样的话就使得我们的抽象类,也就是既有的代码不用修改就能添加新的行为,这是对修改关闭。同时,由于我们在设计这些抽象类的时候,只是在高度抽象的层次上对业务逻辑进行描述,没有具体的描述在很多情况下就是通用的。也就是说,我们的抽象层允许任何形式的扩展,这也就是对扩展开放。
对可变性进行封装
开闭原则从另外一个角度描述可以是这样的:对可变性进行封装,这句话的意思是找到软件系统中可能会发生变化的部分,然后将他们封装起来,只是对外界提供抽象定义的,统一的,通用的交互方式。在面向对象思想经典圣经GOF中说道:考虑你的设计中什么可能发生变化。与通常将焦点放在找到什么会导致我们系统设计发生改变的思考方式正好相反,我们考虑的不是什么会导致设计改变,而是考虑允许什么发生变化而不会导致重新设计。对可变性进行封装意味着:
1. 一种可变性不应该散落在代码的很多角落里面,而应该被封装到一个对象里面,同一个可变性的不同表象意味着同一个继承等级结构中的具体子类。继承应该是封装变化的一种手段,而不是从一般的对象生成特殊对象的手段。
2. 一种可变性不应该与另外一种可变性混淆在一起。
很明显,对可变性进行封装的思想正是从工程的角度解释了如何实现开闭原则。
里氏替换原则(Liskov Substitution Principle)
从开闭原则中我们可以看出,面向对象的设计重要原则就是创建抽象层次,并且从抽象中导出具体描述。具体化可以给出不同的版本,每一个版本都给出不同的实现。从抽象到具体的过程,在编程语言中通常就是继承。针对继承关系我们就要使我们的设计符合里氏替换原则(LSP)。
那么什么是里氏替换原则呢?这个原则的表述比较抽象,如下:
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序在所有的对象o1换成o2的时候,程序的行为完全没有变化,那么就可以说T2类型是T1类型的子类型。
换个简单的说法就是,一个软件实体如果使用一个基类的话,那么一定可以使用其子类实现相同的功能,而且不会引起任何问题。举个例子,现在有一个Father类,他有一个子类Son,现在如果某个符合LSP的软件某块的接口是这样的:
methodFather obj)
那么,如果我们传递的参数是子类的对象时,软件任然会正常运行,不会出错。
LSP是面向对象继承复用的基石。只有当衍生类可以替换掉基类,并且软件单位的功能不会收到任何影响是,基类才能真正被复用,而衍生类也可以在基类的基础上添加新的行为。这里需要说明的是,上面的原则反过来则代换不成立,如果你使用一个父类的对象替换子类的对象话,那么可能会出错,因为子类很有可能在父类的基础上添加了新的方法,而软件模块恰好使用了这个方法,这个时候如果你代换的话,就会导致软件崩溃。
接下来,我们来看一下在Java语言中对LSP的支持,这个支持是在编译层次上的。现在假如我们有一个类Father,这个类有一个public方法叫做speak,然后我们新建一个叫做Son的类继承自Father类,Son类当然也会有speak方法。现在我们可不可以将这个speak方法访问权限变成private呢?答案是不可以,编译器会报错的!这就是java中的语法规则:
1. 子类不能将父类继承的方法权限缩小
从LSP角度来看待这个问题的话,那就是如果我们的子类可以将speak方法设置private的话,那么如果我们在一个软件系统中使用LSP代换父类对象的话,那么运行就出错了!因为子类的方法是private的,不能访问!这个情景就像父亲本来会说话,但是儿子不会说话了,现在儿子所在的场合又必须要说话!这就有问题了(虽然显示生活中,这样的事情可能发生),因此java编译器肯定不能让这样的代码编译通过。但是需要注意的是,java语言对LSP的支持粒度是比较大的,支持的范围是有限的。因为所有的支持都是在语法层次上的,并没有在实际的逻辑上进行支持,这里最著名的问题就要数西方人一直争论的问题:正方形是不是长方形的子类。
依赖倒置原则(Dependence Inversion Principle)
个人认为,依赖倒置原则是7个面向对象原则中最不好理解的的一个原则,这个原则描述的内容相对抽象。实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现。如果说开闭原则是面向对象的设计目标的话,那么依赖倒置原则就是这个设计的主要机制。
依赖倒置原则讲的内容就是:要依赖于抽象,而不是具体。
什么倒转,为什么要倒转?
理解DIP的关键就在于立即倒转这个概念,那么到底什么是倒转呢?简单来说,传统的过程性系统的设计方法倾向于是高层次的模块依赖于低层次的模块实现;抽象的层次依赖于具体的层次。举个例子,假如现在你有一家公司,这家公司具体是做什么行业的以及怎么管理等关键问题需要依赖于你这家公司具体主营的业务以及公司的组织结构和盈利模式等。这就是上层依赖底层,抽象依赖具体。咋一看这么思考问题确实没有什么问题,但是如果我们仔细想一下这个问题就会得出不一样的结论。这个问题关键就在于我们的关注点在什么地方,如果我们的关注点在底层,在具体的问题上那么我们会认为上层依赖下层是合理的;但是如果我们关注上层的抽象的业务逻辑的话,那么我们会认为具体的业务实践应该以我们最高的商业规则为指导,在抽象层的规范下运作。这两种思考模式本身没有什么问题,但是在处理具体的问题时就不一样了。我们知道现实生活中,事物的变化是每时每刻都在进行的,如果我们一直关注具体的细节问题的话,那么我们会非常劳累,陷入各种具体繁杂的事务中去。这就好像,你让某人帮你买瓶饮料,如果你的只是想喝点东西解渴的话,你根本就没必要告诉某人说你要喝什么牌子的什么型号的饮料。你并不会依赖于他给你买的是什么饮料,而他则会依赖于你的要求,因为你要求的是饮料,他决不能给你买个面包。
抽象的层次含有宏观的和重要的商务逻辑,这应该决定具体的层次实现的。抽象层依赖于具体层次显然是不合理的,依赖倒转原则(DIP)就是要把错误的依赖关系再倒转过来。
依赖关系的种类
DIP将是依赖关系的倒转,因此我们需要弄明白什么是依赖关系,都有哪些关系。通俗来讲,依赖就是两个实体之间有关联,一个实体的正常功能发挥需要另外一个实体的协助,比如电脑的运行需要供电设备的协助,那么这两个实体之间就存在依赖关系。那么依赖表明两个实体之间有关联,在软件设计上这个关系称为耦合,因此从狭义上将依赖关系在软件设计上我们可以理解为耦合关系。耦合有很多中,但是整体分为以下3类:
1. 零耦合关系:如果两个类之间没有任何关系,就称为零耦合
2. 具体耦合关系:具体性耦合关系发生在两个具体的类之间,也就是说这两个类本身没有依赖关系,但是实例化之后,这两个类的实例化对象之间可能存在耦合关系。举个例子吧,现在有两个类:人类和凳子类,这两个类之间没有任何逻辑上的依赖关系;但是如果我们把它们具体化之后:有一个特定的人叫张三,有一个特定的凳子是木凳,现在张三的某种行为可能需要这个木凳的存在(比如他在休息),因此他们之间的关系叫做具体耦合关系。
3. 抽象耦合关系:抽象耦合关系发生在一个具体的类和一个抽象类或者接口之间,使两个必须发生关系的类之间存在最大的灵活性。
依赖倒转原则的表述
现在我们可以来说明一下到底什么是依赖倒转原则。简单来说,依赖倒转原则(DIP)要求客户端依赖于抽象的耦合,DIP的表述如下:
抽象不应该依赖于细节,细节应该依赖于抽象。英文原文表达:
Abstractions should not depend upon details. Details should depend upon abstractions.
DIP的另一种表述如下:
要针对接口编程,不要针对实现编程。针对接口编程的含义就是,应该使用java的interface和抽象类来进行类的变量声明,方法定义。不要针对实现编程的意思就是说,不应该使用具体的java进行变量的声明,方法的定义等。要保证做到这一点,一个具体的java类应该只实现java接口和抽象类中声明过的方法,不应该给出多余的方法。倒转依赖关系强调一个系统内部的实体之间关系的灵活性。
怎么做到DIP
根据DIP的表述,我们知道将类之间的关系以抽象的方式耦合是实施的关键。由于一个抽象耦合关系总是要涉及从抽象类继承,并且需要保证再任何引用到基类的地方子类都能进行代换,因此我们上面将的LSP是实现依赖倒转的基础。在抽象层次上进行耦合虽然有灵活性,但是也带来了额外的复杂性。在某些情况下,如果一个具体类发生变化的可能性很小,那么抽象耦合能发挥的好处便十分有限,这是使用具体的耦合关系反而会更好,是的代码更加清晰易懂。DIP是面向对象设计的核心原则,java的很多设计模式都是以DIP为指导进行设计的。
接口隔离原则(Interface Segregation Principle)
接口隔离原则(ISP)讲的是:使用多个专门的接口总比使用单一的复杂接口要好。换句话说,从一个客户类的角度来讲,一个类对另一个类的依赖应该是建立在最小的接口上的。这里有一点需要说明,我们说的接口不是单单指java中的interface,而是两个软件实体之间的一种关于协作的契约,而java的interface只是这种契约在java语言上的一种实现而已。我们更多的描述是在于逻辑上的抽象。
划分我们的角色
如果将接口理解成一个类所提供的所有方法的特征集合也就是一种在逻辑上才存在的概念的话,我们可以理解实施ISP的关键就是在于接口角色类型的划分。一个接口就相当于剧本中角色,而这个角色在舞台上由哪个演员来实现就相当于接口的实现。因此,一个接口就应该简单地代表一个角色,而不是多个角色。如果系统涉及多个角色的话,那么每一个角色应该有一个独立的接口代表。我们需要根据我们实际的业务需求,将我们的接口角色进行合理的划分,尽量避免繁杂臃肿的接口实现。
定制服务
如果我们将接口就简单地理解成java的interface的话,那么我们可以这么认为:ISP讲的就是为同一个角色提供不同宽度的接口,以对应不同的客户端。在现实生活中,这种方式经常见到,比如我们去银行办理业务的时候,比如两个客户都是存钱,客户A要存10W,客户B要存1000W,那银行提供的服务肯定是不一样的(客户B的服务肯定是VIP啊~~~),这就叫做服务定制,或者特殊服务。
不要污染现有的接口
任何使得现有接口变得臃肿的行为都是对接口的污染!我们在任何情况下都不能将一个和现有接口业务逻辑不一致的内容添加到接口中去,一个接口所代表的角色应该就是唯一的,简单的,明了的。有些时候,我们为了省事,将多个接口合并到一个接口中去,美其名曰:代码优化整合!其实就是对接口的污染,其结果只能是使得我们的软件系统中由于出现了畸形的代码而变得的丑陋不堪且难以维护。
纯粹的接口,纯粹的行为
一个接口应该尽量简单而纯粹,不要有任何多余的内容。总之一句话:不要向客户端提供任何它不需要的方法!!
合成/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
合成/聚合复用原则(CARP)讲的内容就是在一个新的对象里面使用一些已经存在的对象,使之成为新对象的一部分,新的对象通过委派达到复用已有功能的目的。这个原则还有一个比较简单的描述:要尽量使用合成/聚合,而不要使用继承实现。合成和聚合的概念是如此地相近,以至于我们一直以来都弄不清他们之间的区别,下面我们就看一看究竟什么是合成,什么是聚合。
合成和聚合的含义
从广义角度来说,合成(Composition)和聚合(Aggregation)都是关联(Association)的特殊种类。同时,合成和聚合都有“拥有”的含义。但是需要注意的是,合成和聚合拥有的程度是不一样的。合成是一种意味比较强烈的拥有关系,也就是说一个实体是属于另外一个实体不可或缺的一部分,部分脱离整体存在没有意义,整体脱离部分则不是一个整体(有点类似于,台湾和中国的关系)。而聚合的拥有意味就比较弱了,一个实体在某种情况下需要另外一个实体的存在,他们在一起相互作用形成一个整体。举个例子,我们要描述“一个坐在凳子上的人”这个事物的时候,人和凳子就是聚合的含义,人离开了这个凳子照样可以存在,因为他还可以坐在别的凳子上;凳子离开了人同样可以单独存在,因为别的人可以坐在这个凳子上。说白了,就是他们只是在某些场景下协作形成一个整体而已,在逻辑上并没有太多的关联。
CARP复用的种类
CARP讲的说就是面向对象设计里面的复用问题,只是它强调合成/聚合复用,而不是继承复用。下面我们比较说明一下合成/聚合复用和继承复用的方式。
合成/聚合复用
由于合成或者聚合可以将已有的对象纳入到一个新的对象中,并使之成为新对象的一部分。因此这里新的对象就是在复用已有对象的功能,这样做有以下好处:
1. 新的对象操作成分对象的唯一方法就是通过该成分对象的接口
2. 这种复用属于黑箱复用,即新类不必关心已有类的内部实现只是关系已有类的对外接口,这和DIP讲的内容是一致的。
3. 这种复用支持包装
4. 这种复用所需的依赖较少
5. 每一个新的类可以将焦点集中放在一个任务上
6. 这种复用可以运行时进行,比较灵活
一般而言,如果一个角色得到了更多的责任,那么他可以使用合成/聚合关系将新的责任委派给已有的合适对象。这就像人们之间的合作,你不可能任何事情都是亲力亲为,有些事情你完成不了的你就必须要委派给被人来帮你完成。(比如你没有对象,你需要委派婚恋机构帮你找到你的那个他/她,如果你还是一直自己寻找的话,那真的就要当一辈子的光棍了~~)。当然这种方式也是有一定的缺点的,主要就是再这种方式中新的对象会有很多委派的对象要管理(比如说你要和婚恋机构保持联系,有的时候他们真的很烦,总是各种电话骚扰……)。
继承复用
合成/聚合复用作为复用手段,可以应用到几乎任何环境中去。而与此不同的是,继承只能应用到很有限的环境中去。换言之,尽管继承是一种重要的复用手段,但是你还是要首先考虑合成/聚合,而不是继承。
整体来说,继承的种类分为两种:实现继承和抽象继承。实现继承是说从一个实现类继承得到子类,比如从“张三”继承得到“小张三”,抽象继承是说从一个抽象的类继承或者一个java interface继承实现你的新类,比如从人继承得到张三。通常而言,在实践中应该尽量使用抽象继承而不是实现继承。继承复用也是有优点的:
1. 新的实现比较容易,因为超类的大部分功能可以通过继承关系自动进入子类
2. 修改或扩展继承而来的实现较为容易
但是继承复用也是有缺点的:
1. 继承复用破坏包装,因为继承复用将超类的实现细节暴露给了子类。由于超类的内部细节对于子类是透明的,因此这种复用是透明的复用,也称“白箱复用”。
2. 如果超类的实现改变时,那么子类的实现也会不得不改变。因此当一个基类发生改变的时候,这种改变就会延续传递给每一个子类,在某些情况我们可能需要重新设计我们的子类。其根本原因就是基类和子类之间的耦合关系太过于强烈所致。
3. 从基类继承而来的实现是静态的,不能在运行时发生改变,因此没有足够的灵活性。
由于上述的种种特点,我们应该尽量使用合成/聚合复用,而不是继承复用,这是一个重要的面向对象设计原则。从实体之间的关系上讲,合成/聚合关系更像是一种“has a”关系,而继承则是“is a”的关系,这种关系更加密切,因此如果一旦需要发生改变,那么影响的范围是比较广泛的。
在现实生活中,合成/聚合模式和继承模式,更像是一个人混社会的方式。合成/聚合模式是我们普通人的模式,我们有什么自己搞不定的困难了,各种依赖朋友帮忙解决,家里面基本不能帮我们解决;而继承模式则是高富帅们的方式,遇到任何问题,他们都能搞定,因为家里的权势实在是太大,他一生下来就从家族里面继承来了众多宝贵的社会资源!!所以说,对于大部分人而言合成/聚合模式比较靠谱,如果你是高富帅那继承模式就OK!
迪米特法则(Law Of Demeter)
迪米特法则(Law Of Demeter)又叫最少知识原则,就是说一个对象应该对其他的对象拥有尽可能少的了解。LOD其实是降低对象之间的耦合性的一种有效手段,它的表述比较多,通常有下面几个:
1. 只与你直接的朋友通讯
2. 不要和“陌生人”讲话
3. 每一个软件单位对其他单位都只有最少的知识,而且局限于与本单位联系密切的单位。
要注意这里表述中的“直接”,“陌生人”,“密切”等关键字的表述是比较模糊的,没有统一定性的描述,这就使得LOD在不同的环境下有不同的解释。如果两个类之间不必直接通讯,那么这两个类就不应该直接发生相互作用。如果其中的一个类需要调用另外一个类的某一个方法的话,应该通过第三者转发实现。
朋友圈和陌生人
这是LOD中重要的两个概念,首先我们需要明确什么是朋友圈,朋友圈就是由某个对象的“朋友”组成的范围。那么什么是朋友呢?符合以下条件的就可以称作是朋友:
1. 当前对象本身
2. 以参数的形式传入到当前对象方法中的对象
3. 当前对象的实例变量直接引用的对象
4. 当前对象的实例变量如果是一个聚集(比如java中的list),那么聚集中的元素也是朋友
5. 当前对象创建的所有对象
除此之外的对象都是陌生人,我们的对象不应该和他们直接通讯。
单一职责原则(Single Responsibility Principle)
这个原则讲的事情比较简单:一个优良的系统设计,强调模块间保持低耦合、高内聚的关系,在面向对象设计中这条规则同样适用,所以面向对象的第一个设计原则就是单一职责原则。单一职责,强调的是职责的分离,在某种程度上对职责的理解,构成了不同类之间耦合关系的设计关键,因此单一职责原则或多或少成为设计过程中一个必须考虑的基础性原则。说白了就是,你的对象的职责应该足够简单,精简,以至于和他任何逻辑上不相关的职责的事情他都不会去做!