![f7ef5fa3985d565d22334a9d1443a644.png](https://i-blog.csdnimg.cn/blog_migrate/d54af0f8030d90650b176c7870217b3b.jpeg)
简述
按照惯例,先上维基百科的解释,什么是设计模式?
在软件工程中, 设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领域引入到计算机科学的。
设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
说道设计模式,就不得不提到软件设计七大原则,可以说是设计模式的抽象和设计基础。
- 单一职责原则 (Single Responsibility Principle):单一职责原则表示一个模块的组成元素之间的功能相关性。从软件变化的角度来看,就一个类而言,应该仅有一个让它变化的原因;通俗地说,即一个类只负责一项职责。
- 开放-关闭原则 (Open-Closed Principle):开放-关闭原则表示软件实体 (类、模块、函数等等) 应该是可以被扩展的,但是不可被修改。
- 里氏替换原则 (Liskov Substitution Principle):派生类(子类)对象应当可以在程序中代替其基类(超类)对象。或者说子类对象能够替换父类对象,而程序逻辑不变。
- 依赖倒转原则 (Dependence Inversion Principle):高层模块不应该依赖低层模块,二者都应该依赖于其抽象。进一步说,抽象不应该依赖于细节,细节应该依赖于抽象。
- 接口隔离原则 (Interface Segregation Principle):一个类不应该依赖他不需要的接口,使用多个专门的接口比使用单一的总接口总要好。
- 迪米特法则(Law Of Demeter):又叫最少知识原则(Least Knowledge Principle或简写为LKP),一个对象应当对其它对象有尽可能少的了解。通俗来说就是,只与直接的朋友通信。
- 组合/聚合复用原则 (Composite/Aggregate Reuse Principle):就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。
按照类型,设计模式应该可以分为 3 个大类,一共 23 种。
- 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式。
23 种设计模式
一、单例模式
定义:采取方法,使整个软件系统中,对某个类只存在一个对象实例。并且该类只提供一个取得其对象实例的方法(静态方法)。
使用场景:频繁创建和销毁的对象、创建对象时消耗资源非常多的对象、工具类对象、频繁访问的数据库或文件对象。
单例模式有八种方法:
- 恶汉式(静态常量)
```java public class Singleton1 { private Singleton1() { }
private static final Singleton1 instance = new Singleton1();
public Singleton1 getInstance() {
return instance;
}
} ```
优缺点:线程安全,在类装载的时候就实例化,有可能造成内存浪费。
- 恶汉式(静态代码块)
```java public class Singleton2 { private Singleton2() { }
private static Singleton2 instance;
static {
instance = new Singleton2();
}
public Singleton2 getInstance() {
return instance;
}
} ```
优缺点:跟上面一样。
- 懒汉式(线程不安全)
```java public class Singleton3 { private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
} ```
优缺点:懒加载,省内存。但是线程不安全。
- 懒汉式(同步方法,线程安全)
就是getInstance()
方法上锁,解决线程安全问题。但是因为要等锁,效率太低。
- 懒汉式(同步代码块+双重检查,线程安全)
```java public class Singleton4 { private static volatile Singleton4 instance;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
} ```
只在初始化的代码块加锁,并且检查两次,相比第四种优化效率。
- 静态内部类
```JAVA public class Singleton5 {
private Singleton5() {
}
public Singleton5 getInstance() {
return Singleton.instance;
}
private static class Singleton {
private static final Singleton5 instance = new Singleton5();
}
} ```
使用不能在外部直接改变内部类的特性,保证了线程安全和单例。并且由于内部类在调用的时候才会初始化,保证了懒加载,没有资源浪费。
- 枚举
java public enum Singleton6 { INSTANCE; }
单例模式简洁高效,线程安全还能抗反射。但是不是懒加载,而且上面的形式不太容易理解。一般实际使用可能是下面这种,一个接口的单例实现:
```java public interface SomeInterface {//一个接口 void doSomething(); }
public enum Singleton7 implements SomeInterface {//一个枚举类来实现接口 INSTANCE;
@Override
public void doSomething() {
System.out.println("做点啥");
}
}
public class TestClass {//测试的时候这样就可以直接获取单例的实体类 public static void main(String[] args) { Singleton7.INSTANCE.doSomething(); } }
```
二、原型模式
定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
优点: 1、性能提高。 2、逃避构造函数的约束。
缺点: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。
原型模式包含以下主要角色:
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
//具体原型类
class Realizetype implements Cloneable
{
Realizetype()
{
System.out.println("具体原型创建成功!");
}
public Object clone() throws CloneNotSupportedException
{
System.out.println("具体原型复制成功!");
return (Realizetype)super.clone();
}
}
//原型模式的测试类
public class PrototypeTest
{
public static void main(String[] args)throws CloneNotSupportedException
{
Realizetype obj1=new Realizetype();
Realizetype obj2=(Realizetype)obj1.clone();
System.out.println("obj1==obj2?"+(obj1==obj2));
}
}
原型模式通常适用于以下场景。
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 对象的创建过程比较麻烦,但复制比较简单的时候。
三、简单工厂模式和工厂方法模式
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
public class SimpleFactory {
public static Product create(String type) {
Product product;
switch (type) {
case "a":
System.out.println("生产了产品A");
product = new ProductA();
break;
case "b":
System.out.println("生产了产品B");
product = new ProductB();
break;
default:
System.out.println("没有这种产品");
product = null;
break;
}
return product;
}
}
interface Product {
}
class ProductA implements Product {
}
class ProductB implements Product {
}
使用的时候,只需要传入相应的参数就可以生产对应的产品:
SimpleFactory.create("a");
优点:
- 将创建实例的工作与使用实例的工作分开,使用者不必关心类对象如何创建,实现了解耦;
- 把初始化实例时的工作放到工厂里进行,使代码更容易维护。 更符合面向对象的原则 & 面向接口编程,而不是面向实现编程。
缺点:
- 工厂类集中了所有实例(产品)的创建逻辑,一旦这个工厂不能正常工作,整个系统都会受到影响;
- 违背“开放 - 关闭原则”,一旦添加新产品就不得不修改工厂类的逻辑,这样就会造成工厂逻辑过于复杂。
- 简单工厂模式由于使用了静态工厂方法,静态方法不能被继承和重写,会造成工厂角色无法形成基于继承的等级结构。
使用场景:
- 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
工厂方法是针对每一种产品提供一个工厂类,通过不同的工厂实例来创建不同的产品实例。主要解决了上面简单工厂模式违背了“开放 - 关闭原则”的问题。
public interface Factory {
Product create();
}
class FactoryA implements Factory {
@Override
public Product create() {
return new ProductA();
}
}
class FactoryB implements Factory {
@Override
public Product create() {
return new ProductB();
}
}
interface Product {
void use();
}
class ProductA implements Product {
@Override
public void use() {
System.out.println("使用产品A");
}
}
class ProductB implements Product {
@Override
public void use() {
System.out.println("使用产品B");
}
}
使用的时候,只要创建对应的工厂就可以生产对应产品并使用了。
Factory factory = new FactoryA();
factory.create().use();
优点:
- 更符合开-闭原则,新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可
- 符合单一职责原则,每个具体工厂类只负责创建对应的产品
缺点
- 添加新产品时,除了增加新产品类外,还要提供与之对应的具体工厂类,系统类的个数将成对增加,在一定程度上增加了系统的复杂度;同时,有更多的类需要编译和运行,会给系统带来一些额外的开销
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度
- 虽然保证了工厂方法内的对修改关闭,但对于使用工厂方法的类,如果要更换另外一种产品,仍然需要修改实例化的具体工厂类
- 一个具体工厂只能创建一种具体产品
四、抽象工厂模式
为了解决上面工厂方法模式里“一个具体工厂只能创建一类产品”的问题,我们又使用了一种新的设计模式:抽象工厂模式。抽象工厂模式允许使用抽象的接口来创建一组相关产品,而不需要知道或关心实际生产出的具体产品是什么,这样就可以从具体产品中被解耦。
public abstract class AbstractFactory {
public abstract AbstractProduct createProductA();
public abstract AbstractProduct createProductB();
}
class FactoryA extends AbstractFactory {
@Override
public AbstractProduct createProductA() {
return new ProductA();
}
@Override
public AbstractProduct createProductB() {
return new ProductB();
}
}
class FactoryB extends AbstractFactory {
@Override
public AbstractProduct createProductA() {
return new ProductA();
}
@Override
public AbstractProduct createProductB() {
return new ProductB();
}
}
abstract class AbstractProduct {
public abstract void use();
}
abstract class AbstractProductA extends AbstractProduct {
}
abstract class AbstractProductB extends AbstractProduct {
}
class ProductA extends AbstractProductA {
@Override
public void use() {
System.out.println("使用了产品A");
}
}
class ProductB extends AbstractProductB {
@Override
public void use() {
System.out.println("使用了产品B");
}
}
使用的时候,两个工厂就可以同时生产两款产品了。
new FactoryA().createProductA().use();
new FactoryA().createProductB().use();
new FactoryB().createProductA().use();
new FactoryB().createProductB().use();
优点:
- 降低耦合。抽象工厂模式将具体产品的创建延迟到具体工厂的子类中,这样将对象的创建封装起来,可以减少客户端与具体产品类之间的依赖,从而使系统耦合度低,这样更有利于后期的维护和扩展;
- 更符合开-闭原则。新增一种产品类时,只需要增加相应的具体产品类和相应的工厂子类即可
缺点:
抽象工厂模式很难支持新种类产品的变化。这是因为抽象工厂接口中已经确定了可以被创建的产品集合,如果需要添加新产品,此时就必须去修改抽象工厂的接口,这样就涉及到抽象工厂类的以及所有子类的改变,这样也就违背了“开放-封闭”原则。
五、建造者模式
定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
使用范围:
- 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
- 当构造过程必须允许被构造的对象有不同表示时。
在这样的设计模式中,有以下几个角色:
- Builder:为创建一个产品对象的各个部件指定抽象接口。
- ConcreteBuilder:实现 Builder 的接口以构造和装配该产品的各个部件,定义并明确它所创建的表示,并提供一个检索产品的接口。
- Director:构造一个使用 Builder 接口的对象,指导构建过程。
- Product:表示被构造的复杂对象。ConcreteBuilder 创建该产品的内部表示并定义它的装配过程,包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
public interface PersonBuilder {
void buildHead();
void buildBody();
void buildFoot();
Person buildPerson();
}
public class ManBuilder implements PersonBuilder {
Person person;
public ManBuilder() {
person = new Man();
}
public void buildbody() {
person.setBody("建造男人的身体");
}
public void buildFoot() {
person.setFoot("建造男人的脚");
}
public void buildHead() {
person.setHead("建造男人的头");
}
public Person buildPerson() {
return person;
}
}
public class PersonDirector {
public Person constructPerson(PersonBuilder pb) {
pb.buildHead();
pb.buildBody();
pb.buildFoot();
return pb.buildPerson();
}
}
public class Person {
private String head;
private String body;
private String foot;
public String getHead() {
return head;
}
public void setHead(String head) {
this.head = head;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getFoot() {
return foot;
}
public void setFoot(String foot) {
this.foot = foot;
}
}
使用:
PersonDirector pd = new PersonDirector();
Person manPerson = pd.constructPerson(new ManBuilder());
建造者(Builder)模式在应用过程中可以根据需要改变,如果创建的产品种类只有一种,只需要一个具体建造者,这时可以省略掉抽象建造者,甚至可以省略掉指挥者角色。
六、享元模式
定义:运用共享技术来有効地支持大量细粒度对象的复用。它通过共享已经存在的又橡来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
优点:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
缺点:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长
享元模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分;
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。下面来分析其基本结构和实现方法。
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
享元模式看起来跟单例模式有些相似,但是也有区别:单例模式是类级别的,一个类只能有一个对象实例;享元模式是对象级别的,可以有多个对象实例,多个变量引用同一个对象实例;享元模式可以再次创建对象 也可以取缓存对象,单例模式则是严格控制单个进程中只有一个实例对象。
public abstract class FlyWeight {
public abstract void use();
}
class ConcreteFlyweight extends FlyWeight {
private String string;
ConcreteFlyweight(String str) {
string = str;
}
@Override
public void use() {
System.out.println("使用了享元模式" + string);
}
}
class FlyweightFactory {
private Hashtable<String, FlyWeight> flyweights = new Hashtable<>();
public FlyweightFactory() {
}
public FlyWeight getFlyWeight(String name) {
FlyWeight flyweight = flyweights.get(name);
if (flyweight == null) {
flyweight = new ConcreteFlyweight(name);
flyweights.put(name, flyweight);
}
return flyweight;
}
}
使用的时候:
new FlyweightFactory().getFlyWeight("test").use();
七、代理模式
定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点是:
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
- 增加了系统的复杂度;
代理模式的主要角色如下:
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
public class Proxy implements Subject {
private RealSubject realSubject;
@Override
public void request() {
if(realSubject==null){
realSubject = new RealSubject();
}
realSubject.request();
}
}
interface Subject{
void request();
}
class RealSubject implements Subject{
@Override
public void request() {
System.out.println("真实的请求");
}
}
应用场景:
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。
代理模式中,代理类中包含了对真实主题的引用,这种方式存在两个缺点。
- 真实主题与代理主题一一对应,增加真实主题也要增加代理。
- 设计代理以前真实主题必须事先存在,不太灵活。采用动态代理模式可以解决以上问题,如 Spring AOP。
八、适配器模式
定义:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
优点:
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
缺点是:对类适配器来说,更换适配器的实现过程比较复杂。
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
类结构模式:
public class Adapter extends Adaptee implements target {
@Override
public void request() {
specificRequest();
}
}
class Adaptee {
public void specificRequest() {
System.out.println("适配者中的业务代码被调用!");
}
}
interface target {
void request();
}
对象结构模式:
class ObjectAdapter implements Target
{
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee)
{
this.adaptee=adaptee;
}
public void request()
{
adaptee.specificRequest();
}
}
适配器模式(Adapter)通常适用于以下场景:
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
扩展:适配器模式(Adapter)可扩展为双向适配器模式,双向适配器类既可以把适配者接口转换成目标接口,也可以把目标接口转换成适配者接口。
九、桥接模式
定义:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
优点是:
- 由于抽象与实现分离,所以扩展能力强;
- 其实现细节对客户透明。
缺点:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,这增加了系统的理解与设计难度。
桥接(Bridge)模式包含以下主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
光看这些概念上的东西确实很难理解,桥接模式也是比较复杂的设计模式了,主要是为了应对多个维度上的变化。举个栗子。假设我们需要一辆车,车按照用途可以分为小汽车、货车和大巴车,按照颜色可以分为黑色和白色和灰色。如果遵循单一职责原则,每一种车每一个颜色都要一个类,如果再有新的颜色,就要分别在三种车里面增加,就太麻烦了。于是可以用这种方式:
abstract class Vehicle {
private Color color;
public Vehicle(Color color) {
this.color = color;
}
void info() {
this.color.getColor();
}
}
class Bus extends Vehicle {
public Bus(Color color) {
super(color);
}
}
class Car extends Vehicle {
public Car(Color color) {
super(color);
}
}
class Truck extends Vehicle {
public Truck(Color color) {
super(color);
}
}
interface Color {
void getColor();
}
class Black implements Color {
@Override
public void getColor() {
System.out.println("这是黑色的车");
}
}
class White implements Color {
@Override
public void getColor() {
System.out.println("这是白色的车");
}
}
class Grey implements Color {
@Override
public void getColor() {
System.out.println("这是灰色的车");
}
}
只需要传入对应的颜色,就可以选择需要的车:
Vehicle vehicle = new Bus(new White());
vehicle.info();
如果这时候需要新的属性怎么办?比如新增一个分类,按照能源类型分类,将车分为电动车和汽油车,只需要新增一个接口,扩展Vehicle
的构造器就可以了。
interface Energy {
void getEnergy();
}
class Electric implements Energy {
@Override
public void getEnergy() {
System.out.println("这时电能的车");
}
}
abstract class Vehicle {
private Color color;
private Energy energy;
public Vehicle(Color color) {
this.color = color;
}
public Vehicle(Color color, Energy energy) {
this.color = color;
this.energy = energy;
}
void info() {
this.color.getColor();
this.energy.getEnergy();
}
}
桥接模式通常适用于以下场景:
- 当一个类存在多个独立变化的维度,且这多个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
十、装饰模式
定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
这个直接上代码比较好理解。假如我们实现了画圆的功能:
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("画圆");
}
}
这时候画圆很简单,只需要new Circle().draw();
就可以了。但是我们想扩展新的功能,因为开闭原则又不能直接修改draw()
这个方法,那应该怎么办?答案是加个装饰器:
abstract class ShapeDecorator implements Shape {
Shape decoratedShape;
public ShapeDecorator(Shape shape) {
this.decoratedShape = shape;
}
@Override
public void draw() {
decoratedShape.draw();
}
}
class RedBorderDecorator extends ShapeDecorator {
public RedBorderDecorator(Shape shape) {
super(shape);
}
@Override
public void draw() {
super.draw();
setRedBorder();
}
void setRedBorder() {
System.out.println("加上了红色边框");
}
}
只需要new RedBorderDecorator(new Circle()).draw();
就可以画带着红边框的圆形啦。
使用场景: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。
十一、外观模式
定义:是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
- 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
缺点如:
- 不能很好地限制客户使用子系统类。
- 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
public class Facade {
private SubSystem01 obj1 = new SubSystem01();
private SubSystem02 obj2 = new SubSystem02();
private SubSystem03 obj3 = new SubSystem03();
public void method() {
obj1.method1();
obj2.method2();
obj3.method3();
}
}
class SubSystem01 {
public void method1() {
System.out.println("子系统01的method1()被调用!");
}
}
class SubSystem02 {
public void method2() {
System.out.println("子系统02的method2()被调用!");
}
}
class SubSystem03 {
public void method3() {
System.out.println("子系统03的method3()被调用!");
}
}
执行方法
new Facade().method();
可以看出子系统的实现细节被屏蔽,外部程序不需要关系具体实现细节。
十二、组合模式
定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。
优点:
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
缺点:
- 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
- 不容易限制容器中的构件;
- 不容易用继承的方法来增加构件的新功能;
组合模式包含以下主要角色。
- 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。
- 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中声明的公共接口。
- 树枝构件(Composite)角色:是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含
Add()
、Remove()
、GetChild()
等方法。
public interface Component
{
public void add(Component c);
public void remove(Component c);
public Component getChild(int i);
public void operation();
}
//树叶构件
class Leaf implements Component
{
private String name;
public Leaf(String name)
{
this.name=name;
}
public void add(Component c){ }
public void remove(Component c){ }
public Component getChild(int i)
{
return null;
}
public void operation()
{
System.out.println("树叶"+name+":被访问!");
}
}
//树枝构件
class Composite implements Component
{
private ArrayList<Component> children=new ArrayList<Component>();
public void add(Component c)
{
children.add(c);
}
public void remove(Component c)
{
children.remove(c);
}
public Component getChild(int i)
{
return children.get(i);
}
public void operation()
{
for(Object obj:children)
{
((Component)obj).operation();
}
}
}
可以看出,组合模式就是将多个对象用容器组合一个树形结构的对象。
使用场景:
- 在需要表示一个对象整体与部分的层次结构的场合。
- 要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合
十三、策略模式
定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
优点:
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
缺点:
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类。
举个栗子,假如我们想提供一个算法,但是内部实现有可能更改或者增加。
public interface Strategy {
public int doOperation(int num1, int num2);
}
class OperationAdd implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
class OperationMultiply implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}
这样的话,就可以调用的时候选择不同的算法实现:
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
扩展:在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用策略工厂模式来管理这些策略类将大大减少客户端的工作复杂度。
十四、责任链模式
定义:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
优点:
- 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
- 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
- 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
- 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
- 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
现在假设我们要设计一个日志系统,日志等级从低到高分为debug、info、error:
abstract class AbstractLogger {
public static int DEBUG = 1;
public static int INFO = 2;
public static int ERROR = 3;
protected int level;
//责任链中的下一个元素
protected AbstractLogger nextLogger;
public void setNextLogger(AbstractLogger nextLogger) {
this.nextLogger = nextLogger;
}
public void logMessage(int level, String message) {
if (this.level <= level) {
write(message);
}
if (nextLogger != null) {
nextLogger.logMessage(level, message);
}
}
abstract protected void write(String message);
}
class ConsoleLogger extends AbstractLogger {
public ConsoleLogger() {
this.level = DEBUG;
setNextLogger(new FileLogger());
}
@Override
protected void write(String message) {
System.out.println("Standard Console::Logger: " + message);
}
}
class FileLogger extends AbstractLogger {
public FileLogger() {
this.level = INFO;
setNextLogger(new ErrorLogger());
}
@Override
protected void write(String message) {
System.out.println("File::Logger: " + message);
}
}
class ErrorLogger extends AbstractLogger {
public ErrorLogger() {
this.level = ERROR;
}
@Override
protected void write(String message) {
System.out.println("Error Console::Logger: " + message);
}
}
使用的时候,会顺着责任链传下去,判断使用对应日志等级方法的进行处理:
AbstractLogger loggerChain = new ConsoleLogger();
loggerChain.logMessage(AbstractLogger.ERROR, "This is an information.");
应用场景:
- 有多个对象可以处理一个请求,哪个对象处理该请求由运行时刻自动确定。
- 可动态指定一组对象处理请求,或添加新的处理者。
- 在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。
扩展,责任链可以分为两种情况:
- 纯的责任链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。
- 不纯的责任链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。
十五、模板方法模式
定义:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。
优点:
- 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
- 它在父类中提取了公共的部分代码,便于代码复用。
- 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
模板方法模式包含以下主要角色。
- 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下:
- 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
- 基本方法:是整个算法中的一个步骤,包含以下几种类型:
- 抽象方法:在抽象类中申明,由具体子类实现。
- 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
- 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
- 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤
abstract class AbstractClass {
public void TemplateMethod() //模板方法
{
SpecificMethod();
abstractMethod1();
abstractMethod2();
}
public void SpecificMethod() //具体方法
{
System.out.println("抽象类中的具体方法被调用...");
}
public abstract void abstractMethod1(); //抽象方法1
public abstract void abstractMethod2(); //抽象方法2
}
//具体子类
class ConcreteClass extends AbstractClass {
public void abstractMethod1() {
System.out.println("抽象方法1的实现被调用...");
}
public void abstractMethod2() {
System.out.println("抽象方法2的实现被调用...");
}
}
模板方法模式通常适用于以下场景:
- 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
- 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
- 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
十六、命令模式
定义:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与备忘录模式结合,实现命令的撤销与恢复。
缺点是:可能产生大量具体命令类。因为计对每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。
命令模式包含以下主要角色:
- 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
- 具体命令角色(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
- 实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
- 调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
//调用者
class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void setCommand(Command command) {
this.command = command;
}
public void call() {
System.out.println("调用者执行命令command...");
command.execute();
}
}
//抽象命令
interface Command {
public abstract void execute();
}
//具体命令
class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand() {
receiver = new Receiver();
}
public void execute() {
receiver.action();
}
}
//接收者
class Receiver {
public void action() {
System.out.println("接收者的action()方法被调用...");
}
}
使用的时候:
Command cmd = new ConcreteCommand();
Invoker ir = new Invoker(cmd);
System.out.println("客户访问调用者的call()方法...");
ir.call();
命令模式通常适用于以下场景:
- 当系统需要将请求调用者与请求接收者解耦时,命令模式使得调用者和接收者不直接交互。
- 当系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现这些功能。
- 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
- 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。
十七、访问者模式
定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
优点:
- 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
- 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
- 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
缺点:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
主要角色:
- 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
- 抽象元素(Element)角色:声明一个包含接受操作
accept()
的接口,被接受的访问者对象作为accept()
方法的参数。 - 具体元素(ConcreteElement)角色:实现抽象元素角色提供的
accept()
操作,其方法体通常都是visitor.visit(this)
,另外具体元素中可能还包含本身业务逻辑的相关操作。 - 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
假设这样一种场景,有多种角色要使用多种物品,其中角色和物品都有可能变动,每个角色使用方式不一样,每种物品提供的功能也不一样,要怎么做设计?我们以电脑为例,假设有笔记本和台式机,使用者分为游戏玩家和程序猿:
public interface Visitor {
void visit(Laptop element);
void visit(Desktop element);
}
//具体访问者游戏玩家
class Gamer implements Visitor {
public void visit(Laptop element) {
System.out.println("游戏玩家使用笔记本-->" + element.use());
}
public void visit(Desktop element) {
System.out.println("游戏玩家使用台式机-->" + element.use());
}
}
//具体访问者程序猿
class Programmer implements Visitor {
public void visit(Laptop element) {
System.out.println("程序猿使用笔记本-->" + element.use());
}
public void visit(Desktop element) {
System.out.println("程序猿使用台式机-->" + element.use());
}
}
//抽象元素类
interface Computer {
void accept(Visitor visitor);
}
//具体元素笔记本类
class Laptop implements Computer {
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String use() {
return "使用笔记本的操作";
}
}
//具体元素台式机类
class Desktop implements Computer {
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String use() {
return "使用台式机的操作";
}
}
//对象结构角色
class ObjectStructure {
private List<Computer> list = new ArrayList<>();
public void accept(Visitor visitor) {
Iterator<Computer> i = list.iterator();
while (i.hasNext()) {
i.next().accept(visitor);
}
}
public void add(Computer element) {
list.add(element);
}
public void remove(Computer element) {
list.remove(element);
}
}
可以看到,无论使用者和物品怎么增加,只需要增加Visitor
和Computer
两个接口的对应实现类就可以了。
使用方式:
ObjectStructure os = new ObjectStructure();
os.add(new Desktop());
os.add(new Laptop());
Visitor visitor = new Programmer();
os.accept(visitor);
System.out.println("------------------------");
visitor = new Gamer();
os.accept(visitor);
通常在以下情况可以考虑使用访问者(Visitor)模式:
- 对象结构相对稳定,但其操作算法经常变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
- 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。
扩展:
访问者(Visitor)模式是使用频率较高的一种设计模式,它常常同以下两种设计模式联用。
(1)与“迭代器模式”联用。因为访问者模式中的“对象结构”是一个包含元素角色的容器,当访问者遍历容器中的所有元素时,常常要用迭代器。
(2)访问者(Visitor)模式同“组合模式”联用。因为访问者(Visitor)模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式)。
十八、迭代器模式
定义:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
优点:
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 遍历任务交由迭代器完成,这简化了聚合类。
- 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
- 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
- 封装性良好,为遍历不同的聚合结构提供一个统一的接口。
缺点:增加了类的个数,这在一定程度上增加了系统的复杂性。、
迭代器模式主要包含以下角色。
- 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
- 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
- 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
- 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
迭代器的原理几乎不用解释了,各种JDK
中的容器类里就常见。一个简单的实现如下:
public interface Iterator {
public boolean hasNext();
public Object next();
}
interface Container {
public Iterator getIterator();
}
class NameRepository implements Container {
public String names[] = {"Robert", "John", "Julie", "Lora"};
@Override
public Iterator getIterator() {
return new NameIterator();
}
private class NameIterator implements Iterator {
int index;
@Override
public boolean hasNext() {
if (index < names.length) {
return true;
}
return false;
}
@Override
public Object next() {
if (this.hasNext()) {
return names[index++];
}
return null;
}
}
}
使用方式:
NameRepository namesRepository = new NameRepository();
for(Iterator iter = namesRepository.getIterator(); iter.hasNext();){
String name = (String)iter.next();
System.out.println("Name : " + name);
}
应用场景:
- 当需要为聚合对象提供多种遍历方式时。
- 当需要为遍历不同的聚合结构提供一个统一的接口时。
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
十九、观察者模式
定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
优点:
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
- 目标与观察者之间建立了一套触发机制。
缺点:
- 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
- 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。
观察者模式的主要角色:
- 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
- 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
public interface Observer {
void response();
}
//具体观察者1
class ConcreteObserver1 implements Observer {
public void response() {
System.out.println("具体观察者1作出反应!");
}
}
//具体观察者1
class ConcreteObserver2 implements Observer {
public void response() {
System.out.println("具体观察者2作出反应!");
}
}//抽象目标
abstract class Subject {
protected List<Observer> observers = new ArrayList<Observer>();
//增加观察者方法
public void add(Observer observer) {
observers.add(observer);
}
//删除观察者方法
public void remove(Observer observer) {
observers.remove(observer);
}
public abstract void notifyObserver(); //通知观察者方法
}
//具体目标
class ConcreteSubject extends Subject {
public void notifyObserver() {
System.out.println("具体目标发生改变...");
System.out.println("--------------");
for (Object obs : observers) {
((Observer) obs).response();
}
}
}
使用方式:
Subject subject = new ConcreteSubject();
Observer obs1 = new ConcreteObserver1();
Observer obs2 = new ConcreteObserver2();
subject.add(obs1);
subject.add(obs2);
subject.notifyObserver();
也就是非常常见的生产-消费模式,适合以下几种情形:
- 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用
二十、中介者模式
定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
优点:
- 降低了对象之间的耦合性,使得对象易于独立地被复用。
- 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。
缺点:当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
中介者模式包含以下主要角色。
- 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
- 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
- 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
public abstract class Mediator {
public abstract void register(Colleague colleague);
public abstract void relay(Colleague cl); //转发
}
//具体中介者
class ConcreteMediator extends Mediator {
private List<Colleague> colleagues = new ArrayList<>();
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMedium(this);
}
}
public void relay(Colleague cl) {
for (Colleague ob : colleagues) {
if (!ob.equals(cl)) {
ob.receive();
}
}
}
}
//抽象同事类
abstract class Colleague {
protected Mediator mediator;
public void setMedium(Mediator mediator) {
this.mediator = mediator;
}
public abstract void receive();
public abstract void send();
}
//具体同事类
class ConcreteColleague1 extends Colleague {
public void receive() {
System.out.println("具体同事类1收到请求。");
}
public void send() {
System.out.println("具体同事类1发出请求。");
mediator.relay(this); //请中介者转发
}
}
//具体同事类
class ConcreteColleague2 extends Colleague {
public void receive() {
System.out.println("具体同事类2收到请求。");
}
public void send() {
System.out.println("具体同事类2发出请求。");
mediator.relay(this); //请中介者转发
}
}
使用的时候:
Mediator md = new ConcreteMediator();
Colleague c1, c2;
c1 = new ConcreteColleague1();
c2 = new ConcreteColleague2();
md.register(c1);
md.register(c2);
c1.send();
System.out.println("-------------");
c2.send();
应用场景:
- 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
- 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
扩展,实际开发中,通常采用以下两种方法来简化中介者模式,使开发变得更简单:
- 不定义中介者接口,把具体中介者对象实现成为单例。
- 同事对象不持有中介者,而是在需要的时f矣直接获取中介者对象并调用。
二十一、备忘录模式
定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
优点:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
缺点是:资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
主要角色如下:
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
//发起人
class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
public Memento createMemento() {
return new Memento(state);
}
public void restoreMemento(Memento m) {
this.setState(m.getState());
}
}
//管理者
class Caretaker {
private Memento memento;
public void setMemento(Memento m) {
memento = m;
}
public Memento getMemento() {
return memento;
}
}
使用方式:
Originator or=new Originator();
Caretaker cr=new Caretaker();
or.setState("S0");
System.out.println("初始状态:"+or.getState());
cr.setMemento(or.createMemento()); //保存状态
or.setState("S1");
System.out.println("新的状态:"+or.getState());
or.restoreMemento(cr.getMemento()); //恢复状态
System.out.println("恢复状态:"+or.getState());
应用场景:
- 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
- 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。
二十二、解释器模式
定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。
优点:
- 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。
缺点:
- 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
- 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
- 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。
解释器模式包含以下主要角色。
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
- 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
我们以创建一个组合表达式为例:
public interface Expression {
public boolean interpret(String context);
}
class TerminalExpression implements Expression {
private String data;
public TerminalExpression(String data) {
this.data = data;
}
@Override
public boolean interpret(String context) {
if (context.contains(data)) {
return true;
}
return false;
}
}
class OrExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public OrExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
class AndExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public AndExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) && expr2.interpret(context);
}
}
使用方式:
//规则:Robert 和 John 是男性
public static Expression getMaleExpression(){
Expression robert = new TerminalExpression("Robert");
Expression john = new TerminalExpression("John");
return new OrExpression(robert, john);
}
//规则:Julie 是一个已婚的女性
public static Expression getMarriedWomanExpression(){
Expression julie = new TerminalExpression("Julie");
Expression married = new TerminalExpression("Married");
return new AndExpression(julie, married);
}
public static void main(String[] args) {
Expression isMale = getMaleExpression();
Expression isMarriedWoman = getMarriedWomanExpression();
System.out.println("John is male? " + isMale.interpret("John"));
System.out.println("Julie is a married women? "
+ isMarriedWoman.interpret("Married Julie"));
}
应用场景:
- 当语言的文法较为简单,且执行效率不是关键问题时。
- 当问题重复出现,且可以用一种简单的语言来进行表达时。
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释。
注意:解释器模式在实际的软件开发中使用比较少,因为它会引起效率、性能以及维护等问题。如果碰到对表达式的解释,在 Java 中可以用 Expression4J 或 Jep 等来设计。
二十三、状态模式
定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
优点:
- 状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
- 减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
- 有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。
缺点:
- 状态模式的使用必然会增加系统的类与对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
状态模式包含以下主要角色:
- 环境(Context)角色:也称为上下文,它定义了客户感兴趣的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为。
//抽象状态类
public abstract class State {
public abstract void Handle(Context context);
}
class Context {
private State state;
//定义环境类的初始状态
public Context() {
this.state = new ConcreteStateA();
}
//设置新状态
public void setState(State state) {
this.state = state;
}
//读取状态
public State getState() {
return (state);
}
//对请求做处理
public void Handle() {
state.Handle(this);
}
}
//具体状态A类
class ConcreteStateA extends State {
public void Handle(Context context) {
System.out.println("当前状态是 A.");
context.setState(new ConcreteStateB());
}
}
//具体状态B类
class ConcreteStateB extends State {
public void Handle(Context context) {
System.out.println("当前状态是 B.");
context.setState(new ConcreteStateA());
}
}
使用方式:
ShareContext context=new ShareContext(); //创建环境
context.Handle(); //处理请求
context.Handle();
context.Handle();
context.Handle();
使用场景:
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
扩展:在有些情况下,可能有多个环境对象需要共享一组状态,这时需要引入享元模式,将这些具体状态对象放在集合中供程序共享
JDK 出现了哪些设计模式
这部分有些乱,没有顺序,基本是从网上找的或者自己想到的,想到哪写到哪里。
- Runtime 用到了单例模式。
- 用到了工厂模式的:
Integer.valueOf()
、Class.forName()
、反射包里的Array.newInstance()
等等。 - 虚拟工厂:
java.sql.DriverManager#getConnection()
、java.sql.Statement#executeQuery()
- 建造者模式:
StringBuilder#append()
、System.arraycopy
- 原型模式:
java.lang.Object
中返回一个浅拷贝对象,java.lang.Cloneable
- 桥接模式:
AWT
、JDBC
- 组合模式:各种容器类:
List
、Map
、Set
- 装饰器模式:
BufferedInputStream
、java.util.Collections#checked[List|Map|Set|SortedSet|SortedMap]()
- 适配器模式:
Arrays.asList()
、InputStreamReader
- 享元模式:
Integer.valueOf()
- 外观模式:
java.lang.Class
、javax.faces.webapp.FacesServlet
- 代理模式:
java.lang.reflect.Proxy
、RMI
- 责任链模式:
java.util.logging.Logger#log()
、javax.servlet.Filter#doFilter()
- 命令模式:
Runnable
- 解释器模式:
java.util.Pattern
、java.text.Format
- 迭代器器模式:
java.util.Iterator
- 中介者模式:
Timer
、java.util.concurrent.Executor#execute()
- 备忘录模式:
java.util.Date
、java.io.Serializable
- 访问者:
javax.lang.model.element.Element
和javax.lang.model.element.ElementVisitor
- 模板模式:
java.util.Collections#sort()
、java.util.AbstractList#indexOf()
- 策略模式:
java.util.Comparator#compare()
、HttpServlet
- 状态模式:
javax.faces.lifecycle.LifeCycle#execute()
、java.util.Iterator
- 观察者模式:
java.util.EventListener
、javax.servlet.http.HttpSessionAttributeListener
常见框架和类库中怎么用的
先以 Spring 为例,后续慢慢补充,这里仅作为一个索引,具体详细怎么用的后续再开文章补充吧。
- 工厂方法:
BeanFactory
、ApplicationContext
- 单例模式:
singleton
作用域的 bean - 代理模式:Spring AOP 就是基于动态代理的。如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib 。
- 模板方法:
jdbcTemplate
、hibernateTemplate
等等 - 观察者模式:
ApplicationEventPublisher
和ApplicationListener
- 适配器模式:
AdvisorAdapter
、HandlerAdapter
- 策略模式:
SimpleInstantiationStrategy
参考文章:
抽象工厂模式(Abstract Factory)- 最易懂的设计模式解析
java设计模式之建造者模式
设计模式概述
设计模式
JDK里的设计模式
面试官:“谈谈Spring中都用到了那些设计模式?”