设计模式的六大原则(快速复习版)

单一职责原则(Single responsibility principle,SRP)

定义:一个类只负责一项职责,不要存在多于一个导致类变更的原因。

原因职责扩散。因为某种原因,职责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的代码、类级别符合单一职责的代码、方法级别符合单一职责的代码。这三种各有各的好处,一般使用场景应该参考如下要点:

  • 当逻辑足够简单,后续不太可能会再次发生职责扩散,那么可以考虑使用if-else这种的代码方式,毕竟最容易修改,最快速完成。
  • 当类中的方法数量足够的少,逻辑简单的完全处于掌控之中的时候,可以在方法级别上满足单一职责,这样毕竟不用新建类,不用大规模地分离代码。
  • 当逻辑快要超出你掌控范围了,赶快进行类级别的单一职责重构吧。否则,你每一次的修改增加都有很大风险会影响其它功能。

里氏替换原则(Liskov Substitution Principle,LSP)

定义:所有引用基类的地方必须能透明地使用其子类的对象,并且程序的行为不会发生变化。

原因:有一功能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

我们发现原本运行正常的相减功能发生了错误。原因就是子类在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了重写后的方法,造成原本运行正常的功能出现了错误。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。

依赖倒置原则(Dependency Inversion Principle,DIP)

定义:高层模块不应该依赖于底层模块,它们都应该依赖抽象;抽象不依赖细节,细节依赖抽象。

原因:类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类了。
依赖倒置原则的核心就是面向接口编程,在实际编程中,一般遵循以下几点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。
  • 变量的声明类型尽量是抽象类或接口。
  • 使用继承时遵循里氏替换原则。

接口隔离原则(Interface Segregation Principle,ISP)

定义:类不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。

原因:一个综合类的接口包含了很多的抽象方法,类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在实现的时候只需要实现自己需要的即可。

接口隔离原则的含义是:
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

注意点

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

迪米特法则(Law of Demeter,LoD;Least Knowledge Principle,LKP)

定义:一个对象应该对其他对象保持最少的了解。

原因:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

注意:迪米特法则又叫最少知道原则,一个类对自己依赖的类应该知道的越少越好;对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
迪米特法则还有一个更简单的定义:只与直接的朋友通信。那么符合什么样的条件才算是直接的朋友呢?

  • 当前对象本身(this)。
  • 以参量形式传入到当前对象方法中的对象。
  • 当前对象的实例变量直接引用的对象。
  • 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友。
  • 当前对象所创建的对象。

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。

代码实例:

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<Animals> list;

    public void generateAnimals(){
        list = new ArrayList<Animals>();
        for(int i=0;i<5;i++){
            Animals a = new Animals();
            a.setCategory("category" + i);
            list.add(a);
        }
    }

    public List<Animals> getAllAnimals(){
        return list;
    }

}
public class Earth {

    private List<Humans> list;

    public void generateHumans(){
        list = new ArrayList<Humans>();
        for(int i=0;i<3;i++){
            Humans a = new Humans();
            a.setName("name" + i);
            list.add(a);
        }
    }

    public List<Humans> getAllHumans(){
        return list;
    }

    public void printAllCreatures(Forests f){
        f.generateAnimals();
        List<Animals> animalsList = f.getAllAnimals();
        for (Animals animals : animalsList) {
            System.out.println(animals.getCategory());
        }

        this.generateHumans();
        List<Humans> 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<Animals> list;

    public void generateAnimals(){
        list = new ArrayList<Animals>();
        for(int i=0;i<5;i++){
            Animals a = new Animals();
            a.setCategory("category" + i);
            list.add(a);
        }
    }

    public List<Animals> getAllAnimals(){
        return list;
    }

    public void printAllAnimals(){
        this.generateAnimals();
        List<Animals> animalsList = this.getAllAnimals();
        for (Animals animals : animalsList) {
            System.out.println(animals.getCategory());
        }
    }

}
public class Earth2 {

    private List<Humans> list;

    public void generateHumans(){
        list = new ArrayList<Humans>();
        for(int i=0;i<3;i++){
            Humans a = new Humans();
            a.setName("name" + i);
            list.add(a);
        }
    }

    public List<Humans> getAllHumans(){
        return list;
    }

    public void printAllCreatures(Forests2 f){
        f.printAllAnimals();

        this.generateHumans();
        List<Humans> 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了。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

开闭原则(Open Closed Principle,OCP)

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

原因:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

其实,我们遵循设计模式前面5大原则的目的就是遵循开闭原则,也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的。

开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性预见性才行。

回想一下前面说的5项原则:

  • 单一职责原则告诉我们实现类要职责单一;
  • 里氏替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要面向接口编程;
  • 接口隔离原则告诉我们在设计接口的时候要精简单一;
  • 迪米特法则告诉我们要降低耦合;
  • 开闭原则是总纲,对扩展开放,对修改关闭。

代码:可以参考前面依赖倒置原则的代码,这就是符合开闭原则的一个例子。还有后面将要讲的策略模式,也是遵循的开闭原则,这里不再赘述。

总结

对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。

参考:http://blog.csdn.net/zhengzhb/article/category/926691/1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值