设计模式六大原则

733 阅读8分钟

单一职责原则

一个类或者模块应该有且只有一个改变的原因。

优点

  1. 降低类的复杂度
  2. 提供类的可读性,提高系统的可维护性
  3. 变更引起的风险降低
  4. 降低耦合度

实现

问题由来:  类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案:  遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

开闭原则

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

优点

  1. 降低程序各部分的耦合度
  2. 提高代码的可复用性
  3. 提高软件的可维护性

实现

  • 问题由来:  在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
  • 解决方案:  当软件需要变化时,尽量通过扩展实体类的行为来实现变化,而不是通过修改已有的代码来实现变化

解决问题关键在于抽象化,抽象化是面向对象设计的第一个核心本质。
在面向对象中,通过抽象类及接口,规定了具体类的特征作为抽象层,相对稳定,从而满足"对修改关闭";从抽象类导出的具体类可以改变系统的行为,从而满足"对扩展开放"

实例

开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭。

用抽象构建框架,用实现扩展细节。

接口隔离原则

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。

实例

未遵循接口隔离原则的设计

image.png

遵循接口隔离原则的设计

image.png

依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

其核心思想是:要面向接口编程,不要面向实现编程。

这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。

优点

  1. 降低类之间的耦合性
  2. 提高系统的稳定性
  3. 降低修改程序造成的风险

实现

  • 问题由来:  类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
  • 解决方案: 将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。 以抽象方式耦合是依赖倒转原则的关键。抽象耦合关系总要涉及具体类从抽象类继承,并且需要保证在任何引用到基类的地方都可以改换成其子类,因此,里氏代换原则是依赖倒转原则的基础。

代码

场景:母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。

class Book {
    public String getContent() {
        return "很久很久以前......";
    }
}

class Monther {
    public void narrate(Book book) {
        System.out.println("妈妈开始讲故事");
        System.out.println(book.getContent());
    }
}

public class DIPClient {
    public static void main(String[] args) {
        Monther monther = new Monther();
        monther.narrate(new Book());
    }
}

输出结果:
妈妈开始讲故事
很久很久以前......

如果此时需要讲报纸上的内容,就需要再新建一个类Newspaper

class Newspaper {
    public String getContent() {
        return "金融风暴卷土而来......";
    }
}

class Book {
    public String getContent() {
        return "很久很久以前......";
    }
}

class Monther {
    public void narrate(Book book) {
        System.out.println("妈妈开始讲书上的故事");
        System.out.println(book.getContent());
    }
    // 修改Mother类。
    public void narrate(Newspaper newspaper) {
        System.out.println("妈妈开始讲报纸上的内容");
        System.out.println(newspaper.getContent());
    }
}

public class DIPClient {
    public static void main(String[] args) {
        Monther monther = new Monther();
        monther.narrate(new Book());

        monther.narrate(new Newspaper());
    }
}

输出结果:
妈妈开始讲书上的故事
很久很久以前......
妈妈开始讲报纸上的内容
金融风暴卷土而来......

如果再来个讲头条的内容,又要新建一个类,然后改动Monther类,这样做不太合理。此时新建接口类,使其依赖于接口,而不是具体实现类,达到Monther类不用修改的目的。

interface IReader {
    String getContent();
}

class Newspaper implements IReader {
    @Override
    public String getContent() {
        System.out.println("妈妈开始讲报纸上的内容");
        return "金融风暴卷土而来......";
    }
}

class Book implements IReader {
    @Override
    public String getContent() {
        System.out.println("妈妈开始讲书上的故事");
        return "很久很久以前......";
    }
}

class Monther {
    public void narrate(IReader reader) {
        System.out.println(reader.getContent());
    }
}

public class DIPClient {
    public static void main(String[] args) {
        Monther monther = new Monther();
        monther.narrate(new Book());
        monther.narrate(new Newspaper());
    }
}

输出结果:
妈妈开始讲书上的故事
很久很久以前......
妈妈开始讲报纸上的内容
金融风暴卷土而来......

里氏替换原则

所有引用基类(父类)的地方必须能透明地使用其子类的对象。通俗讲:子类可以扩展父类的功能,但不能改变父类原有的功能。

实现

  • 问题由来:  有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
  • 解决方案: 当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

实例

举例说明两个数相减,由A类负责

class A {
    public int func1(int a, int b) {
        return a-b;
    }
}

public class LSPClient {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("5-3=" + a.func1(5, 3));
        System.out.println("100-10=" + a.func1(100, 10));
    }
}

输出结果:
5 - 3 = 2
100 - 10 = 90

此时需要新加一个功能:两数相加,然后再与100求和,由B类负责

class A {
    public int func1(int a, int b) {
        return a - b;
    }
}

class B extends A {
    @Override
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 100;
    }
}

public class LSPClient {
    public static void main(String[] args) {
        B b = new B();
        System.out.println("5-3=" + b.func1(5, 3));
        System.out.println("100-10=" + b.func1(100, 10));
        System.out.println("50+20+100=" + b.func2(50, 20));
    }
}

输出结果:
5-3=8
100-10=110
50+20+100=170

迪米特法则

迪米特法则(Law of Demeter,LoD)又叫作最少知识原则。

迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。 其目的是降低类之间的耦合度,提高模块的相对独立性。

优点

  1. 降低类之间的耦合度,提高了模块的相对独立性
  2. 耦合度降低,从而提高了类的可重用率和系统的扩展性

缺点

  • 过度使用迪米特原则,会产生大量的中介类,导致系统的复杂度提高。在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰

实现

  • 问题由来:  类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大
  • 解决方案:  尽量降低类与类之间的耦合
  1. 需强调

    • 从依赖者的角度来说,只依赖应该依赖的对象。
    • 从被依赖者的角度说,只暴露应该暴露的方法。
  2. 需注意

    • 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
    • 在类的结构设计上,尽量降低类成员的访问权限。
    • 在类的设计上,优先考虑将一个类设置成不变类。
    • 在对其他类的引用上,将引用其他对象的次数降到最低。
    • 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
    • 谨慎使用序列化(Serializable)功能。

总结

  1. 找出应用中可能需要变化之处,把他们独立出来,不要和哪些不需要变化的代码混在一起。

参考: