1.开闭原则(OCP--open close principle)是面向对象设计中“可复用设计”的基石。
开闭原则中的“开”,指对组件功能的拓展是开放的,当需求发生变动时,能够对原模块进行拓展,使其满足新加进来的需求;
开闭原则中的“闭”,指对原功能代码的改动是封闭禁止的。
因此,实现开闭原则的关键就在于使用“抽象”。把系统的全部可能的行为抽象为一个抽象的底层,这个抽象底层规定了全部详细实现必须提供的方法的特征。而作为系统设计的抽象层,要预见全部可能的拓展,当需要对系统的功能进行拓展时,可以从抽象底层导出一个或多个新的详细实现,能够改变系统的行为。
假如一个系统符合开闭原则,那么它应该具有如下的优点:
1)可复用好。能够在软件开发完毕后,仍然能够对软件进行拓展,添加新的功能,而系统原先的代码不必变动。
2)可维护性好。对于系统原有的组件,不必进行改动,这样保证了原有功能的稳定性
2.单一原则
就一个类而言,它只负责一项职责。这就要求在进行功能划分时,将其划分的更加清楚。
单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
注意:单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。
3.里氏替换原则
所有引用基类的地方必须能透明的使用其子类的对象---子类能够替换掉父类的对象,并且程序逻辑不变。
里氏替换原则在以下两种场景有不同的表现:
1)里氏替换原则是针对继承而言的,如果继承是为了更好的代码重用,即方法的共享,那么共享的父类方法就应该保持不变,子类不能重新定义。子类只能新添加方法来拓展功能,父类和子类都能实例化,子类继承的方法和父类是一致的,父类调用方法的地方,子类可以调用同一个继承来的方法,这时用子类方法替换父类方法,逻辑一致。
2)而假如继承是为了实现多态,多态的前提是子类覆盖并重新定义父类的方法,为了符合里氏替换原则,这时应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法。当父类是抽象类时,也就不存在可实例化的父类对象在程序里。也就不存在子类能够替换父类方法这一场景了。
因此,不符合里氏替换原则的场景是,子类和父类都是非抽象类,且父类的方法被子类重新定义了。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
就拿下面这个例子来说,正方形在定义上属于长方形的一种,当把正方形设置为长方形的子类时,在一些情况下使用正方形的方法替换父类长方形的方法却会出错。
/** * 定义一个长方形类,只有标准的get和set方法 * * @author sxh * */ public class Rectangle { protected long width; protected long height; public void setWidth(long width) { this.width = width; } public long getWidth() { return this.width; } public void setHeight(long height) { this.height = height; } public long getHeight() { return this.height; }}
/** * 定义一个正方形类继承自长方形类,只有一个side * * @author sxh * */ public class Square extends Rectangle { public void setWidth(long width) { this.height = width; this.width = width; } public long getWidth() { return width; } public void setHeight(long height) { this.height = height; this.width = height; } public long getHeight() { return height; }} public class SmartTest{ /** * 长方形的长不短的增加直到超过宽 * @param r */ public void resize(Rectangle r){ while (r.getHeight() <= r.getWidth() ){ r.setHeight(r.getHeight() + 1); } }}
在上边的代码中我们定义了一个长方形和一个继承自长方形的正方形,看着是非常符合逻辑的,但是当我们调用SmartTest类中的resize方法时,长方形是可以的,但是正方形就会一直增大,一直long溢出。但是我们按照我们的里氏替换原则,父类可以的地方,换成子类一定也可以,所以上边的这个例子是不符合里氏替换原则的。
分析下来,我们发现子类并未重写或重载父类的方法,因此,这个继承关系根本不成立。这时,对这种不符合里氏替换原则的关系,我们需要重构他们的关系。
我们重新定义一个父类,让正方形和长方形都继承这个父类。
public abstract class Quadrangle { protected abstract long getWidth(); protected abstract long getHeight();} /** * 自己声明height和width * @author sxh * */ public class Rectangle extends Quadrangle { private long width; private long height; public void setWidth(long width) { this.width = width; } public long getWidth() { return this.width; } public void setHeight(long height) { this.height = height; } public long getHeight() { return this.height; }}
/** * 自己声明height和width * @author xingjiarong * */ public class Square extends Quadrangle{ private long width; private long height; public void setWidth(long width) { this.height = width; this.width = width; } public long getWidth() { return width; } public void setHeight(long height) { this.height = height; this.width = height; } public long getHeight() { return height; }}
在基类Quadrange类中没有赋值方法,因此类似于SamrtTest的resize()方法不可能适用于Quadrangle类型,而只能适用于不同的具体子类Rectangle和Aquare,因此里氏替换原则不可能被破坏了。
4.组合复用原则
合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
1)组合复用有以下好处:
-
新对象存取成分对象的唯一方法是通过成分对象的接口。
-
这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
-
这种复用支持包装。
-
这种复用所需要的依赖较少。
-
每一个新的类可以将焦点集中到一个任务上。
-
这种复用可以在运行时间动态进行,新对象可以动态的引用与成分对象类型相同的对象。
组合复用的缺点就是用组合复用建造的系统会有较多的对象需要管理。
除了组合可以实现复用外,之前讲到的继承也是实现复用的一种手段。但继承会导致代码的耦合度增加,当基类发生变更时,它的子类要跟着相应的改变。按照组合复用的原则,应该首选组合,然后才是继承,并且在使用继承时要严格遵守里氏替换原则,必须满足“IS-a”的关系才是继承,而组合是“Has-a”的关系。
5.依赖倒置原则
高层模块不应该依赖于低层模块,两者都应该依赖其抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
在软件的设计中,细节具有多变性,而抽象相对稳定,因此以抽象为基础搭建起来的系统比以实现为基础搭建起来机构更加稳定些。这里的抽象是指接口或者抽象类,而实现指具体的实现类。
使用接口和抽象类的目的是制定好规范和契约,而不去设计任何具体的操作,把展示细节的任务交给实现类去完成。
public class Test1 { // 商店 interface Shop { void sell(); } // 大润发超市 class daRunFa implements Shop{ @Override public void sell() { System.out.println("欢迎来到大润发..."); } } // 华润万家 class huaRun implements Shop { @Override public void sell() { System.out.println("欢迎来到华润万家..."); } } // 消费者 class Customer { public void shopping(Shop shop) { shop.sell(); } } @Test public void fun() { Customer customer = new Customer(); customer.shopping(new daRunFa()); customer.shopping(new huaRun()); } }
最终输出:欢迎来到大润发... 欢迎来到华润万家...
6.接口隔离原则
接口隔离原则建议为每一个类建立一个单独的接口,而不是建立一个很大的接口供所有依赖它的类去调用。
对比单一职责和接口隔离原则,两者都提高了类的内聚性,降低了代码的耦合度。但两者仍有不同:
a) 单一职责注重的是对类的功能的划分,接口隔离原则则是注重了对接口依赖的隔离,防止接口过于臃肿导致功能不明确。
b) 单一职责主要约束的是类,它针对的是程序中具体的实现和细节;而后者主要针对程序整体架构构建。
注意点:接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
7.迪米特法则
迪米特法则强调,当两个软件实体无需直接通信,那么就不应该发生直接的相互调用,可以通过第三方转发该调用。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。
迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成搏击,也就是说,信息的隐藏促进了软件的复用。
实现迪米特法则的注意点:
a) 类的成员权限尽量设置的低些
b) 类的属性成员不要暴露,尽量设置get和set方法
c) 只依赖该依赖的方法,只暴露该暴露的方法。