OO设计原则
在软件软件系统中,一个模块设计得好不好的最主要、最重要的标志,就是该模块在多大程度上将自己的内部数据和其他与实现有关的细节隐藏起来。一个设计得好的模块可以将它所有的实现细节隐藏起来,彻底地将提供给外界的API和自己的实现分隔开来。这样一来,模块与模块之间就可以仅仅通过彼此的API相互通信,而不理会模块内部的工作细节。
OO设计根本的指导原则是提高可维护性和可复用性。这些原则主要有:
<b>1. </b><b>开闭原则</b>
一个软件实体应该对扩展开放,对修改关闭。
在设计一个模块的时候,就当使这个模块可以在不被修改的前提下被扩展。换言之,就当可以在不必修改源代码的情况下改变这个模块的行为。
如何做到既不修改,又可以扩展?
解决问题的关键在于抽象化:在Java语言里,可以给出一个或多个抽象Java类或Java接口,规定出所有的具体类必须提供的方法特征作为系统设计的抽象层。这个抽象层预见了所有的可能扩展,因此,在任何扩展情况下都不会改变。这就使得系统的抽象层不需要修改,从而满足了—对修改关闭。
同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的。
开闭原则实际上是对“对可变性的封闭原则“:找到一个系统的可变因素,将之封装起来。这个原则意昧着两点:
1) 一个可变性不应当散落在代码的很多角落里,而应当被封装到一个对象里面。同一种可变性的不同表象意昧着同一个继承等级结构中的具体子类。
继承就当被看作是封装变化的方法,而不应当被认为是从一般的对象生成特殊对象的方法。
2) 一种可变性不应当与另一种可变性混合在一起。(所有类图的继承结构一般不会超过两层,不然就意昧着将两种不同的可变性混合在了一起。)
开闭原则是总的原则,其它几条是开闭原则的手段和工具。
<b>2. </b><b>依赖倒转原则</b>
依赖倒转原则讲的是:要依赖于抽象,不要信赖于实现。
开闭原则是目标,而达到这一目标的手段是依赖倒转原则。
抽象层次包含的是应用系统的商务逻辑和宏观的、对整个系统来说重要的战略性决定,是必然性的体现;而具体层次则含有一些次要的与实现有关的算法和逻辑,以及战术性的决定,带有相当大的偶然性选择。具体层次的代码是会经常有变动的,不能避免出现错误。
抽象层次含有一个应用系统最重要的宏观商务逻辑,是做战略判断和决定的地方,那么抽象层次就应当是较为稳定的,应当是复用的重点;也应当是维护的重点。
在很多情况下,一个Java程序需要引用一个对象。这个时候,如果这个对象有一个抽象类型的话,应当使用这个抽象类型作为变量的静态类型。这就是针对接口编程的含义。
一般而言,在创建一个对象时,Java语言要求使用new关键词以及这个类本身。而一旦这个对象已经被创建出来,那么就可以灵活地使用这个对象的抽象类型来引用它。比如:List employees = new Vector();因此,Java语言中创建一个对象的过程是违背“开闭原则”以及依赖倒转原则的(因为先生成了具体的类型,再使用抽象的引用),虽然在这个类被创建出来以后,可以通过多态性使得客户端依赖于其抽象类型。正是由于这个问题,设计模式给出了多个创建模式,特别是几个工厂模式,用于解决对象创建过程中的依赖倒转问题。
工厂模式将创建一个类的实例的过程封装起来,消费这个实例的客户端仅仅取得实例化的结果,以及这个实例的抽象类型。当然,任何方法都无法回避Java语言所要求的new关键字和直接调用具体类的构造子的做法(这违背了里氏代换原则)。简单工厂模式将这个违反“开闭原则”和依赖倒转原则的做法封装到了一个类里面,而工厂方法模式将这个违反原则的做法推迟到了具体工厂角色中。通过适当的封装,工厂模式可以净化大部分的结构,而将违反原则的做法孤立到易于控制的地方。
联合使用Java接口和Java抽象类:声明类型的工作由Java接口承担,但是同时给出的还有一个Java抽象类,为这个接口给出一个缺省实现。如果一个具体类直接实现这个Java接口的话,它就必须自行实现所有的接口;相反,如果它继承自抽象类的话,它就可以省去一些不必要的方法,因为它可以从抽象类中自动得到这些方法的缺省实现。这其实就是缺省适配模式。
<b>依赖倒转的缺点:</b>
1) 因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以避免对具体类的直接引用,此原则的使用还会导致大量的类。对不熟悉面向对象技术的工程师来说,维护这样的系统需要较好地面向对象的设计知识。
2) 依赖倒转原则假定所有的具体类都是会变化的,这也不总是正确的。有一些具体类可能相当稳定、不会发生变化,消费这个具体类实例的客户端完全可以依赖这个具体类型,而不必为此发明一个抽象类型。
<b>3. </b><b>里氏代换原则</b>
任何基类可以出现的地方,子类一定可以出现。
开闭原则的关键步骤是抽象化。而基类与子类的继承关系就是抽象化的具体体现,里氏代换原则是对实现抽象化的具体步骤的规范。
<b>4. </b><b>合成/聚合复用原则</b>
要尽量使用合成/聚合,而不是继承关系达到复用的目的。
合成/聚合原则要求我们首先考虑合成/聚合关系,里氏代换原则要求在使用继承时,必须确定这个继承关系符合一定的条件(继承是用来封装变化的;任何基类可以出现的地方,子类一定可以出现。)
合成/聚合原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到得复用已有功能的目的。
<b>5. </b><b>迪米特原则</b>
一个软件实体应当尽可能少的其他实体发生相互作用。模块之间的交互要少。这样做的结果是当系统的功能需要扩展时,会相对更容易地做到对修改的关闭。
一个对象应当对其他对象有尽可能少的了解。
<b>迪米特原则的具体操作:</b>
1) 优先考虑将一个类设置成不变类。不变类易于设计、实现和使用。比如Java API中的String,BigInteger等类。
一个对象与外界的通信大体上分成两种,一种是改变这个对象的状态,另一种是不改变这个对象的状态的。如果一个对象的内部状态根本就是不可能改变的,那么它与外界的通信当然就大大地减少。
当涉及任何一个类的时候,都首先考虑这个类的状态是否需要改变。即便一个类必须是可变类,在给它的属性设置赋值方法的时候,也要保持吝啬的态度。除非真的需要,否则不要为一个属性设置赋值方法。
2) 尽量降低一个类的访问权限。
3) 谨慎使用Serializable,一旦将一个类设置成Serializable,就不能再在新版本中修改这个类的内部结构,包括private的方法和句段。
4) 尽量降低成员的访问权限。
<b>6. </b><b>接口隔离原则</b>
应当为客户端提供尽可能小的单独接口,而不要提供大的总接口。也即是使用多个专门的接口比使用单一的总接口要好。
接口隔离原则与迪米特都是对一个软件实体与其他的软件实体的通信限制。迪米特原则要求尽可能地限制通信的宽度和深度,接品隔离原则要求通信的宽度尽可能地窄。这样做的结果使一个软件系统在功能扩展过程当中,不会将修改的压力传递到其他对象。
一个接口相当于剧本中的一种角色,而此角色在一个舞台上由哪一个演员来演则相当于接口的实现。因此,一个接口应当简单地代表一个角色,而不是多个角色。如果系统涉及到多个角色的话,那么每一个角色都应当由一个特定的接口代表。
定制服务:如果客户端仅仅需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。(向客户端提供public接口是一种承诺,没有必要做出不必要的承诺,过多的承诺会给系统的维护造成不必要的负担。)
设计原则是基本的工具,应用这些规则可使代码更加灵活、更容易维护,更容易扩展。基本原则:封装变化 Encapsulate what varies. 面向接口变成而不是实现 Code to an interface rather than to an implementation. 优先使用组合而非继承 Favor Composition Over Inheritan
什么是设计原则?
设计原则是基本的工具,应用这些规则能够使您的代码更加灵活、更容易维护,更容易扩展。
基本原则
封装变化Encapsulate what varies.
面向接口变成而不是实现 Code to an interface rather than to an implementation.
优先使用组合而非继承 Favor Composition Over Inheritance
SRP: The single responsibility principle 单一职责
系统中的每一个对象都应该只有一个单独的职责,而任何对象所关注的就是自身职责的完成。
Every object in your system should have a single responsibility ,and all the object s services should be focused on carrying out that single responsibility .
每一个职责都是个设计的变因,需求变化的时候,需求变化反映为类职责的变化。当您系统里面的对象都只有一个变化的原因的时候,您就已很好的遵循了SRP原则。
假如一个类承担的职责过多,就等于把这些职责耦合在了一起。一个职责的变化就可能削弱或抑制这个类其他职责的能力。这种设计会导致脆弱的设计。当变化发生的时候,设计会遭到意想不到的破坏。
SRP 让这个系统更容易管理维护,因为不是任何的问题都搅在一起。
内聚Cohesion 其实是SRP原则的另外一个名字.您写了高内聚的软件其实就是说您很好的应用了SRP原则。
怎么判断一个职责是不是个对象的呢?您试着让这个对象自己来完成这个职责,比如:“书自己阅读内容”,阅读的职责显然不是书自己的。
仅当变化发生时,变化的轴线才具备实际的意义,假如没有征兆,那么应用SRP或任何其他的原则都是不明智的。
DRY : Don't repeat yourself Principle
通过抽取公共部分放置在一个地方避免代码重复.
Avoid duplicate code by abstracting out things that are common and placing those thing in a single location .
DRY 很简单,但却是确保我们代码容易维护和复用的关键。
您尽力避免重复代码候实际上在做一件什么事情呢?是在确保每一个需求和功能在您的系统中只实现一次,否则就存在浪费!系统用例不存在交集,所以我们的代码更不应该重复,从这个角度看DRY可就不只是在说代码了。
DRY 关注的是系统内的信息和行为都放在一个单一的,明显的位置。就像您能够猜到正则表达式在.net中的位置相同,因为合理所以能够猜到。
DRY 原则:怎样对系统职能进行良好的分割!职责清楚的界限一定程度上确保了代码的单一性。
OCP : Open-Close Principle开闭原则
类应该对修改关闭,对扩展打开;
Classes should be open for extension ,and closed for modification .
OCP 关注的是灵活性,改变是通过增加代码进行的,而不是改变现有的代码;
OCP的应用限定在可能会发生的变化上,通过创建抽象来隔离以后发生的同类变化
OCP原则传递出来这样一个思想:一旦您写出来了能够工作的代码,就要努力确保这段代码一直能够工作。这能够说是个底线。稍微提高一点需要,一旦我们的代码质量到了一个水平,我们要尽最大努力确保代码质量不回退。这样的需要使我们面对一个问题的时候不会使用凑活的方法来解决,或说是放任自流的方式来解决一个问题;比如代码添加了无数对特定数据的处理,特化的代码越来越多,代码意图开始含混不清,开始退化。
OCP 背后的机制:封装和抽象;封闭是建立在抽象基础上的,使用抽象获得显示的封闭;继承是OCP最简单的例子。除了子类化和方法重载我们更有一些更优雅的方法来实现比如组合;
怎样在不改变源代码(关闭修改)的情况下更改他的行为呢?答案就是抽象,OCP背后的机制就是抽象和多态
没有一个能够适应任何情况的贴切的模型!一定会有变化,不可能完全封闭.对程式中的每一个部分都肆意的抽象不是个好主意,正确的做法是研发人员仅仅对频繁变化的部分做出抽象。拒绝不成熟的抽象和抽象本身相同重要。
OCP是OOD很多说法的核心,假如这个原则有效应用,我们就能够获更强的可维护性 可重用 灵活性 健壮性 LSP是OCP成为可能的主要原则之一
LSP: The Liskov substitution principle
子类必须能够替换基类。
Subtypes must be substitutable for their base types.
LSP关注的是怎样良好的使用继承.
必须要清楚是使用一个Method还是要扩展他,但是绝对不是改变他。
LSP清楚的指出,OOD的IS-A关系是就行为方式而言,行为方式是能够进行合理假设的,是客户程式所依赖的。
LSP让我们得出一个重要的结论:一个模型假如孤立的看,并不具备真正意义的有效性。模型的有效性只能通过他的客户程式来表现。必须根据设计的使用者做出的合理假设来审视他。而假设是难以预测的,直到设计臭味出现的时候才处理他们。
对于LSP的违反也潜在的违反了OCP
DIP:依赖倒置原则
高层模块不应该依赖于底层模块 二者都应该依赖于抽象
抽象不应该依赖于细节 细节应该依赖于抽象
什么是高层模块?高层模块包含了应用程式中重要的策略选择和业务模型。这些高层模块使其所在的应用程式区分于其他。
假如高层模块依赖于底层模块,那么在不同的上下文中重用高层模块就会变得十分困难。然而,假如高层模块单独于底层模块,那么高层模块就能够很容易的被重用。该原则就是框架设计的核心原则。
这里的倒置不但仅是依赖关系的倒置也是接口任何权的倒置。应用了DIP我们会发现往往是客户拥有抽象的接口,而服务者从这些抽象接口派生。
这就是着名的Hollywood原则:"Don't call us we'll call you."底层模块实现了在高层模块声明并被高层模块调用的接口。
通过倒置我们创建了更灵活 更持久更容易改变的结构
DIP的简单的启发规则:依赖于抽象;这是个简单的陈述,该规则建议不应该依赖于具体的类,也就是说程式汇总任何的依赖都应该种植于抽象类或接口。
假如一个类很稳定,那么依赖于他不会造成伤害。然而我们自己的具体类大多是不稳定的,通过把他们隐藏在抽象接口后面能够隔离不稳定性。
依赖倒置能够应用于任何存在一个类向另一个类发送消息的地方
依赖倒置原则是实现许多面向对象技术多宣称的好处的基本底层机制,是面向对象的标志所在。
ISP:接口隔离原则
不应该强迫客户程式依赖他们无需的使用的方法。
接口不是高内聚的,一个接口能够分成N组方法,那么这个接口就需要使用ISP处理一下。
接口的划分是由使用他的客户程式决定的,客户程式是分离的接口也应该是分离的。
一个接口中包含太多行为时候,导致他们的客户程式之间产生不正常的依赖关系,我们要做的就是分离接口,实现解耦。
应用了ISP之后,客户程式看到的是多个内聚的接口。