程序员社区

设计模式总结(上)


title: 设计模式
date: 2020/03/02 14:39


前言

设计模式是为了让程序具有更好的:

  1. 代码重用性(相同功能的代码,不用多次编写)
  2. 可读性(编程规范性,便于其他程序员阅读)
  3. 可扩展性(当需要增加新功能时很方便)
  4. 可靠性(当新增新的功能后,对原来的永能没有影响)

从而使程序展现 高内聚、低耦合 的特性。

重点:不要依赖实现编程;封装变化

高内聚:一个模块由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。模块的内聚反映模块内部联系的紧密程度。一个模块只需做好一件事件,不要过分关心其他任务。

低耦合:模块及模块之间信息或参数依赖的程度要低。

单一职责:一个类只负责一个职责。当然会出现“职责扩散”,出现时要及时的“重构”。

接口隔离:一个类对另一个类的依赖应该建立在最小的接口上

依赖倒置:依赖抽象,不要依赖具体类。

里式替换:尽量不要重写/重载父类的方法。

开闭原则:一个软件实体如类、模块和函数应该对扩展开放(针对提供方),对修改关闭(针对使用方)

迪米特法则:只与“直接朋友”交互。

好莱坞原则:低层组件不可以直接调用高层组件,但是高层组件控制何时以及如何让低层子组件参与。

结构型模式:描述类类、类对象之间的结合方式,实现上基本上都是尽量利用组合/关联关系替代继承关系。

行为模式:主要是根据具体的场景描述各个对象之间的相互关系和职责和通信方式。

单例模式:确保一个类只有一个实例,并提供一个全局访问点。

简单工厂模式:定义一个创建对象的类,由这个类来封装实例化对象的行为(代码)。

工厂方法模式:1. 对象创建很复杂,2. 多个地方要使用这个对象

抽象工厂模式:生产一类产品族

原型模式:当创建类的实例的过程很昂贵或很复杂时,可以使用clone方法。

建造者模式:封装一个产品的建造过程,并且该建造过程对一类产品通用;如果需要修改建造过程,那么直接修改建造者类即可。

适配器模式:将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作

桥接模式:将事物对象(毛笔)和其具体行为、具体特征(颜色)分离开来,使它们可以各自独立的变化;当一个对象有多个纬度变化且要进行排列组合时使用。

装饰器模式:包装一个对象,并提供额外的行为。

组合模式:将层次结构组装成一个树型结构,让客户以一致的方式处理个别对象以及对象组合。要点:将树型结构格式的不同层级对象继承同一个组件。

外观模式:引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。

享元模式:运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。

代理模式:包装一个对象,并控制对它的访问。

模板方法模式:定义一个算法的步骤,并允许子类为一个或多个步骤的实现。

命令模式:解决动作请求者直接调用动作接收者导致的高度耦合,如果想切换动作接收者,需要修改的代码过多。使用场景:动作的接收者会经常的变动(例如:由很多“if/else”来决定最终调用者是谁)

访问者模式:为一系列元素(实现了Element接口的类)添加不同操作的方法。一般配合组合模式一起用,参见P628.

迭代器模式:提供一种方法顺序访问一个集合中的各个元素,而又不暴露其内部的表示。迭代器模式将存储数据和遍历数据的职责分离。

观察者模式:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe)模式。

中介者模式:使用一个中介对象封装多个同级别对象之间的交互

备忘录模式:保存对象的历史信息

解释器模式:我看不进去,暂略。当需要定义一些简单的语法规则,可以使用它

状态模式:对象在内部状态改变时会改变它的行为。封装基于状态的行为,并将行为委托到当前状态。不同状态下表现不同的行为时使用。

策略模式:将可以互换的行为封装起来,然后使用委托的方法,决定使用哪个行为。系统需要动态地在几种算法中选择一种可以使用策略模式。

责任链模式:有一个以上的对象有机会能够处理某个请求时使用。(其实我觉得不要强行使用这个设计模式,当你调试的时候会崩溃的,我觉得Tomcat使用FilterChain的主要原因是因为他有“往返”,像我们这种普通的,直接list.for()就完事了)

一、设计模式的6大设计原则

1.1 单一职责原则

定义

一个类应该只有一个引起变化的原因。通俗的说,就是一个类只负责一个职责。

问题由来

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

解决方案

分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

职责扩散:因为某种原因,职责P被分化为粒度 更细的职责P1和P2。

例子

class Animal {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸空气");
    }
}

public class Client {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
    }
}

如上所示,我们有一个Animal类,里面有一个呼吸的方法,由于一开始只有牛羊猪三种动物,所以他们呼吸空气是没问题的,但有突然告诉你多了一个动物:鱼,这样鱼呼吸空气是肯定不妥的,所以我们就需要对其进行修改:

class Animal {
    public void breathe(String animal) {
        if (animal.equals("鱼")) {
            System.out.println(animal + "呼吸水");
        } else {
            System.out.println(animal + "呼吸空气");
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
        animal.breathe("鱼");
    }
}

这样就是我们上面所说的职责扩散,一开始Animal类的breathe方法负责所有动物的呼吸,但后来发现鱼是不能呼吸空气的,所以就要对breathe方法进行扩展,从而导致breathe方法的职责扩散为:陆地动物的呼吸和鱼的呼吸。

虽然像上面这种修改方式很简单,但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用"猪""牛""羊"等相关功能带来风险,也许某一天你会发现程序运行的结果变为"牛呼吸水"了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。

所以我们要符合单一职责对其进行修改,新建2个类,分别包含不同的职责:

class Terrestrial {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸空气");
    }
}

class Aquatic {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸水");
    }
}

public class Client {
    public static void main(String[] args) {
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");
        terrestrial.breathe("羊");
        terrestrial.breathe("猪");
        Aquatic aquatic = new Aquatic();
        aquatic.breathe("鱼");
    }
}

当Animal类中方法不是很多的时候,还有一种修改方式:

class Animal {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸空气");
    }

    public void breathe2(String animal) {
        System.out.println(animal + "呼吸水");
    }
}

public class Client {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
        animal.breathe2("鱼");
    }
}

这样虽然也违背了单一职责原则(当修改职责鱼呼吸的时候,要修改职责陆地动物呼吸的类),但在方法级别上却是符合单一职责原则的(修改职责鱼呼吸的方法,不会碰到陆地动物呼吸的方法),因为它并没有动原来方法的代码。

总结

  1. 单一职责可以降低类的复杂度,一个类只负责一项职责。
  2. 提高类的可读性,可维护性
  3. 降低变更引起的风险
  4. 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则。

1.2 接口隔离原则(接口依赖隔离)

定义

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上**。不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。

接口隔离原则(Interface Segregation Principle, ISP)表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中的方法分组,然后用多个接口替代它,每个接口服务于一个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好很多。

问题由来

类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

设计模式总结(上)插图

解决方案

将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

设计模式总结(上)插图1

例子

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

public class Segregation1 {

    public static void main(String[] args) {
    }

}

//接口
interface Interface1 {
    void operation1();
    void operation2();
    void operation3();
    void operation4();
    void operation5();
}

class B implements Interface1 {
    public void operation1() {
        System.out.println("B 实现了 operation1");
    }
    
    public void operation2() {
        System.out.println("B 实现了 operation2");
    }
    public void operation3() {
        System.out.println("B 实现了 operation3");
    }
    public void operation4() {
        System.out.println("B 实现了 operation4");
    }
    public void operation5() {
        System.out.println("B 实现了 operation5");
    }
}

class D implements Interface1 {
    public void operation1() {
        System.out.println("D 实现了 operation1");
    }
    
    public void operation2() {
        System.out.println("D 实现了 operation2");
    }
    public void operation3() {
        System.out.println("D 实现了 operation3");
    }
    public void operation4() {
        System.out.println("D 实现了 operation4");
    }
    public void operation5() {
        System.out.println("D 实现了 operation5");
    }
}

class A { //A 类通过接口Interface1 依赖(使用) B类,但是只会用到1,2,3方法
    public void depend1(Interface1 i) {
        i.operation1();
    }
    public void depend2(Interface1 i) {
        i.operation2();
    }
    public void depend3(Interface1 i) {
        i.operation3();
    }
}
  
class C { //C 类通过接口Interface1 依赖(使用) D类,但是只会用到1,4,5方法
    public void depend1(Interface1 i) {
        i.operation1();
    }
    public void depend4(Interface1 i) {
        i.operation4();
    }
    public void depend5(Interface1 i) {
        i.operation5();
    }
}

遵循接口隔离原则的设计

package com.atguigu.principle.segregation.improve;

public class Segregation1 {

    public static void main(String[] args) {
        A a = new A();
        a.depend1(new B()); // A类通过接口去依赖B类
        a.depend2(new B());
        a.depend3(new B());

        C c = new C();

        c.depend1(new D()); // C类通过接口去依赖(使用)D类
        c.depend4(new D());
        c.depend5(new D());

    }

}

// 接口1
interface Interface1 {
    void operation1();

}

// 接口2
interface Interface2 {
    void operation2();

    void operation3();
}

// 接口3
interface Interface3 {
    void operation4();

    void operation5();
}

class B implements Interface1, Interface2 {
    public void operation1() {
        System.out.println("B 实现了 operation1");
    }

    public void operation2() {
        System.out.println("B 实现了 operation2");
    }

    public void operation3() {
        System.out.println("B 实现了 operation3");
    }

}

class D implements Interface1, Interface3 {
    public void operation1() {
        System.out.println("D 实现了 operation1");
    }

    public void operation4() {
        System.out.println("D 实现了 operation4");
    }

    public void operation5() {
        System.out.println("D 实现了 operation5");
    }
}

class A { // A 类通过接口Interface1,Interface2 依赖(使用) B类,但是只会用到1,2,3方法
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend2(Interface2 i) {
        i.operation2();
    }

    public void depend3(Interface2 i) {
        i.operation3();
    }
}

class C { // C 类通过接口Interface1,Interface3 依赖(使用) D类,但是只会用到1,4,5方法
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend4(Interface3 i) {
        i.operation4();
    }

    public void depend5(Interface3 i) {
        i.operation5();
    }
}

总结

接口是设计时对外部设定的"契约",通过分散定义多个接口,可以预防外来变更的扩散(例如有一天,接口I1需要加一个方法6,而B、D类都用不到,但是他们还要实现),提高系统的灵活性和可维护性。

注意点:

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

运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

1.3 依赖倒转原则

定义

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

上面的定义不难理解,主要包含两次意思:

1)高层模块不应该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。换言之,模块间的依赖是通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

2)接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。这一点其实不用多说,很好理解,“面向接口编程”思想正是这点的最好体现。

问题由来 & 如何判断哪个是高层组件

类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决方案

将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多

在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

依赖倒置原则的核心思想是面向接口编程

例子

我们用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。

场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:

class Book {
    public String getContent() {
        return "很久很久以前有一个阿拉伯的故事......";
    }
}

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

public class Client {
    public static void main(String[] args) {
        Mother mother = new Mother();
        mother.narrate(new Book());
    }
}

运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:

class Newspaper {
    public String getContent() {
        return "林书豪38+7领导尼克斯击败湖人......";
    }
}

这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。

所以,我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

interface IReader {
    String getContent();
}

Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader 接口,这样就符合依赖倒置原则了,代码修改为:

interface IReader {
    String getContent();
}

class Book implements IReader {
    public String getContent() {
        return "很久很久以前有一个阿拉伯的故事......";
    }
}

class Newspaper implements IReader {
    public String getContent() {
        return "林书豪38+7领导尼克斯击败湖人......";
    }
}

class Mother {
    public void narrate(IReader reader) {
        System.out.println("妈妈开始讲故事");
        System.out.println(reader.getContent());
    }
}

public class Client {
    public static void main(String[] args) {
        Mother mother = new Mother();
        mother.narrate(new Book());
        mother.narrate(new Newspaper());
    }
}

这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

依赖倒置,究竟倒置在哪?

在传统的应用架构中,低层次的组件设计用于被高层次的组件使用,这一点提供了逐步的构建一个复杂系统的可能。在这种结构下,高层次的组件直接依赖于低层次的组件去实现一些任务。这种对于低层次组件的依赖限制了高层次组件被重用的可行性。

依赖反转原则的目的是把高层次组件从对低层次组件的依赖中解耦出来,这样使得重用不同层级的组件实现变得可能。把高层组件和低层组件划分到不同的包/库(在这些包/库中拥有定义了高层组件所必须的行为和服务的接口,并且存在高层组件的包)中的方式促进了这种解耦。由于低层组件是对高层组件接口的具体实现,因此低层组件包的编译是依赖于高层组件的,这颠倒了传统的依赖关系。众多的设计模式,比如插件,服务定位器或者依赖反转,则被用来在运行时把指定的低层组件实现提供给高层组件。 —— WIKI

设计模式总结(上)插图2

图1中,高层对象A依赖于底层对象B的实现;图2中,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。

在上面的例子中,Mother就是高层组件,Book、Newspaper等是低层组件,但是发现每次想要更换一种读物就需要修改Mother类的代码,这样是很不妥的,于是高层组件就定义了一个抽象IReader(规范),让低层组件进行实现(由高层来规定抽象层的接口规范),高层组件Mother只需要依赖IReader进行代码的书写,而具体的细节由它的子类进行实现。从原本的Mother类依赖Book类(高层组件依赖底层组件)变为了Book类依赖于IReader类(低层组件依赖于高层组件)

不懂的话,再看看这个吧

思维方式倒置

还是以上面的为例,最开始的版本,我们会先写Book类,然后再写Mother类(先写低层组件再写高层组件),而有了依赖倒置原则,我们会先写IReader这个抽象,然后就可以写Mother类,最后再写IReader的实现(先写高层组件再写底层组件)。

依赖注入(DI)的3种方式

由于高层组件是依赖于抽象编写的代码,所以我们就需要采用一种方法来将具体的实现注入进高层组件,一般有以下3种方式:

  1. 构造器
  2. 方法参数(上面例子采用的就是这种方式)
  3. setter方法

扩展:Spring的依赖注入,Spring启动时会将全部的对象初始化出来并且注入已经创建的部分实例,之后再循环这些对象,查看他们所需的依赖在容器中是否存在,如果存在,则将其注入。

总结

在实际编程中,我们一般需要做到如下3点:

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

1.4 里式替换原则

定义

如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。

是不是看不懂,通俗解释就是:所有引用基类的地方必须能透明地使用其子类的对象

问题由来

有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障

解决方案

当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法

为啥不要重载?因为你重载的时候很有可能一不小心变成重写或者调用错误。例如:

class P {
    public void func(Integer a) {
        System.out.println("parent");
    }
}

class C {
    public void func(Object a) {
        System.out.println("children");
    }
}

public class Client {
    public static void main(String[] args) {
        // 本来想调用父类方法,却调用了子类的
        new C().func(1);
    }
}

继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性(打破了封装)程序的可移植性降低增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。

例子

我们需要完成一个两数相减的功能,由类A来负责。

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

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

后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:1. 两数相减 2. 两数相加,然后再加100。

由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:

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

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

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

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

我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。

如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

总结

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

细化来说:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。(这个上面说了)
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。(否则会出现编译异常)
abstract class P {
    abstract List<String> func();
}

class C extends P{

    // 提示:... attempting to use incompatible return type
    @Override
    Collection<String> func() {
        return null;
    }
}

https://segmentfault.com/a/1190000016509799

1.5 开闭原则

定义

一个软件实体如类、模块和函数应该对扩展开放(针对提供方),对修改关闭(针对使用方)

问题由来

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

解决方案

其实满足其他设计原则设计出来的程序,一定是满足开闭原则的,开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节

例子

我们可以拿“依赖倒置”的例子来看,这个例子就是满足开闭原则的,当增加一种读物的时候,不需要改Mother类的任何代码,只需要增加一个IReader的实现即可。

我们把它改成不是开闭原则的:

abstract class IReader {
    int type;
}

class Book extends IReader {
    public Book() {
        type = 1;
    }
}

class Newspaper extends IReader {
    public Newspaper() {
        type = 2;
    }
}

class Mother {
    public void narrate(IReader reader) {
        if (reader.type == 1) {
            System.out.println("很久很久以前有一个阿拉伯的故事......");
        } else if (reader.type == 2) {
            System.out.println("林书豪38+7领导尼克斯击败湖人......");
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Mother mother = new Mother();
        mother.narrate(new Book());
        mother.narrate(new Newspaper());
    }
}

这样,每增加一种读物,就需要修改Mother类中的方法。

总结

一个软件实体如类、模块和函数应该对扩展开放(针对提供方),对修改关闭(针对使用方)。

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

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

1.6 迪米特法则(最少知道原则)

定义

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

自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

问题由来

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

解决方案

对任何对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:

  • 对象本身
  • 作为方法入参传入的对象
  • 方法中new出来的对象的方法
  • 对象的成员变量

例子

人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。 —— WIKI

class People {
    void walkTheDog(Dog dog) {
        dog.leg.walk();
    }
}

class Dog {
    Leg leg;
}

class Leg {
    void walk() {
        System.out.println("走。。。");
    }
}

上面这样写就违背了迪米特法则,因为它直接使用了Dog中的Leg对象进行操作。

class People {
    void walkTheDog(Dog dog) {
        //dog.leg.walk();
        dog.walk();
    }
}

class Dog {
    Leg leg;

    public void walk() {
        leg.walk();
    }
}

class Leg {
    void walk() {
        System.out.println("走。。。");
    }
}

总结

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

1.7 设计原则核心思想

  1. 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  2. 针对抽象编程,而不是针对实现编程。
  3. 为了交互对象之间的松耦合设计而努力

二、UML类图

2.1 UML简介

UML(Unified modeling language UML,统一建模语言),是一种用于软件系统分析和设计的语言工具,它用于帮助软件开发人员进行思考和记录思路的工具。

UML 本身是一套符号的规定,就像数学符号和化学符号一样,这些符号用于描述软件模型中的各个元素和他们之间的关系,比如类、接口、实现、泛化、依赖、组合、聚合等。

设计模式总结(上)插图3

2.2 UML类图

UML类图分为:

  1. 用例图
  2. 静态结构图:类图、对象图、包图、组件图、部署图
  3. 动态行为图:交互图(时序图与协作图)、状态图、活动图

其中类图是用来描述类与类之间的关系的,也是UML图中最核心的。

2.3 类图

类之间的关系有:依赖、泛化(继承)、实现、关联、聚合与组合。

2.3.1 依赖关系

只要是在类中用到了对方,那么他们之间就存在依赖关系。

public class PersonServiceBean {
    private PersonDao personDao;

    public void save(Person person) {
    }

    public IDCard getIDCard(Integer personid) {
        return null;
    }

    public void modify() {
        Department department = new Department();
    }

}

class PersonDao {
}

class Person {
}

class IDCard {
}

class Department {
}
设计模式总结(上)插图4

2.3.2 泛化关系

泛化关系实际上就是继承关系,他是依赖关系的特例

abstract class DaoSupport {
    public void save(Object entity) {
    }

    public void delete(Object id) {
    }
}

public class PersonServiceBean extends DaoSupport {
}
设计模式总结(上)插图5

2.3.3 实现关系

实现关系实际上就是 A 类实现 B 接口,他是依赖关系的特例

interface PersonService {
    void delete(Interger id);
}

public class PersonServiceBean implements PersonService {
    public void delete(Interger id) {
    }
}
设计模式总结(上)插图6

2.3.4 关联关系

关联关系就是类与类之间的联系,他是依赖关系的特例

它具有导航性和多重性。

导航性:双向关系或单向关系

多重性:即一个类包含另一个类的多少份,“1” 表示有且仅有一个,“0...” 表示0个或多个,“0,1” 表示0个或一个,“n,m” 表示n到m个都可以,“m...”表示至少m个。

class Person {
    // 单向关系 + 有且仅有一个(“1”) => 单向一对一关系
    // 如果是List<IDCard> 那就是“0...”
    IDCard card;
}

class IDCard {
}
设计模式总结(上)插图7
class Person {
    // 双向关系 + 有且仅有一个(“1”) => 双向一对一关系
    IDCard card;
}

class IDCard {
    Person person;
}
设计模式总结(上)插图8

2.3.5 聚合关系

聚合关系(Aggregation)表示的是整体和部分的关系,表示整体与部分可以分开。聚合关系是关联关系的特例,所以他也具有关联关系的导航性与多重性

如:一台电脑由键盘(keyboard)、显示器(monitor),鼠标等组成;组成电脑的各个配件是可以从电脑上分离出来的,使用带空心菱形的实线来表示:

class Computer {
    Mouse mouse;
    Monitor monitor;

    void setMouse(Mouse mouse) {
        this.mouse = mouse;
    }

    void setMonitor(Monitor monitor) {
        this.monitor = monitor;
    }
}
设计模式总结(上)插图9

2.3.6 组合关系

组合关系:也是整体与部分的关系,但是整体与部分不可以分开。组合关系是关联关系的特例,所以他也具有关联关系的导航性与多重性

在程序中我们定义实体:Person 与 IDCard、Head, 那么 Head 和 Person 之间的关系就是组合关系,IDCard 和 Person 之间就是聚合关系。

但是如果在程序中 Person 实体中定义了对 IDCard 进行级联删除,即删除 Person 时连同 IDCard 一起删除,那么 IDCard 和 Person 就是组合了。

public class Person{
private IDCard card;
private Head head = new Head();
}
public class IDCard{} public class Head{}
设计模式总结(上)插图10

三、创建类设计模式

3.1 单例模式

定义

确保一个类只有一个实例,并提供一个全局访问点

实现

特点:

  1. 构造器私有化
  2. 自行创建,并且用静态变量保存
  3. 向外提供这个实例
  4. 为强调该对象这是一个单例, 我们可以用final修饰

实现方式:

饿汉式:直接创建对象,不存在线程安全问题

  • 直接实例化饿汉式(简洁直观)
  • 枚举式(最简洁)
  • 静态代码块饿汉式(适合复杂实例化)

懒汉式:延迟创建对象

  • 线程不安全(适用于单线程)
  • 线程安全(适用于多线程)
  • 静态内部类形式(适用于多线程)

使用前后对比

我只演示 “经典饿汉式”、“饿汉枚举式”、“懒汉式双重检查”和“懒汉式静态内部类”。

经典饿汉式

public class Singleton {
    // 自行创建,并且用静态变量保存
    private static Singleton ourInstance = new Singleton();

    // 向外提供这个实例
    public static Singleton getInstance() {
        return ourInstance;
    }

    // 构造器私有
    private Singleton() {
    }
}

这种写法是最经典的写法,而且比较简单,在类装载的时候就完成实例化,从而没有线程同步问题

但是这种写法有一个缺点就是,它在类装载的时候就完成实例化,没有达到LazyLoading的效果。由于导致类装载的原因有很多种,如果从始至终从未使用过这个实例,则会造成内存的浪费

结论:这种单例模式可用,可能造成内存浪费。

与这个方式类似的还有采用静态代码块的方式,这里就不再写了。

枚举式

public enum Singleton {
    INSTANCE;

    public void whateverMethod() {
    }
}

这种写法不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

但是,我们没有办法把它当作一个正常的类来使用,就比如,它没有成员变量,无法组合其他对象进去。

结论:如果满足使用要求,推荐使用。

懒汉式双重检查

public class Singleton {

    // volatile是为了防止指令重排序
    private static volatile Singleton ourInstance;

    public static Singleton getInstance() {
        if (ourInstance == null) {
            synchronized (Singleton.class) {
                if (ourInstance == null) {
                    ourInstance = new Singleton();
                }
            }
        }
        return ourInstance;
    }
}

这种方式是我在工作中最常用的方式,主要它是懒加载的而且线程安全并且编写起来不复杂。

懒汉式静态内部类

/*
 * 在内部类被加载和初始化时,才创建INSTANCE实例对象
 * 静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。
 * 因为是在内部类加载和初始化时,创建的,因此是线程安全的
 */
public class Singleton6 {
    private Singleton6(){
        
    }
    private static class Inner{
        private static final Singleton6 INSTANCE = new Singleton6();
    }
    
    public static Singleton6 getInstance(){
        return Inner.INSTANCE;
    }
}

这种方式,只不过是把第一种方式写进了内部类中,这样就解决了第一种方式的问题:

  1. 由于导致类装载的原因有很多种,如果从始至终从未使用过这个实例,则会造成内存的浪费。
  2. 懒加载

何时使用

需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级
对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)

符合6大设计原则的哪些

单一职责 未涉及

接口隔离 未涉及

依赖倒置 未涉及

里式替换 未涉及

开闭原则 未涉及

迪米特法则 未涉及

3.2 简单工厂模式

定义

定义一个创建对象的类,由这个类来封装实例化对象的行为(代码)。

简单工厂模式并不属于 GoF 23 个经典设计模式,但由于它的设计思想很简单,所以在工作中经常会使用到它。

使用前后对比

一个Pizza店,它有一个售卖Pizza的方法,代码如下:

public class Client {
    public static void main(String[] args) {
        PizzaStore pizzaStore = new PizzaStore();
        pizzaStore.orderPizza(1);
    }
}

// pizza店
class PizzaStore {

    // 售卖pizza,type为pizza的种类
    Pizza orderPizza(int type) {
        Pizza pizza;
        
        // 根据类型获取pizza面饼
        if (type == 1) {
            pizza = new CheesePizza();
        } else if (type == 2) {
            pizza = new VegetablePizza();
        } else {
            return null;
        }

        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
}

// Pizza的抽象
interface Pizza {

    // 烘烤
    void bake();

    // 切割
    void cut();

    // 包装
    void box();
}

// 芝士pizza
class CheesePizza implements Pizza {

    @Override
    public void bake() {

    }

    @Override
    public void cut() {

    }

    @Override
    public void box() {

    }
}

// 蔬菜pizza
class VegetablePizza implements Pizza {

    @Override
    public void bake() {

    }

    @Override
    public void cut() {

    }

    @Override
    public void box() {

    }
}

通过上面的代码,我们发现PizzaStore与Pizza、CheesePizza、VegetablePizza具有很强的耦合性,我们设想一下,如果现在不止一个PizzaStore而是一个连锁店,所有的披萨店是不是都需要把这份代码重新写一遍;当新增了一份“新奥尔良披萨”,就需要把所有连锁店的代码都修改一遍。

所以,我们根据设计原则:“封装变化”,将代码修改一下。

// pizza店
class PizzaStore {

    private PizzaFactory pizzaFactory = new PizzaFactory();

    // 售卖pizza,type为pizza的种类
    Pizza orderPizza(int type) {

        // 根据类型获取pizza面饼
        Pizza pizza = pizzaFactory.createPizza(type);

        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}

// pizza工厂,日后新增一种pizza,只需要改这个工厂类就好了
class PizzaFactory {

    Pizza createPizza(int type) {
        Pizza pizza = null;

        if (type == 1) {
            pizza = new CheesePizza();
        } else if (type == 2) {
            pizza = new VegetablePizza();
        }

        return pizza;
    }
}

这样修改,就可以解决我们上面的问题了,PizzaStore与具体的Pizza实现解耦了,在日后新增、删除Pizza就不需要改PizzaStore的代码了。

设计模式总结(上)插图11

何时使用

其实,简单工厂模式就是对封装的概念进行了实现,将多个地方使用到的代码单独封装成一个类。

所以,在多个地方使用到的代码,而且会动态变化,向这种地方就可以使用工厂模式。

扩展:简单工厂中的方法可以写成静态的。缺点,无法通过继承来改变创建方法的行为;而且静态工厂与客户端代码耦合起来了。

符合6大设计原则的哪些

单一职责 符合,工厂类只负责对象的创建

接口隔离 未涉及

依赖倒置 未涉及

里式替换 未涉及

开闭原则 违背,当新增一种Pizza,要修改工厂类的代码

迪米特法则 未涉及

3.3 工厂方法模式

定义

定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行

使用前后对比

https://www.cnblogs.com/gcdd/p/12292128.html

设计模式总结(上)插图12

何时使用

创建对象需要大量重复的代码。

设计模式总结(上)插图13

符合6大设计原则的哪些

单一职责 符合,工厂类只负责生产对象

接口隔离 符合,它实现的接口为最小接口

依赖倒置 符合,高低层组件都是面向抽象编程的

里式替换 未涉及

开闭原则 符合,新增一个产品,只需要再增加一个工厂类就行了

迪米特法则 未涉及

3.4 抽象工厂模式

定义

工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”

使用前后对比

例子看书把。

设计模式总结(上)插图14
类图

何时使用

系统中有多于一个的产品族,而每次只使用其中某一产品族。

属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。

符合6大设计原则的哪些

单一职责 符合,只负责生成对象

接口隔离 符合,实现的是最小接口

依赖倒置 符合,面对的抽象编程

里式替换 未涉及

开闭原则 符合,新增一类产品族,只需要增加一个类

迪米特法则 未涉及

工厂总结

工厂方法模式和抽象工厂模式都可以搭配着简单工厂一起使用。

工厂就是为了将对象的创建,从代码中提取出去,避免一改到处找。

工厂是只提供一个产品,抽象工厂是提供一堆相同族的产品

3.5 原型模式

定义

原型模式允许我们通过复制现有的实例(Java中的clone方法)来创建新的实例。

Spring的prototype域就使用了原型模式。

使用前后对比

class Client {
    public static void main(String args[]) {
        WeeklyLog log_previous = new WeeklyLog(); //创建原型对象
        log_previous.setName("张无忌");
        log_previous.setDate("第12周");
        log_previous.setContent("这周工作很忙,每天加班!");

        WeeklyLog log_new;
        log_new = log_previous.clone(); //调用克隆方法创建克隆对象
        log_new.setDate("第13周");
    }
}


//工作周报WeeklyLog:具体原型类,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class WeeklyLog implements Cloneable {
    private String name;
    private String date;
    private String content;

    @Override
    protected WeeklyLog clone() {
        try {
            return (WeeklyLog) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

深/浅拷贝

class Client {
    public static void main(String args[]) {
        WeeklyLog log_previous = new WeeklyLog(); //创建原型对象
        log_previous.setName("张无忌");
        log_previous.setDate("第12周");
        log_previous.setContent("这周工作很忙,每天加班!");
        log_previous.setAttachment(new Attachment("变象怪桀.avi"));

        WeeklyLog log_new;
        log_new = log_previous.clone(); //调用克隆方法创建克隆对象
        log_new.setDate("第13周");

        System.out.println(log_previous.getAttachment() == log_new.getAttachment()); // true
    }
}


//工作周报WeeklyLog:具体原型类,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class WeeklyLog implements Cloneable {
    private String name;
    private String date;
    private String content;

    // 附件
    private Attachment attachment;

Java中的clone方法默认是浅拷贝,如果log_new修改了附件,那么log_previous也会受到影响。

我猜测Object#clone是这样写的:

Object clone() {
    Object o = new Object();
    List<Field> fields = findAllFiled(this);
    fields.for() {
        field.set(o);
    }
}

主要有两种方式实现深copy:重写clone方法或采用序列化技术。

重写clone方法
class WeeklyLog implements Cloneable {
    private String name;
    private String date;
    private String content;

    // 附件
    private Attachment attachment;

    @Override
    protected WeeklyLog clone() {
        try {
            WeeklyLog weeklyLog = (WeeklyLog) super.clone();
            weeklyLog.setAttachment(attachment.clone());
            return weeklyLog;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

//附件类
class Attachment implements Serializable,Cloneable {
    private String name; //附件名

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Attachment(String name) {
        this.name = name;
    }

    @Override
    public Attachment clone() throws CloneNotSupportedException {
        return (Attachment) super.clone();
    }
}
序列化

何时使用

当创建给定类的实例的过程很昂贵或很复杂时

当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。

如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。

注:原型可以和工厂模式配合使用

符合6大设计原则的哪些

单一职责 未涉及

接口隔离 未涉及

依赖倒置 由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类 写在配置文件中,增加或减少产品类对原有系统都没有任何影响。

里式替换 未涉及

开闭原则 违背了,需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。

迪米特法则 违背了,需要重写Object的方法

3.6 建造者模式

定义

建造者模式的核心在于如何一步步构建一个包含多个组成部件的完整对象使用相同的构建过程构建不同的产品,在软件开发中,如果我们需要创建复杂对象并希望系统具备很好的灵活性和可扩展性可以考虑使用建造者模式。

设计模式总结(上)插图15

在建造者模式结构图中包含如下几个角色:

  • Builder(抽象建造者):它为创建一个产品 Product 对象的各个部件指定抽象接口,在该接口中一般声明两类方法一类方法是 buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是 getResult(),它们用于返回复杂对象。Builder 既可以是抽象类,也可以是接口。
  • ConcreteBuilder(具体建造者):它实现了 Builder 接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。
  • Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程
  • Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其 construct() 建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者 Setter 方法将该对象传入指挥者类中。

我觉得“指挥者”类对于客户端来说就相当于一个工厂,只不过这个工厂封装了对象构建的步骤而且对于所有产品的构建过程相同。

我们客户端可以直接使用“指挥者”类,如果想要建造另一种产品,那么只需要把“指挥者”类中的builder通过配置文件等方式修改就可以了。

使用前后对比

public class Client {
    public static void main(String[] args) {
        AbstractHouse house = new CommonHouse();
        house.build();
    }

}

abstract class AbstractHouse {

    //将建造的流程写好, 抽象的方法
    public abstract void buildBasic();

    public abstract void buildWalls();

    public abstract void roofed();

    public void build() {
        buildBasic();
        buildWalls();
        roofed();
    }
}

class CommonHouse extends AbstractHouse {

    // ...一些字段

    @Override
    public void buildBasic() {
        System.out.println(" 普通房子打地基5米 ");
    }

    @Override
    public void buildWalls() {
        System.out.println(" 普通房子砌墙10cm ");
    }

    @Override
    public void roofed() {
        System.out.println(" 普通房子屋顶 ");
    }
}

这种方式就将产品本身和产品的创建过程耦合在一起,违背了“单一职责原则”,那我们就可以将建造的步骤和过程提取成两个类,如下代码:

public class Client {
    public static void main(String[] args) {
        HouseDirector houseDirector = new HouseDirector();
        House house = houseDirector.constructHouse();
    }
    
}

//指挥者,这里去指定制作流程,返回产品
class HouseDirector {

    // 此处可以通过配置文件修改
    HouseBuilder houseBuilder = new CommonHouseBuilder();

    //如何处理建造房子的流程,交给指挥者
    public House constructHouse() {
        houseBuilder.buildBasic();
        houseBuilder.buildWalls();
        houseBuilder.roofed();
        return houseBuilder.buildHouse();
    }
}

// 抽象的建造者
abstract class HouseBuilder {

    protected House house = new House();

    //将建造的流程写好, 抽象的方法
    public abstract void buildBasic();

    public abstract void buildWalls();

    public abstract void roofed();

    //建造房子好, 将产品(房子) 返回
    public House buildHouse() {
        return house;
    }
}

class CommonHouseBuilder extends HouseBuilder {

    @Override
    public void buildBasic() {
        System.out.println(" 普通房子打地基5米 ");
    }

    @Override
    public void buildWalls() {
        System.out.println(" 普通房子砌墙10cm ");
    }

    @Override
    public void roofed() {
        System.out.println(" 普通房子屋顶 ");
    }
}

//产品->Product
class House {
}
设计模式总结(上)插图16

注:在有些情况下,为了简化系统结构,可以将 Director 和抽象建造者 Builder 进行合并。

总结

  • 优点:

在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。

扩展方便,只需要增加新产品的建造者类就行了。

  • 缺点:

如果产品之间建造方式差异很大,就不适合使用

  • 何时使用:
  1. 生成的产品非常复杂,需要好多步骤,这些产品对象通常包含多个成员属性。
  2. 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
  3. 隔离复杂对象的创建和使用,并使相同的创建过程可以创建不同的产品。

符合6大设计原则的哪些

单一职责 符合,产品的创建过程和产品独立

接口隔离 符合

依赖倒置 符合,面向抽象编程

里式替换 未涉及

开闭原则 符合,在建造者模式中,客户端只需实例化指挥者类,指挥者类针对抽象建造者编程,客户端根据需要传入具体的建造者类型,指挥者将指导具体建造者一步一步构造一个完整的产品(逐步调用具体建造者的 buildX() 方法),相同的构造过程可以创建完全不同的产品。如果需要修改建造的产品,只需要修改配置文件,更换具体产品的建造者类即可;如果需要增加新产品,可以增加一个新的具体产品建造者类作为抽象产品建造者的子类,再修改配置文件即可,原有代码无须修改,完全符合“开闭原则”。

迪米特法则 未涉及

四、结构型模式

结构型模式的本质在于:描述类类、类对象之间的结合方式,实现上基本上都是尽量利用组合/关联关系替代继承关系。

4.1 适配器模式

定义

将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

设计模式总结(上)插图17

Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。

Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在类适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。

Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

使用前后对比

例子我不想写了

https://juejin.im/post/5ba28986f265da0abc2b6084#heading-5

看SpringMVC的例子,自己跟一下源码其实很简单。

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

这个方法mappedHandler.getHandler()的返回值为Object,代表着他会返回多种不同类型的对象,由于这些对象没有实现同一个接口,需要调用的方法名也不一样,在doDispatch方法中不能向下面这样写吧:

Object o = mappedHandler.getHandler();
if (o instanceof Controller) {
    o.handle();
} else if (o instanceof HttpRequestHandle) {
    o.handleRequest();
}

这样写的话,日后添加一种组件,就需要改这部分代码,违背了“开闭原则”,所以Spring提供了一个HandlerAdapter接口,根据不同的Handler类型都提供一个实现,从而使得在代码中直接调用handlerAdapter的方法即可。

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

注:其实我觉得这个地方主要用了命令模式。

总结

优点

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  2. 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上 增加新的适配器类,完全符合“开闭原则”。
  3. 对象适配器可以把多个不同的适配者适配到同一个目标;

何时使用

系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要

符合6大设计原则的哪些

单一职责 符合

接口隔离 符合

依赖倒置 符合

里式替换 对象适配器符合,可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。

开闭原则 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

迪米特法则 未涉及

4.2 桥接模式

在正式介绍桥接模式之前,我先跟大家谈谈两种常见文具的区别,它们是毛笔和蜡笔。假如我们需要大中小 3 种型号的画笔,能够绘制 12 种不同的颜色,如果使用蜡笔,需要准备 3×12 = 36 支,但如果使用毛笔的话,只需要提供 3 种型号的毛笔,外加 12 个颜料盒即可涉及到的对象个数仅为 3 + 12 = 15,远小于36,却能实现与 36 支蜡笔同样的功能。如果增加一种新型号的画笔,并且也需要具有 12 种颜色,对应的蜡笔需增加 12 支,而毛笔只需增加一支。为什么会这样呢?通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两个不同的变化原因)融合在一起,无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛笔中,颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性,而毛笔很好地将二者解耦,使用起来非常灵 活,扩展也更为方便。

定义

它把事物对象和其具体行为、具体特征分离开来,使它们可以各自独立的变化。

事物对象仅是一个抽象的概念。如“圆形”、“三角形”归于抽象的“形状”之下,而“画圆”、“画三角”归于实现行为的“画图”类之下,然后由“形状”调用“画图”。

设计模式总结(上)插图18
  • Abstraction
    • 定义抽象的接口
    • 该接口包含实现具体行为、具体特征的Implementor接口
  • Refined Abstraction
    • 抽象接口Abstraction的子类,依旧是一个抽象的事物名
  • Implementor
    • 定义具体行为、具体特征的应用接口
  • ConcreteImplementor
    • 实现Implementor接口

注:其实这个设计模式也是封装变化的体现

使用前后对比

使用前:类膨胀

设计模式总结(上)插图19
类似这样

使用后:

// 毛笔
abstract class WritingBrush {

    // 绘画
    abstract void paint();
}

// 粗毛笔
class ThickWritingBrush extends WritingBrush {

    private Color color;

    @Override
    void paint() {
        System.out.println("粗毛笔正在画" + color.color());
    }

    public ThickWritingBrush(Color color) {
        this.color = color;
    }
}

// 细毛笔等省略。。。

interface Color {

    String color();
}

class Black implements Color {

    @Override
    public String color() {
        return "黑色";
    }
}

// 其他颜色省略。。。

/**
 * "Client"
 */
class BridgePattern {
    public static void main(String[] args) {
        WritingBrush writingBrush = new ThickWritingBrush(new Black());
        writingBrush.paint();
    }
}

在这里,事物对象的抽象是毛笔,具体的事物是粗毛笔、细毛笔等,而颜色为特征抽象,具体的颜色为特征。

在这里,如果paint()方法的实现仅仅只是简单的调用一下组合的对象,我们其实可以将其写在抽象父类中,那我们其实也可以把“尺寸”这个特征也独立出来,成为一个特征的抽象,将抽象父类改为不抽象的,代码如下:

// 毛笔
class WritingBrush {

    Model model;

    Color color;

    // 绘画
    void paint() {
        System.out.println(model.model() + "毛笔正在画" + color.color());
    }

    public WritingBrush(Model model, Color color) {
        this.model = model;
        this.color = color;
    }
}

// 型号
interface Model {
    String model();
}

class Thick implements Model {

    @Override
    public String model() {
        return "粗";
    }
}


interface Color {

    String color();
}

class Black implements Color {

    @Override
    public String color() {
        return "黑色";
    }
}

// 其他颜色省略。。。

/**
 * "Client"
 */
class BridgePattern {
    public static void main(String[] args) {
        new WritingBrush(new Thick(), new Black()).paint();
    }
}

在审查系统中,模型计算任务部分就是采用这种方式进行编写的,但是为了注入Spring容器简单,所以还是为每一种任务(也就是xx型号xx颜色毛笔)实现了一个具体的实现类(也就是说,我还是写了36种毛笔),虽然类显得比较多,但是很清晰。

JDBC

设计模式总结(上)插图20

它只有一个纬度变化(Driver),但是它将抽象(DM)和实现(Driver)分离开来了。

总结

优点

  1. 分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。
  2. 在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
  3. 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。

何时使用:

  1. 在软件开发中如果一个类或一个系统有多个变化维度时,都可以尝试使用桥接模式对其进行设计。
  2. 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

符合6大设计原则的哪些

单一职责 符合

接口隔离 未涉及

依赖倒置 符合

里式替换 符合

开闭原则 符合,当增加新的毛笔形状或者颜色时,原有系统无须做任何修改,只需增加一个对应的扩充抽象类或具体实现类即可,系统具有较好的可扩展性,完全符合“开闭原则”。

迪米特法则 未涉及

4.3 装饰者模式

定义

动态的将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

设计模式总结(上)插图21

使用前后对比

星巴克咖啡订单项目(咖啡馆):

  1. 咖啡种类/单品咖啡:Espresso(意大利浓咖啡)、ShortBlack、LongBlack(美式咖啡)、Decaf(无因咖啡)
  2. 调料:Milk、Soy(豆浆)、Chocolate
  3. 要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便
  4. 使用OO的来计算不同种类咖啡的费用:客户可以点单品咖啡,也可以单品咖啡+调料组合。

方案一:各种继承

方案二:采用桥接模式,在Coffee类中保持调料的引用;但是如果日后增加一种调料,改动量较大,且违反”开闭原则“。

方案三:采用装饰者模式

// 饮料父类
abstract class Beverage {
    private String description;

    public String getDescription() {
        return description;
    }

    public Beverage() {
    }

    public Beverage(String description) {
        this.description = description;
    }

    /**
     * 获取商品总价的方法
     */
    public abstract double cost();
}

class Mocha extends CondimentDecorator {

    public Mocha(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ",摩卡";
    }

    /**
     * 获取商品总价的方法
     */
    @Override
    public double cost() {
        return beverage.cost() + 0.2;
    }

}

// 装饰者父类
abstract class CondimentDecorator extends Beverage {

    protected Beverage beverage;

    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }

    // 子类可以将父类的方法变成抽象方法
    public abstract String getDescription();
}

class Espresso extends Beverage {

    /**
     * 获取商品总价的方法
     */
    @Override
    public double cost() {
        return 1.99;
    }

    public Espresso() {
        super("浓缩咖啡");
    }
}


public class Client {

    public static void main(String[] args) {
        // 摩卡咖啡的描述和价格
        Beverage beverage = new Espresso();
        beverage = new Mocha(beverage);

        System.out.println("beverage.getDescription() = " + beverage.getDescription());
        System.out.println("beverage.cost() = " + beverage.cost());
    }
}

总结

优点

  1. 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
  2. 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
  3. 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。

缺点

  1. 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
  2. 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。

何时使用

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  2. 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:
    第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;
    第二类是因为类已定义为不能被继承(如 Java 语言中的 final 类)。

符合6大设计原则的哪些

单一职责 未涉及

接口隔离 未涉及

依赖倒置 未涉及

里式替换 未涉及

开闭原则 符合,具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。

迪米特法则 未涉及

4.4 组合模式

定义

将对象组合成树形结构来表现“整体/部分”层次结构。

组合可以让客户以一致的方式处理个别对象以及对象组合。

设计模式总结(上)插图22
  • Component (抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
  • Leaf (叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
  • Composite (容器构件):它在组合结构中表示容器节点对象容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。

使用前后对比

看书吧

总结

优点

  1. 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  2. 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  3. 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。

何时使用

  1. 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。

符合6大设计原则的哪些

单一职责 不符合,抽象类负责了叶子和容器组件的行为。

接口隔离 符合

依赖倒置 符合

里式替换 违背

开闭原则 符合,在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。

迪米特法则 符合

4.5 外观模式

定义

外观模式是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。

设计模式总结(上)插图23
  • 外观类(Facade): 为调用端提供统一的调用接口,外观类知道哪些子系统负责处理请求,从而将调用端的请求代理给适当子系统对象
  • 调用者(Client): 外观接口的调用者
  • 子系统的集合:指模块或者子系统,处理Facade对象指派的任务,他是功能的实际提供者

总结

  1. 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
  2. 外观模式使客户端与子系统解耦,让子系统内部的模块更易维护和扩展
  3. 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
  4. 当系统需要进行分层设计时,可以考虑使用Facade模式
  5. 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个 Facade 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facade 类交互,提高复用性
  6. 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的。

4.6 享元模式

定义

享元模式( Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。

不懂啥是细粒度。

常用于系统底层开发,解决系统的性能问题,例如:数据库连接池、Integer缓存池、String常量池。

享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率。

设计模式总结(上)插图24

享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类。

  • Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
    • FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。

内部状态和外部状态

内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变

外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。

比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多一点,所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化的,所以棋子坐标就是棋子的外部状态。

例子

// 网站工厂类,根据需要返回压一个网站
class WebSiteFactory {


    //集合, 充当池的作用
    private HashMap<String, ConcreteWebSite> pool = new HashMap<>();

    //根据网站的类型,返回一个网站, 如果没有就创建一个网站,并放入到池中,并返回
    public WebSite getWebSiteCategory(String type) {
        if (!pool.containsKey(type)) {
            //就创建一个网站,并放入到池中
            pool.put(type, new ConcreteWebSite(type));
        }

        return (WebSite) pool.get(type);
    }

    //获取网站分类的总数 (池中有多少个网站类型)
    public int getWebSiteCount() {
        return pool.size();
    }
}

abstract class WebSite {

    public abstract void use(User user);//抽象方法
}

//具体网站
class ConcreteWebSite extends WebSite {

    //共享的部分,内部状态
    private String type; //网站发布的形式(类型)


    //构造器
    public ConcreteWebSite(String type) {
        this.type = type;
    }


    @Override
    public void use(User user) {
        System.out.println("网站的发布形式为:" + type + " 在使用中 .. 使用者是" + user.getName());
    }
}

public class Client {

    public static void main(String[] args) {
        // 创建一个工厂类
        WebSiteFactory factory = new WebSiteFactory();

        // 客户要一个以新闻形式发布的网站
        WebSite webSite1 = factory.getWebSiteCategory("新闻");


        webSite1.use(new User("tom"));

        // 客户要一个以博客形式发布的网站
        WebSite webSite2 = factory.getWebSiteCategory("博客");

        webSite2.use(new User("jack"));

        // 客户要一个以博客形式发布的网站
        WebSite webSite3 = factory.getWebSiteCategory("博客");

        webSite3.use(new User("smith"));

        // 客户要一个以博客形式发布的网站
        WebSite webSite4 = factory.getWebSiteCategory("博客");

        webSite4.use(new User("king"));

        System.out.println("网站的分类共=" + factory.getWebSiteCount());
    }

}

这个例子中,网站的类型就是内部状态,使用网站的用户就是外部状态。

总结

优点

(1) 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。

(2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

缺点

(1) 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。

(2) 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。

适用场景

(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。

(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。

(3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

4.7 代理模式

定义

代理模式可以为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象

静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类

设计模式总结(上)插图25
interface ITeacherDao {

    void teach(); // 授课的方法
}

class TeacherDao implements ITeacherDao {

    @Override
    public void teach() {
        System.out.println(" 老师授课中  。。。。。");
    }
}

//代理对象,静态代理
class TeacherDaoProxy implements ITeacherDao {

    private ITeacherDao target; // 目标对象,通过接口来聚合

    //构造器
    public TeacherDaoProxy(ITeacherDao target) {
        this.target = target;
    }

    @Override
    public void teach() {
        System.out.println("开始代理  完成某些操作。。。。。 ");//方法
        target.teach();
        System.out.println("提交。。。。。");//方法
    }
}


public class Client {

    public static void main(String[] args) {
        //创建目标对象(被代理对象)
        TeacherDao teacherDao = new TeacherDao();

        //创建代理对象, 同时将被代理对象传递给代理对象
        TeacherDaoProxy teacherDaoProxy = new TeacherDaoProxy(teacherDao);

        //通过代理对象,调用到被代理对象的方法
        //即:执行的是代理对象的方法,代理对象再去调用目标对象的方法
        teacherDaoProxy.teach();
    }

}

动态代理

静态代理有一个缺点,必须为每一个类写一个代理类,我们可以采用动态代理来实现。

//接口
interface ITeacherDao {

    void teach(); // 授课方法

    void sayHello(String name);
}

class TeacherDao implements ITeacherDao {
    @Override
    public void teach() {
        System.out.println(" 老师授课中.... ");
    }

    @Override
    public void sayHello(String name) {
        System.out.println("hello " + name);
    }
}

class ProxyFactory {

    //维护一个目标对象 , Object
    private Object target;

    //构造器 , 对target 进行初始化
    public ProxyFactory(Object target) {

        this.target = target;
    }

    //给目标对象 生成一个代理对象
    public Object getProxyInstance() {

        //说明
        /*
         *  public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
                                          
            //1. ClassLoader loader : 指定当前目标对象使用的类加载器, 获取加载器的方法固定
            //2. Class<?>[] interfaces: 目标对象实现的接口类型,使用泛型方法确认类型
            //3. InvocationHandler h : 事情处理,执行目标对象的方法时,会触发事情处理器方法, 会把当前执行的目标对象方法作为参数传入
         */
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                (proxy, method, args) -> {
                    System.out.println("JDK代理开始~~");
                    //反射机制调用目标对象的方法
                    Object returnVal = method.invoke(target, args);
                    System.out.println("JDK代理提交");
                    return returnVal;
                });
    }
}

public class Client {

    public static void main(String[] args) {
        //创建目标对象
        ITeacherDao target = new TeacherDao();

        //给目标对象,创建代理对象, 可以转成 ITeacherDao
        ITeacherDao proxyInstance = (ITeacherDao) new ProxyFactory(target).getProxyInstance();

        // proxyInstance=class com.sun.proxy.$Proxy0 内存中动态生成了代理对象
        System.out.println("proxyInstance=" + proxyInstance.getClass());

        //通过代理对象,调用目标对象的方法
        //proxyInstance.teach();

        proxyInstance.sayHello(" tom ");
    }

}

总结

优点:

  1. 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  2. 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。

不同类型的代理模式也具有独特的优点,例如:

(1) 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。(Dubbo)

(2) 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。

(3) 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。(可以用于redis缓存)

(4) 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。(拦截器)

缺点:

  1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。

何时使用:

  1. 当客户端对象需要访问远程主机中的对象时可以使用远程代理。
  2. 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。
  3. 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。
  4. 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。

符合6大设计原则的哪些

单一职责 未涉及

接口隔离 未涉及

依赖倒置 未涉及

里式替换 未涉及

开闭原则 符合,客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。

迪米特法则 未涉及

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 设计模式总结(上)

一个分享Java & Python知识的社区