1. 设计原则

1. SOLID

  • 单一职责(Single Responsibility Principle,SRP)
    • 一个类或者模块只负责完成一个职责(或者功能)
    • 单一职责可以使类也可以是模块,可以把模块看做粗颗粒度的代码块
    • 例子:
      • Spring中BeanFactory(获取bean的基础方法getBean Type Provider等) ParentBeanFactory(获取多Bean的方法)
      • Serialization序列化和反序列化类,可以拆分成序列化类和反序列化类
  • 开闭原则(Open Closed Principle,OCP)
    • 对扩展开放、对修改关闭
    • 添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
    • 例子:Servlet中的init()、Spring中Bean的生命周期回调、
  • 里氏替换原则(Liskov Substitution Principle,LSP)
    • 在程序中,子类能够替换父类对象,并能保证程序的逻辑行为正确。
  • 接口隔离原则(Interface Segregation Principle,ISP)
    • 调用方不应该被强迫依赖它不需要的接口。
    • 职责更单一,复用性更好
    • 例子:Comparable接口、Serializable接口
  • 依赖倒置(Dependency Inversion Principle,DIP)

    • 具体依赖抽象,抽象不依赖具体
    • 例子:Tomcat依赖Servlet规范、Java Web依赖Servlet

      1.2 KISS:尽量保持简单

  • Keep It Simple and Stupid.

  • Keep It Short and Simple.
  • Keep It Simple and Straightforward.

    1.3 YAGNI:You Ain’t Gonna Need It. (你不会需要它)

  • 不要去设计当前用不到的功能;不要去编写当前用不到的代码。

    1.4 DRY :Don’t Repeat Yourself (不要重复你自己)

  • 实现逻辑重复、功能语义重复和代码执行重复。

    1.5 迪米特法则 LOD (最小知识原则,The Least Knowledge Principle。)

  • 高内聚、松耦合:“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。

    • 高内聚:指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
    • 松耦合:类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。
  • 每个模块只应该了解那些与它关系密切的模块的有限知识。或者说,每个模块只和自己的朋友“说话”,不和陌生人“说话”。
  • 不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。

2. 设计模式

  • 设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。

    2.1 创建型

    2.1.1 单例模式

    • Java中实现:
      • 懒汉式:
      1. double-check:先判断是否为空,为空就进入同步代码块,然后再判断是否为空,为空就创建。最后返回实例对象
      2. 直接在方法上加锁:内部判断是否为空,为空就创建,不为空就返回实例对象
      3. 静态内部类:只有静态内部类被使用的时候才会被加载执行创建操作。
      • 饿汉式:直接创建一个static final 的对象
    • 代码示例

      • JDK 中 java.lang.Runtime 类就是一个单例类:饿汉式(使用static final 实现,静态方法getRuntime()获取)

        2.1.2 工厂模式

    • 类型:简单工厂、工厂方法和抽象工厂

      • 简单工厂:工厂类里面用一个静态方法创建对象,然后返回
      • 工厂方法:利用多态,接口定义一个创建方法,然后类实现接口,对应不同类创建的过程。
        • 工厂方法模式比起简单工厂模式更加符合开闭原则。
      • 抽象工厂:一个工厂负责创建多个不同的类
    • 代码示例:

      • JDK Calendar:getInstance() 方法可以根据不同 TimeZone 和 Locale创建不同的 Calendar 子类对象
      • Spring中BeanFactory和ApplicationContext相关类

        2.1.3 建造者模式

    • 用于创建复杂的对象,用Builder类传递参数,在build的时候可以先进行校然后创建对象。可以实现属性校验和对象分离。

    • 代码示例

      • JDK Calendar:内涵Builder的内部类

        2.1.4 原型模式

    • 利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。有以下两种实现方式:

      • 浅拷贝:Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。
      • 深拷贝:
        • 递归拷贝对象属性的实际值
        • 序列化对象,然后反序列化对象

2.2 结构型

2.2.1 代理模式 (Wrapper)

  • 它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
    • 静态代理:需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。
    • 动态代理:我们不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
  • 常用实现方式:
    • 继承(静态代理):通过super调用父类方法实现代理
    • 组合(静态代理):通过传入被代理类然后调用对应方法来实现代理
    • JDK动态代理(依赖反射):
    • Cglib:字节码植入:
  • 代码示例

    • Spring中的AOP
    • JDK动态代理
    • RPC框架:远程代理

      2.2.2 桥接模式(Wrapper)

  • 将抽象和实现解耦,让它们可以独立变化。

  • 一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
    • 通过组合关系来替代继承关系,避免继承层次的指数级爆炸。
  • 桥接就是面向接口编程的集大成者。面向接口编程只是说在系统的某一个功能上将接口和实现解藕,而桥接是详细的分析系统功能,将各个独立的纬度都抽象出来,使用时按需组合。

image.png

2.2.3 装饰器 (Wrapper)

  • 装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
  • 代码示例
    • Collections: 通过 unmodifiableColletion() 静态方法,来创建 UnmodifiableCollection 类对象
    • Java IO 类库
    • Spring应用 Spring 使用到了装饰器模式。TransactionAwareCacheDecorator 增加了对事务的支持,在事务提交、回滚的时候分别对 Cache 的数据进行处理
    • Java IO

image.png
image.png

2.2.4 适配器(Wrapper)

  • 实现方式:类适配器和对象适配器。
    • 类适配器使用继承关系来实现。接口多并且接口大部分相同
    • 对象适配器使用组合关系来实现。接口多并且接口大部分不同
  • 使用场景:
    • 封装有缺陷的接口设计:对外部系统提供的接口进行二次封装,抽象出更好的接口设计
    • 统一多个类的接口设计:功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义;
    • 替换依赖的外部系统:我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动;
    • 兼容老版本接口:版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
    • 适配不同格式的数据:用在不同格式的数据之间的适配。

  • 代码示例
    • Collections:Enumeration 类是适配器类,它适配的是客户端代码(使用 Enumeration 类)和新版本 JDK 中新的迭代器 Iterator 类
    • Spring MVC: dispatcher从handlerMapping里面拿到Handler 然后执行handler拿到结果返回。Spring 定义了统一的接口 HandlerAdapter,并且对每种 Controller 定义了对应的适配器类。这些适配器类包括:AnnotationMethodHandlerAdapter、SimpleControllerHandlerAdapter、SimpleServletHandlerAdapter 等。
    • Slf4j:对不同日志框架(log4j、logback)的接口进行二次封装,适配成统一的 Slf4j 接口定义。 此外 Slf4j 还提供了JCL的反向适配

      2.2.5 代理、桥接、装饰器、适配器四者的差异

      • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
      • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
      • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
      • 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

2.2.6 门面(外观)模式:Facade Design Pattern

  • 门面模式让子系统更加易用
  • 业务使用业务场景:
    • 解决易用性问题:门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。
      • Linux Shell 命令:封装系统调用,提供更加友好、简单的命令
    • 解决性能问题:封装关联接口调用,如A->B->C三个接口依次调用,可以在此基础上再封装一个接口,减少网络调用次数,提升性能。
    • 解决分布式事务问题:
  • 与适配器比较:适配器模式注重的是兼容性,而门面模式注重的是易用性
  • 适配器和门面模式的区别
    • 适配器是做接口转换,解决的是原接口和目标接口不匹配的问题。注重兼容性
    • 门面模式做接口整合,解决的是多接口调用带来的问题。注重易用性

2.2.7 组合模式(Composite Design Pattern,组合模式)

  • 将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者。)可以统一单个对象和组合对象的处理逻辑。
  • 一个示例:“将一组对象(员工和部门)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(部门与子部门的嵌套结构)。组合模式让客户端可以统一单个对象(员工)和组合对象(部门)的处理逻辑(递归遍历)。
  • 代码示例

    • Spring中的应用Cache:组合模式主要应用在能表示成树形结构的一组数据上。树中的结点分为叶子节点和中间节点两类。对应到 Spring 源码,EhCacheManager、SimpleCacheManager、NoOpCacheManager、RedisCacheManager 等表示叶子节点,CompositeCacheManager 表示中间节点。叶子节点包含的是它所管理的 Cache 对象,中间节点包含的是其他 CacheManager 管理器,既可以是 CompositeCacheManager,也可以是具体的管理器,比如 EhCacheManager、RedisManager 等。

      2.2.7 享元模式

  • 顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象

  • 当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。
  • 享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建好的享元对象,以达到复用的目的。
  • 代码示例

    • Integer : -128~127 之间的整型对象复用;IntegerCache 相当于,我们上一节课中讲的生成享元对象的工厂类,只不过名字不叫 xxxFactory 而已。

      1. //方法一:
      2. -Djava.lang.Integer.IntegerCache.high=255
      3. //方法二:
      4. -XX:AutoBoxCacheMax=255
    • String:常量字符串复用;字符串常量池,它并非事先创建好需要共享的对象,而是在程序的运行期间,根据需要来创建和缓存字符串常量。

  • 单例和享元模式:
    • 在单例模式中,一个类只能创建一个对象,
    • 在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。
    • 对象池、连接池、线程池、享元模式都是为了复用,但是前者不是共享,享元是共享
  • 注意:享元模式的实现 垃圾回收 不友好,内存不会被回收;可达性分析中属于静态常量引用 属于ROOT

    2.3 行为型

    2.3.1 观察者 Observer Design Pattern 【发布订阅模式(Publish-Subscribe Design Pattern)】

  • 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

  • 被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。
  • Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener
  • 而基于消息队列的实现方式,被观察者和观察者解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。
  • 代码示例

    • Google Guava 的 EventBus 框架
    • JDK:java.util.Observable 和 java.util.Observer
    • Spring 中实现的观察者模式:Event 事件(相当于消息)、Listener 监听者(相当于观察者)、Publisher 发送者(相当于被观察者),事件发送到 ApplicationContext 中,然后,ApplicationConext 将消息发送给事先注册好的监听者。

      2.3.2 模板模式 Template Method Design Pattern

  • 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

  • 复用和扩展:复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
  • 代码示例
    • Collections 类 :Collections.sort()
    • Java Servlet、
    • JUnit TestCase、
    • Java InputStream、
    • Java AbstractList
    • Spring 中的应用:Bean创建两步:对象的创建和对象的初始化。利用模板模式,Spring 能让用户定制 Bean 的创建过程。
      1️⃣ - 设计模式 - 图4
      这里的模板模式的实现,并不是标准的抽象类的实现方式,而是有点类似我们前面讲到的 Callback 回调的实现方式,也就是将要执行的函数封装成对象(比如,初始化方法封装成 InitializingBean 对象),传递给模板(BeanFactory)来执行。
      • Spring中带Template后缀的JdbcTemplate、RedisTemplate 等
  • 模板模式 VS 回调

    • 从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。
    • 从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

      2.3.3 策略模式 Strategy Design Pattern

  • 策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。

  • “运行时动态确定”才是策略模式最典型的应用场景。
  • 代码示例

    • Spring 中的应用 AOP: AopProxy 是策略接口,JdkDynamicAopProxy、CglibAopProxy 是两个实现了 AopProxy 接口的策略类; 这里还有一个创建工厂,AopProxyFactory 是一个工厂类接口,DefaultAopProxyFactory 是一个默认的工厂类

      2.3.4 责任链 Chain Of Responsibility Design Pattern

  • 将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

  • 代码示例
    • Java Servlet 中的 Filter
    • Spring MVC 中的 interceptor
    • Dubbo Filter
  • AOP 和 FIlter

    • Filter 可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息; Interceptor 可以拿到你请求的控制器和方法,却拿不到请求方法的参数; Aop 可以拿到方法的参数,但是却拿不到http请求和响应的对象

      2.3.5 状态模式

  • 前置:有限状态机

    • 有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
      • 状态机实现方式一:分支逻辑法—》参照状态转移图,将每一个状态转移,原模原样地直译成代码。
      • 状态机实现方式二:查表法:对状态建维度表,直接查询
      • 状态机实现方式三:状态模式:状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。

        2.3.6 迭代器模式 Iterator Design Pattern 游标模式(Cursor Design Pattern)

  • 迭代器模式,也叫游标模式。它用来遍历集合对象。这里说的“集合对象”,我们也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如,数组、链表、树、图、跳表。

  • 迭代器的优势
    • 迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
    • 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
    • 迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。
  • 使用迭代器的原因

    • 类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。
    • 代码示例
      • Java 中 Iterator 迭代器
        • ArrayList中的modCount(fail-fast 解决方式,让遍历操作直接抛出运行时异常):ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1。当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于 expectedModCount,也就是看,在创建完迭代器之后,modCount 是否改变过。
          • 如果一个ArrayList调用iterator()两次,会生成两个迭代器,互不影响,但是同时去执行做操就会触发checkForComodification()的检查,就会触发ConcurrentModificationException异常。
        • 使用Java中iterator时 它只能删除游标指向的前一个元素,而且一个 next() 函数(返回下一个元素,并将游标后移)之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错。

          2.3.7 访问者模式 Visitor Design Pattern

  • 允许一个或者多个操作应用到一组对象上,解耦操作和对象本身

  • 访问者模式针对的是一组类型不同的对象。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
  • 双分派与单分派

    • Single Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的编译时类型来决定。
    • Double Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时类型来决定。

      2.3.8 备忘录模式 Memento Design Pattern 也称快照模式

  • 在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

    2.3.9 命令模式 Command Design Pattern

  • 命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。

  • 命令模式的主要作用和应用场景:用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,
  • 策略模式与命令模式

    • 策略模式中,不同的策略具有相同的目的、不同的实现、互相之间可以替换。比如,BubbleSort、SelectionSort 都是为了实现排序的,只不过一个是用冒泡排序算法来实现的,另一个是用选择排序算法来实现的。
    • 在命令模式中,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相之间不可替换。

      2.3.10 解释器模式 Interpreter Design Pattern

  • 解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

  • 代码示例
    • Spring中的 SpEL,定义的语法规则,然后解析

2.3.11 中介模式 Mediator Design Pattern

  • 中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
  • 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。、

image.png

总结

image.png

创建型

  • 创建型设计模式包括:单例模式、工厂模式、建造者模式、原型模式。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。