定义:一个类只负责一项职责,不要存在多于一个导致类变更的原因。
原因:职责扩散。因为某种原因,职责R被分成了更细粒度的职责R1和R2。比如,有一个类T负责两个不同的职责,职责R1和职责R2,当职责R1需求发生变化的时候需要修改类T,此时就有风险会使得职责R2发生故障。
场景:类或者方法比较复杂,职责扩散不可避免,在扩散超出我们控制范围之前,立即使用单一职责原则对代码进行重构。
所以说,单一职责原则不是必须的,而是要看具体的场景。下面举例说明。
代码实例:
public class Animal {
public void breath(String animal){
System.out.println(animal + " breath air!");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
animal.breath("sheep");
animal.breath("pig");
animal.breath("cow");
}
}
如上代码就是符合单一职责原则的例子。Animal类只负责呼吸只一个职责。
现在出于某种原因,职责发生了扩散,比如我们发现不是所有Animal都是呼吸空气的,像鱼就是呼吸水的,那么最简单的就是在Animal中增加一个判断逻辑。
public class Animal2 {
public void breath(String animal){
if("fish".equals(animal)){
System.out.println(animal + " breath water!");
}else {
System.out.println(animal + " breath air!");
}
}
}
public class Main {
public static void main(String[] args) {
Animal2 animal = new Animal2();
animal.breath("sheep");
animal.breath("pig");
animal.breath("cow");
animal.breath("fish");
}
}
但是很明显,它不符合单一职责原则,对后续的维护十分不便利。如果按照单一职责的话,那么应该修改如下:
public class Aquatic {
//水生生物
public void breath(String animal){
System.out.println(animal + " breath water!");
}
}
public class Terrestrial {
//陆生生物
public void breath(String animal){
System.out.println(animal + " breath air!");
}
}
public class Main {
public static void main(String[] args) {
Terrestrial t = new Terrestrial();
t.breath("sheep");
t.breath("pig");
t.breath("cow");
Aquatic a = new Aquatic();
a.breath("fish");
}
}
可以看到,这样修改处理需要将原有的Animal类进行分解之外,还需要修改客户端。还可以用下这种修改方式。
public class Animal3 {
public void aquaticBreath(String animal){
//水生动物呼吸
System.out.println(animal + " breath water!");
}
public void terrestrialBreath(String animal){
////陆生动物呼吸
System.out.println(animal + " breath air!");
}
}
public class Main {
public static void main(String[] args) {
Animal3 animal = new Animal3();
animal.terrestrialBreath("sheep");
animal.terrestrialBreath("pig");
animal.terrestrialBreath("cow");
animal.aquaticBreath("fish");
}
}
如上所示,水生和陆生的动物分别调用不同的方法,虽然在类级别上违反了单一职责原则,但是在方法层面上确是符合单一职责的,即一个方法只负责一个职责。
单一职责的优点:
总结:
并不是说写任何代码一定要符合单一职责原则,我们在上面总共列出三种代码,分别是if-else的代码、类级别符合单一职责的代码、方法级别符合单一职责的代码。这三种各有各的好处,一般使用场景应该参考如下要点:
定义:所有引用基类的地方必须能透明地使用其子类的对象,并且程序的行为不会发生变化。
原因:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P2由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
使用:类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
里氏替换原则主要是为了解决继承的问题,父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改(重写或者重载),就会对整个继承体系造成破坏。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
代码实例:
public class Calculator {
public int operation(int a,int b){
return a-b;
}
}
public class Main {
public static void main(String[] args) {
Calculator c = new Calculator();
System.out.println("100-50=" + c.operation(100, 50));
}
}
如上是实现一个计算器,但是只有减法功能。现在需要扩展该计算器,增加一个自定义计算。
public class SelfDefitionCalculator extends Calculator {
public int operation(int a,int b){
return a*b;
}
public int cal(int a,int b){
return operation(a,b) + 100;
}
}
public class Main {
public static void main(String[] args) {
SelfDefitionCalculator sdc = new SelfDefitionCalculator();
//想使用父类的减法功能
System.out.println("100-50=" + sdc.operation(100, 50));
//使用自定义操作
System.out.println("100*50+100=" + sdc.cal(100, 50));
}
}
运行后结果为:
100-50=5000
100*50+100=5100
我们发现原本运行正常的相减功能发生了错误。原因就是子类在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了重写后的方法,造成原本运行正常的功能出现了错误。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
定义:高层模块不应该依赖于底层模块,它们都应该依赖抽象;抽象不依赖细节,细节依赖抽象。
原因:类A是高层模块,负责实现具体的业务逻辑,类B是底层模块,负责原子操作。类A依赖类B,当需求发生变化,需要将类A改为依赖类C,那么必须修改类A的代码,这会给程序带来很大的风险。
使用:给底层模块的类B和类C抽象出一个接口I(或者抽象类),类A依赖接口I,这样,当需要改变依赖对象的时候,就降低了修改类A的几率。
代码实例:
public class Book {
public String getContent(){
return "Book Content Start!";
}
}
public class Mother {
public void tellStory(Book book){
System.out.println(book.getContent());
}
}
public class Main {
public static void main(String[] args) {
Mother mother = new Mother();
mother.tellStory(new Book());
}
}
在如上的代码中,高层模块Mother类直接依赖具体的实现类Book,违反了依赖倒置原则。当不再读书上的故事,而是读报纸、杂志、网络上的故事的时候,需要将Mother类中的Book改成对应的底层模块类,不仅为Mother类的正常运行引进了风险,而且不利于维护。
现在依据依赖倒置原则进行重构,高层和底层模块都依赖抽象。
public interface IReader {
public String getContent();
}
public class Magazine implements IReader {
@Override
public String getContent() {
return "Magazine Content Start!";
}
}
public class Newspaper implements IReader {
@Override
public String getContent() {
return "Newspaper Content Start!";
}
}
public class Mother2 {
public void tellStory(IReader reader){
System.out.println(reader.getContent());
}
}
public class Main {
public static void main(String[] args) {
Mother2 mother = new Mother2();
mother.tellStory(new Magazine());
}
}
在重构后的代码中可以看到,Mother2类依赖的是抽象IReader接口,以后需要读别的类型的故事,就不再需要修改Mother2类了。
依赖倒置原则的核心就是面向接口编程,在实际编程中,一般遵循以下几点:
定义:类不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
原因:一个综合类的接口包含了很多的抽象方法,类A在实现这个接口的时候,不得不实现很多它其实不需要的方法。
代码实例:
public interface Creature {
public void makeSound();
public void drink();
public void breath();
public void photosynthesis();
}
public class Animal implements Creature {
@Override
public void makeSound() {
System.out.println("Animal make sound");
}
@Override
public void drink() {
System.out.println("Animal drink water");
}
@Override
public void breath() {
System.out.println("Animal breath air");
}
@Override
public void photosynthesis() {
System.out.println("Animal can not photosynthesis");
}
}
public class Plants implements Creature {
@Override
public void makeSound() {
System.out.println("Plants can not make sound");
}
@Override
public void drink() {
System.out.println("Plants drink water");
}
@Override
public void breath() {
System.out.println("Plants breath air");
}
@Override
public void photosynthesis() {
System.out.println("Plants photosynthesis");
}
}
public class Main {
public static void main(String[] args) {
Creature sheep = new Animal();
sheep.breath();
sheep.drink();
sheep.makeSound();
Creature appleTree = new Plants();
appleTree.breath();
appleTree.drink();
appleTree.photosynthesis();
//unexpected behaviors
System.out.println("Below are unexpected behaviors:");
sheep.photosynthesis();
appleTree.makeSound();
}
}
在如上的代码中,接口Creature包含了生物的很多特性方法。比如makeSound(发声)、drink(饮水)、breath(呼吸)、photosynthesis(光合作用)。它的实现类Animal和Plants并不是都需要所有这些方法,比如Animal不需要光合作用,Plants不需要发声。如此,在Animal和Plants中不得不实现它们不需要的方法,最后还可能引起在客户端中发生意想不到的行为,比如动物进行光合作用,这显然是很不安全的。
解决:为了符合接口隔离原则,必须对以上的Creature接口进行拆分,我们将所有的生物行为拆到三个接口中:
public interface Creature2 {
public void drink();
public void breath();
}
public interface AnimalCreature {
public void makeSound();
}
public interface PlantsCreature {
public void photosynthesis();
}
public class Animal2 implements Creature2, AnimalCreature {
@Override
public void makeSound() {
System.out.println("Animal make sound");
}
@Override
public void drink() {
System.out.println("Animal drink water");
}
@Override
public void breath() {
System.out.println("Animal breath air");
}
}
public class Plants2 implements Creature2, PlantsCreature {
@Override
public void photosynthesis() {
System.out.println("Plants photosynthesis");
}
@Override
public void drink() {
System.out.println("Plants drink water");
}
@Override
public void breath() {
System.out.println("Plants breath air");
}
}
其中接口Creature2值存放公共的方法,AnimalCreature和PlantsCreature分别存放各自需要的方法。如此,Animal2和Plants2在实现的时候只需要实现自己需要的即可。
接口隔离原则的含义是:
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
注意点:
定义:一个对象应该对其他对象保持最少的了解。
原因:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
注意:迪米特法则又叫最少知道原则,一个类对自己依赖的类应该知道的越少越好;对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
迪米特法则还有一个更简单的定义:只与直接的朋友通信。那么符合什么样的条件才算是直接的朋友呢?
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。
代码实例:
public class Animals {
private String category;
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
public class Humans {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Forests {
private List list;
public void generateAnimals(){
list = new ArrayList();
for(int i=0;i<5;i++){
Animals a = new Animals();
a.setCategory("category" + i);
list.add(a);
}
}
public List getAllAnimals(){
return list;
}
}
public class Earth {
private List list;
public void generateHumans(){
list = new ArrayList();
for(int i=0;i<3;i++){
Humans a = new Humans();
a.setName("name" + i);
list.add(a);
}
}
public List getAllHumans(){
return list;
}
public void printAllCreatures(Forests f){
f.generateAnimals();
List animalsList = f.getAllAnimals();
for (Animals animals : animalsList) {
System.out.println(animals.getCategory());
}
this.generateHumans();
List humansList = this.getAllHumans();
for (Humans humans : humansList) {
System.out.println(humans.getName());
}
}
}
public class Main {
public static void main(String[] args) {
Earth earth = new Earth();
Forests forest = new Forests();
earth.printAllCreatures(forest);
}
}
在以上的代码中,类Earth中的List、Humans、Forests所创建的对象都是直接的朋友,而List、Animals所创建的对象都不是直接的朋友(具体参考上面判断直接朋友的方法),这违反了迪米特法则。
从逻辑上看Animals的行为也不应该由Earth来负责,完全可以交给Forests来处理,所以,Earth和Animals的耦合其实是不必要的。
修改后的代码:
public class Forests2 {
private List list;
public void generateAnimals(){
list = new ArrayList();
for(int i=0;i<5;i++){
Animals a = new Animals();
a.setCategory("category" + i);
list.add(a);
}
}
public List getAllAnimals(){
return list;
}
public void printAllAnimals(){
this.generateAnimals();
List animalsList = this.getAllAnimals();
for (Animals animals : animalsList) {
System.out.println(animals.getCategory());
}
}
}
public class Earth2 {
private List list;
public void generateHumans(){
list = new ArrayList();
for(int i=0;i<3;i++){
Humans a = new Humans();
a.setName("name" + i);
list.add(a);
}
}
public List getAllHumans(){
return list;
}
public void printAllCreatures(Forests2 f){
f.printAllAnimals();
this.generateHumans();
List humansList = this.getAllHumans();
for (Humans humans : humansList) {
System.out.println(humans.getName());
}
}
}
public class Main {
public static void main(String[] args) {
Earth2 earth = new Earth2();
Forests2 forest = new Forests2();
earth.printAllCreatures(forest);
}
}
更改过后,Earth2中的对象都是直接朋友,非直接朋友Animals被移到了Forests2中,成了Forests2的直接朋友。如此实现了解耦,下次如果修改Animals类,就不需要修改Earth2了。
迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
原因:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
解决:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
其实,我们遵循设计模式前面5大原则的目的就是遵循开闭原则,也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的。
开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
回想一下前面说的5项原则:
代码:可以参考前面依赖倒置原则的代码,这就是符合开闭原则的一个例子。还有后面将要讲的策略模式,也是遵循的开闭原则,这里不再赘述。
对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。
参考:http://blog.csdn.net/zhengzhb/article/category/926691/1