我们在应用程序开发中,一般要求尽量两做到可维护性和可复用性。
应用程序的复用可以提高应用程序的开发效率和质量,节约开发成本,恰当的复用还可以改善系统的可维护性。而在面向对象的设计里面,可维护性复用都是以面向对象设计原则为基础的,这些设计原则首先都是复用的原则,遵循这些设计原则可以有效地提高系统的复用性,同时提高系统的可维护性。 面向对象设计原则和设计模式也是对系统进行合理重构的指导方针。
常用的面向对象设计原则包括7个,这些原则并不是孤立存在的,它们相互依赖,相互补充。
前5个原则组合称为:SOLID 固定原则
1.单一职责原则
类的职责要单一,不能将太多的职责放在一个类中。(高内聚、低耦合)
定义:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。(Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class.),即又定义有且仅有一个原因使类变更。
2)类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。
3)单一职责原则是 实现高内聚、低耦合的 指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
1、降低类的复杂性,类的职责清晰明确。比如数据职责和行为职责清晰明确。
2、提高类的可读性和维护性,
4、变更引起的风险减低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否合理,但是“职责”和“变化原因”都是没有具体标准的,一个类到底要负责那些职责?这些职责怎么细化?细化后是否都要有一个接口或类?这些都需从实际的情况考虑。因项目而异,因环境而异。
2.开闭原则( Open - ClosedPrinciple ,OCP )
对扩展开放,对修改关闭(设计模式的核心原则是)
定义:一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭. 意思是,在一个系统或者模块中,对于扩展是开放的,对于修改是关闭的,一个 好的系统是在不修改源代码的情况下,可以扩展你的功能. 而实现开闭原则的关键就是抽象化.
原则分析 :
1)当软件实体因需求要变化时, 尽量通过扩展已有软件实体,可以提供新的行为,以满足对软件的新的需求,而不是修改已有的代码,使变化中的软件有一定的适应性和灵活性 。已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。
2)实现开闭原则的关键就是抽象化 :在"开-闭"原则中,不允许修改的是抽象的类或者接口,允许扩展的是具体的实现类,抽象类和接口在"开-闭"原则中扮演着极其重要的角色..即要预知可能变化的需求.又预见所有可能已知的扩展..所以在这里"抽象化"是关键!
3)可变性的封闭原则:找到系统的可变因素,将它封装起来. 这是对"开-闭"原则最好的实现. 不要把你的可变因素放在多个类中,或者散落在程序的各个角落. 你应该将可变的因素,封套起来..并且切忌不要把所用的可变因素封套在一起. 最好的解决办法是,分块封套你的可变因素!避免超大类,超长类,超长方法的出现!!给你的程序增加艺术气息,将程序艺术化是我们的目标!
例子:我们前面提到的模板方法模式和观察者模式都是开闭原则的极好体现。
3.里氏代换原则( Liskov Substitution Principle ,LSP )
任何基类可以出现的地方,子类也可以出现
定义:第一种定义方式相对严格:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
-
/**
-
* 长方形类Rectangle:
-
*
-
*/
-
class Rectangle {
-
double length;
-
double width;
-
public double getLength() {
return length; }
-
public void setLength(double height) {
this.length = length; }
-
public double getWidth() {
return width; }
-
public void setWidth(double width) {
this.width = width; }
-
}
正方形集成长方形:
-
/**
-
* 正方形类Square:
-
*/
-
class Square extends Rectangle {
-
public void setWidth(double width) {
-
super.setLength(width);
-
super.setWidth(width);
-
}
-
public void setLength(double length) {
-
super.setLength(length);
-
super.setWidth(length);
-
}
-
}
由于正方形的度和宽度必须相等,所以在方法setLength和setWidth中,对长度和宽度赋值相同。类TestRectangle是我们的软件系统中的一个组件,它有一个resize方法要用到基类Rectangle,resize方法的功能是模拟长方形宽度逐步增长的效果 :
测试类TestRectangle:
-
class TestRectangle {
-
public void resize(Rectangle objRect) {
-
while(objRect.getWidth() <= objRect.getLength() ) {
-
objRect.setWidth( objRect.getWidth () +
1 );
-
}
-
}
-
}
我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。
我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。
3)子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
4)提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
5)提高产品或项目的开放性。
1)继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
2)降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
3)增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构
4.接口隔离原则(Interface Segregation Principle,ISL):
客户端不应该依赖那些它不需要的接口。(这个法则与迪米特法则是相通的)
定义:
客户端不应该依赖那些它不需要的接口。
例子:
重构后:
5.依赖倒转原则( Dependence Inversion Principle ,DIP ):
要依赖抽象,而不要依赖具体的实现.
定义:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。简单的说,依赖倒置原则要求客户端依赖于抽象耦合。原则表述:
1)抽象不应当依赖于细节;细节应当依赖于抽象;
2)要针对接口编程,不针对实现编程。
原则分析:
1)如果说开闭原则是面向对象设计的目标,依赖倒转原则是到达面向设计"开闭"原则的手段..如果要达到最好的"开闭"原则,就要尽量的遵守依赖倒转原则. 可以说依赖倒转原则是对"抽象化"的最好规范! 我个人感觉,依赖倒转原则也是里氏代换原则的补充..你理解了里氏代换原则,再来理解依赖倒转原则应该是很容易的。
2)依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中。
例子:某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。
-
/**
-
* 抽象接口
-
* @author mo-87
-
*
-
*/
-
abstract
public
class
AbstractSource {
-
abstract public void getSource();
-
}
-
/**
-
* 抽象接口
-
* @author mo-87
-
*
-
*/
-
abstract
public
class
AbstractStransformer {
-
abstract public void transform();
-
}
-
/**
-
* 具体实现
-
* @author mo-87
-
*
-
*/
-
public
class
DatabaseSource
extends
AbstractSource{
-
-
public void getSource(){
-
System.
out.println(
"Get source data");
-
}
-
}
-
-
/**
-
* 依赖注入是依赖AbstractSource抽象注入的,而不是具体
-
* DatabaseSource
-
*
-
* @author mo-87
-
*
-
*/
-
public
class
XMLStransformer {
-
/**
-
*
-
*/
-
private AbstractSource source;
-
/**
-
* 构造注入(Constructor Injection):通过构造函数注入实例变量。
-
*/
-
public void XMLStransformer(AbstractSource source){
-
this.source = source;
-
}
-
-
/**
-
* 设值注入(Setter Injection):通过Setter方法注入实例变量。
-
* @param source : the sourceto set
-
*/
-
public void setSource(AbstractSource source) {
-
this.source = source;
-
}
-
/**
-
* 接口注入(Interface Injection):通过接口方法注入实例变量。
-
* @param source
-
*/
-
public void transform(AbstractSource source ) {
-
source.getSource();
-
System.
out.println(
"Stransforming ...");
-
}
-
}
-
public
class XMLStransformer {
-
/**
-
*
-
*/
-
private AbstractSource source;
-
/**
-
* 构造注入(Constructor Injection):通过构造函数注入实例变量。
-
*/
-
public void XMLStransformer(AbstractSource source){
-
this.source = source;
-
}
-
-
/**
-
* 设值注入(Setter Injection):通过Setter方法注入实例变量。
-
* @param source : the sourceto set
-
*/
-
public void setSource(AbstractSource source) {
-
this.source = source;
-
}
-
/**
-
* 接口注入(Interface Injection):通过接口方法注入实例变量。
-
* @param source
-
*/
-
public void transform(AbstractSource source ) {
-
source.getSource();
-
System.out.println(
"Stransforming ...");
-
}
-
}
优点:采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结构就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。
6 .合成/聚合复用原则(Composite/Aggregate ReusePrinciple ,CARP):
要尽量使用对象组合,而不是继承关系达到软件复用的目的
定义:经常又叫做合成复用原则(Composite ReusePrinciple或CRP),尽量使用对象组合,而不是继承来达到复用的目的。
就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。简而言之,要尽量使用合成/聚合,尽量不要使用继承。
原则分析:
2)组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
-
public
class Head
-
{
-
private Mouth mouth;
-
public Head() {
-
mouth =
new Mouth();
-
}
-
}
-
public
class Mouth
-
{
-
}
-
public
class Car
-
{
-
private Engine engine;
-
public Car(Engine engine) {
-
this.engine = engine;
-
}
-
public void setEngine(Engine engine) {
-
this.engine = engine;
-
}
-
}
-
public
class Engine
-
{
-
}
7.迪米特法则(Law of Demeter,LoD:
系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度
定义:又叫最少知识原则(Least Knowledge Principle或简写为LKP)几种形式定义:
(1) 不要和“陌生人”说话。英文定义为:Don't talk to strangers.
Ÿ(2) 只与你的直接朋友通信。英文定义为:Talk only to your immediatefriends.
(3) 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
简单地说,也就是,一个对象应当对其它对象有尽可能少的了解。一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public方法,我就调用这么多,其他的一概不关心。
法则分析:
迪米特法则与设计模式Facade模式、Mediator模式使民无知
系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度,因为在你的系统中,扩展的时候,你可能需要修改这些类,而类与类之间的关系,决定了修改的复杂度,相互作用越多,则修改难度就越大,反之,如果相互作用的越小,则修改起来的难度就越小..例如A类依赖B类,则B类依赖C类,当你在修改A类的时候,你要考虑B类是否会受到影响,而B类的影响是否又会影响到C类. 如果此时C类再依赖D类的话,呵呵,我想这样的修改有的受了。
迪米特法则是目的,而接口隔离法则是对迪米特法则的规范. 为了做到尽可能小的耦合性,我们需要使用接口来规范类,用接口来约束类.要达到迪米特法则的要求,最好就是实现接口隔离法则,实现接口隔离法则,你也就满足了迪米特法则。
原文地址:hguisu 面向对象设计模式原则详解 http://blog.csdn.net/hguisu/article/details/7571617