面向对象设计的六大原则

单一职责原则

  • 原则定义:一个类应该只负责一个职责。

  • 原则也适用于方法,反模式如下

    • 方法中if else语句很多
    • 一个类依赖了很多其它的类
    • 方法根据输入值执行不同的任务
  • 实践方法:高内聚,低耦合。

开闭原则

  • 原则定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
  • 实践方法:一个类一旦开发完成,后续增加新的功能就不应该通过修改这个类来完成,而是通过继承,增加新的类。(并不绝对,应该完全重构还是得重构)

里氏替换原则

  • 原则定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
  • 实践方法:面向抽象(接口)编程,尽量避免重写父类的方法。

依赖倒置原则

  • 原则定义:抽象不应该依赖于细节,细节应当依赖于抽象。
  • 声明方法参数的类型,实例变量的类型,方法的返回值类型,类型强制转换等等场景,尽量依赖抽象。

接口隔离原则

  • 原则定义:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
  • 实践方法:接口中的方法需要功能统一。

迪米特原则

又称最少知识原则

  • 原则定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
  • 实践方法:“隐藏自己,做好清理”,利用封装,将不必要暴露的东西隐藏起来。

UML类图

IMG_20220626_110544

设计模式概览

创建型

主要解决如何灵活创建对象或者类的问题

简单工厂模式

静态工厂方法(Static Factory Method)模式,是由一个工厂对象决定创建出哪一种产品类的实例,非GOF23种设计模式之一,但是非常常用。

  • 可以在一个工厂中生产一个产品类的产品,如电脑,可以生产IOS以及Windows的。

  • 使用过程中也可以使用java反射机制代替条件判断。

  • 使用场景:

    • 某个类频繁改动,被引用很多,不想直接new这个类的对象
    • 某个类的构建过程很复杂
    • 某个类的构建过程中依赖的依赖项无法在调用处找到

工厂方法模式

定义一个用于创建对象的接口,让子类决定实例化哪个类。

工厂方法模式是简单工厂方法模式的升级版本,为了克服简单工厂方法模式的缺点而生,使用场景简单工厂方法完全一样

  • 为每一种要生产的产品配备一个工厂,就是说每个工厂只生产一种特定的产品。这样做的好处就是当以后需要增加新的产品时,直接新增加一个对应的工厂就可以了,而不是去修改原有的工厂,符合编程原则的开闭原则

抽象工厂模式

抽象工厂为创建一组相关或者是相互依赖的对象提供一个接口,而不需要指定他们的具体类

  • 使用场景

    • 暂无
  • 一个工厂要生产某一个品牌家族里面的系列产品,关键在于品牌家族的概念。如小米和苹果,都有自己的工厂,也实现同一个抽象工厂。抽象工厂有多个方法,可以生产多个产品类。

构建者模式

将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示

  • 使用场景:
    • 当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。
  • 使用方法
    • 给目标类创建一个静态内部类Builder,其成员变量与目标类一致(copy过去)
    • 目标类构造方法改为私有的,参数为Builder
    • Builder的构造方法参数为目标类的必填参数
    • Builder中的方法,对成员变量进行赋值操作,返回Builder类型实例(以便链式调用)
    • Builder提供build方法,构建目标类的实例并返回(将自己this作为参数调用目标类的构造函数)
  • 真实场景
    • StringBuilder

单例模式

  • 使用场景
    • 保证系统中只有一个实例,需要考虑类实现序列化时如何保证?如何保证不能通过反射创建新的实例?
  • 使用方法,5种方法
  1. 饿汉式,静态常量,定义实例对象时直接初始化,线程安全
  2. 懒汉式,单null检查,线程不安全
  3. 懒汉式+同步方法,线程安全,性能低
  4. 双重null检查(同步块+volatile关键字禁止重排序)
  5. 静态内部类,(Holder),推荐
  6. 枚举,唯一一个不能反射创建的方法,使用枚举充当Holder,但是不能懒加载。(枚举不能被继承且只允许实例化一次)

原型模式

使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

  • 使用场景
    • 某个类创建实例代价过高
    • 当构建的多个对象实例都需要处于某种初始状态
  • 使用方法
    • 实现java.lang.Cloneable接口
  • 真实场景
    • Spring创建Bean

结构型

主要用于将类或对象进行组合从而构建灵活而高效的结构

适配器模式

将一个接口转换为客户端所期待的接口,从而使两个接口不兼容的类可以在一起工作,也称为Wrapper(包装器)

  • 使用场景
    • 需要使用一个现存的类,但它提供的接口与我们系统的接口不兼容,而我们还不能修改它
    • 整合其它多个团队开发的奇奇怪怪的模块
  • 使用方法
    • Adapter实现目标接口Target,而且必须要引用Adaptee,因为我们要在此类中包装Adaptee的功能

桥接模式

桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。又称为柄体(Handle and Body)模式或接口(Interfce)模式。

  • 假设某个汽车厂商生产三种品牌的汽车:Big、Tiny和Boss,每种品牌又可以选择燃油、纯电和混合动力。如果用传统的继承来表示各个最终车型,一共有3个抽象类加9个最终子类。

image-20230109160317407

image-20230109160420459

  • 使用场景
    • 避免直接继承带来的子类爆炸
    • 出现两个变化维度时可以考虑
  • 真实使用场景
    • Java IO流

组合模式

组合模式允许以相同的方式处理单个对象和对象的组合体

  • 使用场景

    • 当你的程序结构有类似树一样的层级关系时,例如文件系统,视图树,公司组织架构等等
    • 当你要以统一的方式操作单个对象和由这些对象组成的组合对象的时候
  • 使用方法

    • 设计一个个体与组合通用的接口Component,根据透明方式以及安全方式的不同,有不同的处理
    • 设计组合类,其继承Component并持有一个Component的集合
    • 叶子节点只需要继承Component

装饰者模式

装饰模式是在不必改变原类和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象

  • 使用场景

    • 需要在运行时动态的给一个对象增加额外的职责时候
    • 需要给一个现有的类增加职责,但是又不想通过继承的方式来实现的时候(应该优先使用组合而非继承),或者通过继承的方式不现实的时候(可能由于排列组合产生类爆炸的问题)
  • 使用方法

    • 装饰者基类实现和被装饰原始对象相同的接口I,内部持有一个接口I的引用,用于接收被装饰的对象
    • 构建多个装饰者子类,继承装饰者基类,实现不同的功能,在I的方法中加入自己的逻辑
  • 与代理模式的区别

    • 代理模式侧重于使用代理类增强被代理对象的访问,而装饰者模式侧重于使用装饰者类来对被装饰对象的功能进行增减
    • 装饰者模式主要是提供一组装饰者类,然后形成一个装饰者栈,来动态的对某一个对象不断加强,而代理一般不会使用多级代理
  • 和适配器模式的区别

    • 适配器模式的意义是要将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的。而装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方式而提升性能

外观模式

提供一个高层次的接口,使得子系统更易于使用,也称为门面模式Facade

  • 使用场景
    • 聚合其它模块
  • 使用方法
    • Facade类引用其它多个子模块,对外提供统一的接口
    • 大部分的外观模式没有限制客户端直接使用子系统。也就是说如果用户遇到了通过外观方法无法完成的功能,可以自己调用相关子系统去完成

享元模式

允许使用对象共享来有效地支持大量细粒度对象

  • 使用场景
    • 存在大量相似对象,每个对象之间只是根据不同的使用场景有些许变化时
  • 使用方法
    • 定义一个共享对象通用的接口,定义所有对象共享的操作(一定要区分出内部状态与外部状态,共享对象只持有内部状态,内部状态不可以从客户端设置,而外部状态必须从客户端设置。)
    • 实现需要共享的对象类,其一般是一个不可变类,内部只保存需要共享的内部状态,它可能不止一个
    • 构建共享对象工厂

代理模式

为其他对象提供一种代理以控制对这个对象的访问

  • 静态与动态

    • 静态代理:预先确定了代理与被代理者的关系,即类与被代理类的依赖关系在编译期间就确定了

    • 动态代理:类与被代理类的依赖关系在运行期才能确定,应用于AOP

      • JDK动态代理:首先使用接口来定义好操作的规范。然后通过Proxy类产生的代理对象调用被代理对象的操作,而这个操作又被分发给InvocationHandler接口的 invoke方法具体执行
      • CGLIB动态代理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。

行为型

主要解决类或者对象之间互相通信的问题

责任链模式

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止

  • 使用场景
    • 一个请求需要被多个对象中的某一个处理,但是到底是哪个对象必须在运行时根据条件决定。

命令模式

将一个请求封装成一个对象,从而让用户使用不同的请求把客户端参数化;对请求排队或记录日志,以及支持可撤销的操作。

  • 应用场景
    • 当需要将各种执行的动作抽象出来,使用时通过不同的参数来决定执行哪个对象
    • 当某个或者某些操作需要支持撤销的场景
    • 当要对操作过程记录日志,以便后期通过日志将操作过程重新做一遍时
    • 当某个操作需要支持事务操作的时候
  • 使用方法
    • 声明一个命令接口 (Command),有一个execute方法
    • 构建那些可以具体完成命令的角色(Receiver)
    • 构建各种具体命令(ConcreteCommand),接收Receiver作为参数
    • 构建命令的调用者 (Invoker),可以持有命令的集合,遍历执行

解释器模式

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

  • 解释器模式(Interpreter)是一种针对特定问题设计的一种解决方案,如解析字符串匹配,代码解析。
  • 如Antlr的工作原理?

迭代器模式

提供一种方法顺序访问一个容器对象中的各个元素,而又不需要暴露该对象的内部表示。

  • Java的集合框架中常用

中介者模式

用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互

  • 使用场景
    • 中介者模式经常用在有众多交互组件的UI上。为了简化UI程序,MVC模式以及MVVM模式都可以看作是Mediator模式的扩展

备忘录模式

在不破坏封闭的前提下捕获一个对象的内部状态,并在该对象之外保存这个状态,从而可以将对象恢复到原先保存的状态

  • 使用场景
    • 保存类对象
  • 使用方法
    • 定义Originator,需要保存状态的类
    • 构建备忘录Memento,一般是个POJO
    • 构建CareTaker,负责保存和恢复Originator的状态,状态是保存在这类里面的。可以持有一系列备忘录的集合,保存多个状态

观察者模式

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都可以得到通知并自动更新。

也被称为发布订阅模式

  • 使用场景
    • 发布订阅
  • 使用方法
    • 被观察者持有观察者的集合

策略模式

策略模式定义了一系列的算法,并将每一个算法封装起来,使他们可以相互替换。

  • 使用场景
    • 个操作有好多种实现方法,而你需要根据不同的情况使用if-else等分支结构来确定使用哪种实现方式。有多种策略可以实现一个特定的目的,使用何种策略取决于调用者(客户端)。
  • 使用方法
    • 定义一个抽象策略接口
    • 定义多个不同的具体的策略类

状态模式

当一个对象内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

  • 使用场景
  • 使用方法
    • 定义一个状态接口,有一个action方法。不同状态下的action实现不同
    • 定义一个Context上下文类,持有状态接口的引用,用于切换状态。提供一个代理的action方法
    • 定义各种状态类

模板方法模式

在一个方法中定义一个算法的骨架,而将一些步骤的实现延迟到子类中,使得子类可以在不改变一个算法的结构前提下即可重定义该算法的某些特定步骤

  • 使用场景
    • 两个或者多个类大体相似,但是个别方法局部不一致,可以使用此模式重构
  • 一般情况下,程序的执行流是子类调用父类的方法,模板方法模式使得程序流程变成了父类调用子类方法,这个使得程序比较难以理解和跟踪。

访问者模式

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于其内部各个元素的新操作

  • 使用方法
    • 构建Element,element类里面将自己this作为参数传递给visitor的visit()方法,把自己交给访问者访问。
    • 构建ObjectStructure,持有Element的引用集合,可以遍历访问
    • 构建Visitor接口,一个Element一个visit方法

分派

  • 分派:根据对象的类型而对方法进行的选择。发生在编译时的分派叫静态分派,例如重载(overload),发生在运行时的分派叫动态分派,例如重写(overwrite)
    • 依据单个宗量进行方法的选择就叫单分派,Java 动态分派只根据方法的接收者一个宗量进行分配,所以其是单分派
    • 依据多个宗量进行方法的选择就叫多分派,Java 静态分派要根据方法的接收者与参数这两个宗量进行分配,所以其是多分派
  • ObjectStructure遍历element时的accept是一个单分派过程,Element的visitor.visit(this),也是一次单分派过程。两次动态单分派结合起来就完成了一次伪动态双分派

个人常用的设计模式

  • 简单工厂模式:复杂类的构建,list接口以及details接口中对于PO到VO的转换,复用构建过程
  • 构建者模式:构建飞书机器人的消息文本
  • 单例模式:可复用,线程安全的的Client
  • 代理模式:在Windows本地调试KVM接口
  • 责任链模式:多类型用户登录
  • 模板方法模式:Java与Groovy函数的业务逻辑,都属于Function,但是细节部分不一样

参考文章