UML 类图

参考:

类定义了对象的属性和行为,是对象的静态视图。类元素之间有泛化、聚合、关联等关系。

访问修饰符:+-#~ 分别表示公有、私有、保护、默认 (包)。

泛化、实现

泛化 (Generalizations) 表示继承的含义,使用实线空心三角箭头指向父类。下图表示 Circle 泛化/继承自 Shape,接口或抽象类名称使用斜体。

实现 (Realizations) 表示类和接口之间的实现关系,使用虚线空心三角箭头指向接口。

聚合、组合

聚合 (Aggregations) 表示整体和部分组成的含义,例如树木之于森林,使用实线空心菱形箭头表示。

组合 (Composition) 表示整体和部分的强聚合关系,例如心脏之于人,使用实线实心菱形箭头表示。

聚合 vs 组合

  • 两者都体现了 “has-a” 关系,就是部分对象作为另一个整体对象的属性。
  • 在聚合中,部分对象可以属于多个整体对象 (一对一、一对多、多对多),部分对象的生命周期不受整体对象的生命周期影响,如上图中一个联系人可以属于多个联系人组,当联系人组删除,不会有联系人删除,又例如员工之于公司。
  • 在组合中,部分对象只能属于一个整体对象 (一对一、一对多),其生命周期完全受控于整体对象,整体对象消失时,部分对象也必须消失,如上图中联系人不能独立于地址簿存在,又例如引擎之于汽车。

关联、依赖

关联 (Associations) 表示两个类元素之间的一种简单的“使用”关系,不像聚合/组合那样含有“包含”的意思,体现在代码中就是一个类的实例引用了另一个类的实例。下图表示 Team 的实例将作为 Player 中的一个名为 playsFor 的属性。

关联可以具有方向,没有方向时表示 A 和 B 互相知道对方,如下图,若指明了方向则表示 A 知道 B,B 不知道 A。

关联不一定只限于两个类之间,如下图,Role 称为关联类,表示关联连接也具有属性和行为。如下图 Employee 在不同的 Project 中扮演着不同的 Role。

依赖 (Dependency) 使用虚线箭头表示,是最弱的一种关系,和关联相比意在表示运行时的、临时性的而非固定的依赖,体现在代码中就是方法参数、局部变量或静态方法调用。下图表示 A 运行过程中会用到 B,应避免双向依赖。

嵌套

嵌套 (Nestings) 可以表示内部类的含义,使用下图所示的方式表示。


设计模式七大原则

单一职责原则 (Single Responsibility Principle, SRP)

一个类 (或方法、模块) 只承担单一的职责,降低耦合度,较少代码变更带来的风险。否则,当一个职责发生变化时,可能会影响其他职责。

开闭原则 (Open Closed Principle, OCP)

对扩展开放,对修改关闭。当模块功能发生变化时,尽量通过扩展的方式而不是修改现有代码,避免给旧代码引入错误。

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

所有引用基类的地方必须能透明地使用其子类的对象。即子类应尽量不要重写父类的方法,避免发生多态。这样做是为了减少父类改变带来的影响,且只有当子类可以替换基类而系统功能不受影响时,基类才能算是真正意义上的被复用。

即子类应该尽可能少的重写父类的方法,如果一个子类重写了大部分父类的方法,那应当将子类提升到与父类一个级别,通过让两者继承同一个基类,或使用组合、聚合的方法实现。

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

类之间的依赖关系应该建立在最小的接口上,不应该依赖它不需要的接口。
例如类 A 依赖于接口 IX 的实现类 X 中的 (a, b, c) 方法,类 B 依赖于接口 IX 的实现类 Y 中的 (d, e) 方法,则 X 和 Y 将不得不实现额外的方法,应当将接口 IX 拆分为两个接口分别包含 (a, b, c) 和 (d, e)。

迪米特法则 (Law of Demeter, LOD)

也称为最少知识原则 (Least Knowledge Principle, LKP),一个对象应当对其他对象有尽可能少的了解,只和“朋友”通信,不和陌生人通信。

对象之间的耦合关系可以看作朋友关系,出现在成员变量、方法参数、返回值中的可以看作直接朋友,否则出现在局部变量中的是间接朋友,也就是陌生人。

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

高层模块不应该依赖低层模块,模块应依赖于抽象层,而不是具体实现。即应该面向接口编程,当一个对象与其他对象有依赖关系时,通过声明接口注入 (通过构造器或访问器) 不同的实现类实例来实现。
每个底层实现类都应有接口或抽象类,且不应暴露接口或抽象类中未声明的方法。

组合/聚合复用原则 (Composite/Aggregate Reuse Principle, CARP)

尽量使用组合/聚合的方式,而不是通过继承达到复用的目的。

Note

单一职责原则、开闭原则、里氏代换原则、接口隔离原则、迪米特法则合称 SOLID


23 种设计模式

参考

创建型 (5 种)

单例模式

单例模式保证一个类只有一个实例,在 Java 代码中:

  1. 不仅要私有化构造方法,还要在其中抛异常防止反射创建对象;
  2. 饿汉式推荐通过枚举或将静态属性实现,类加载时创建;
  3. 懒汉式推荐通过静态内部类实现,外部类加载时不会加载内部类。

工厂方法模式

工厂方法模式用于生产指定产品,简单工厂不满足开闭原则,工厂方法模式扩展会引入多个类。


不使用工厂时,客户端需要知道具体产品的类名。

简单工厂,也称为静态工厂,一般是通过工厂类的静态方法来获取产品对象,客户端只需要知道工厂方法和产品的名字,无需知道类名。扩展产品时需要修改工厂方法的代码,不满足开闭原则。使用场景:一般适用于产品较少,逻辑不太复杂的场景。

简单工厂模式中一类产品只有一个工厂类,而工厂方法模式有一组实现了相同工厂接口的工厂类,即将产品的创建下放到子类。通过新增工厂类来实现扩展,无需修改原有工厂方法的代码,解决了简单工厂不满足开闭原则的问题。

抽象工厂模式

工厂方法模式中,一般一个具体的工厂只生产一种具体的产品,具体工厂中只有一个或一组重载的工厂方法,一个工厂无法提供多种不同的产品,这时就要使用抽象工厂模式。
我们将同一个工厂生产的不同种类的产品称为一个“产品族”,例如小米工厂和苹果工厂分别生产自己的智能家居产品族,包括智能手机、笔记本电脑等等…

使用场景:当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。

抽象工厂可以方便的增加产品族,而难以增加新产品种类。

建造者模式

建造者模式屏蔽了复杂对象的组建细节,可以将复杂对象的多个部件(属性)和其组装过程分开,用户只需指定复杂对象的关键属性,不需要关注具体构造细节,就可以得到该对象。不同的指挥者和建造者组合,可以产生不同的对象。

简化:如果只有一个具体建造者,可以省略建造者接口;建造者可以同时扮演指挥者。

原型模式

原型模式(Prototype Pattern)是一种创建型设计模式,它使用已有的对象作为原型,通过克隆这些原型对象来创建新的对象,而不是通过实例化对象的类来创建。这种模式可以避免在创建新对象时的重复初始化工作。

使用场景:短时间内频繁的创建类似的对象时,可以使用原型模式,要注意深拷贝和浅拷贝!

JS 中使用原型对象来实现数据共享和继承;Spring 中 Bean 的 Scope 除了单例还有原型。

结构型 (7 种)

适配器模式

使用场景:现有的类可以满足客户端的需要,但是它所提供的接口不一定是客户端所期望的。

举例:客户端期望使用统一的 JDBC 接口操作数据库,不同的数据库驱动,就是 JDBC 接口和个数据库引擎之间的适配器。

优点:具体的实现对客户端来说是透明的,可以灵活的替换和扩展适配器类。

桥接模式

问题:当一个类有多个独立的变化维度时,随着每个维度类型的增加,类的数量会膨胀。例如一个文本编辑器需要处理 m 种操作系统和 n 种编码方式的文件(m 种类型和 n 种实现方式),针对不同操作系统进行实现,会有 m*n 个类。而桥接模式将两个维度分离,只需要 m+n 个类。

桥接模式的核心思想是将抽象部分与实现部分分离,使它们可以独立变化,使用组合或聚合而不是继承,以此来降低耦合性。

使用场景:当一个类具有两个独立变化的维度,且这两个维度都需要扩展时,可以使用桥接模式。

装饰模式

装饰模式(Decorator)也叫包装器模式(Wrapper)。给一个类增加行为可以通过继承或关联,继承是静态的,且一些语言存在单继承问题。装饰器模式是指将一个类关联到另一个装饰器类中,由装饰器类决定是否调用嵌入类的方法以扩展自己的行为。

装饰模式在不需要创建子类的情况下扩展了类的行为,并且对客户端来说是透明的。

使用场景:参考 Java IO 流中的 Buffer。

外观模式

外观模式也称门面模式,是指外部客户端与内部子系统之间的通信必须通过一个统一的外观对象进行,客户端无需关注子系统实现细节。

使用场景:各种工具类。

注意:一个子系统可以有多个外观类;不要试图通过外观类为子系统增加功能。

享元模式

享元模式的目的是使用共享来实现大量细粒度对象的复用,节省内存空间。享元对象需要区分内部状态(可共享)和外部状态(不可共享)。

享元模式的核心在于享元工厂类需要维护一个用于存储享元对象的享元池,当需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回,并在享元池中保存该对象。

使用场景:Java 中的字符串常量池;数据库连接池等;Spring 容器等。

组合模式

组合模式也称为部分 - 整体模式,将部分和整体的关系以树结构来表示,使客户端对单个对象(叶子)与组合对象(树枝)具有一致的访问性。

优点:客户端代码可以一致的处理单个对象和组合对象,符合开闭原则,可以在不修改源代码,只用重新组合对象就可以实现新的需求。

组件:

  1. 抽象构件 Component,用于规定叶子和树枝节点的行为;
  2. 叶子构件 Leaf,没有子节点,实现具体的处理逻辑;
  3. 树枝节点 Composite,管理子节点并调用其方法。

代理模式

代理模式可以在不改变原有代码的情况下扩展被代理对象的行为,控制客户端对被代理对象的访问,在一定程度上降低了系统的耦合度。

使用场景:

  1. 安全代理:屏蔽对真实对象的直接访问、给不同客户端不同权限;
  2. 远程代理:通过代理类进行远程方法调用,使客户端可以访问远程机器上的对象;
  3. 懒加载:先加载轻量的代理对象,需要时再加载真实对象,例如缩略图;
  4. 缓存代理:给真实对象添加缓存机制,供多个客户端共享数据。

行为型 (11 种)

模板方法模式

模板方法模式定义好了一个算法的骨架,将其中的一些步骤延迟到子类中实现,子类可以在不改变算法结构的情况下重定义算法中的某些步骤。

使用场景:回调/钩子方法。

命令模式

命令模式也称为动作模式或事务模式,将请求封装为一个对象,将发送请求和执行命令的责任分开,降低请求者与接收者之间的耦合,使得命令可以像对象一样存储、传输。

使用场景:遥控器、撤消/重做、事务、Spark 编程。

迭代器模式

迭代器模式也称游标模式,目的是提供一种访问集合对象的方式,而不需要暴露集合的内部表示和实现。

使用场景:Java 集合。

观察者模式

观察者模式定义了对象间 1:N 的一种依赖关系,当被观察的目标对象状态发生改变时,对应的观察者对象能够能到通知并进行相应的操作。

观察者模式有推、拉两种模式。

使用场景:监听器、发布订阅、聊天室。

中介者模式

像聊天室一样的,对象的网状多对多关系维护复杂,这些对象称为同事对象。中介者模式使用一个中介对象封装一系列对象交互操作,使同事对象之间不需要显示的互相引用,降低了耦合。

使用场景:MVC 中的 Controller、Vuex、聊天室。

备忘录模式

备忘录模式也称为快照模式,指保存某个对象某个时刻的内部状态,以便于在需要时进行回滚、撤销操作(SVN、Git)。

  • 发起人 (Originator):负责创建备忘录,记录自身需要保存的状态,并可通过设置备忘录回滚;
  • 备忘录 (Memento):保存 Originator 的内部状态;
  • 负责人 (Caretaker):负责存储、管理备忘录;

解释器模式

不常用,解释器模式目的是描述如何构建一个能够解释特定语言或文法的句子的解释器。
SQL、正则表达式解析等。

状态模式

一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,状态模式用于解决系统中复杂对象的状态转换,以及不同状态下行为的封装问题。

使用场景:代码中包含大量与对象状态有关的条件语句时,可以考虑使用状态模式。

缺点:不符合开闭原则。

策略模式

策略模式指将实现某个功能的一系列算法封装为一个算法族,并且可以互相替换,客户端可以根据环境决定调用哪一个算法实现功能。

例如客户端根据数据特征选择合适的排序算法。

责任链模式

责任链模式将处理器对象以链式结构连接起来,使请求对象沿着链传递,多个处理器对象都有机会处理该请求。

使用场景:过滤器、拦截器、JS 事件冒泡。

访问者模式

不常用。