设计模式之美(二)——设计模式

简介: 《设计模式之美》是极客时间上的一个代码学习系列,在学习之后特在此做记录和总结。

  《设计模式之美》是极客时间上的一个代码学习系列,在学习之后特在此做记录和总结。

  设计模式要干的事情就是解耦,也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。

  每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可以解决哪类问题;第二部分是解决方案,即这个模式的设计思路和具体的代码实现。不过,代码实现并不是模式必须包含的。如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉。


image.png


一、创建型


  创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

  单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。

1)单例模式

  单例设计模式(Singleton Design Pattern)是指一个类只允许创建一个对象(或者实例),那这个类就是一个单例类。


public class IdGenerator { 
  private static IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) {
      synchronized(IdGenerator.class) {     // 此处为类级别的锁
        if (instance == null) {
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
}

  (1)实战案例一:处理资源访问冲突

  将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。

  (2)实战案例二:表示全局唯一类

  从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类、唯一递增 ID 号码生成器。

  实现:要实现一个单例,需要关注的点无外乎下面几个:

  (1)构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;

  (2)考虑对象创建时的线程安全问题;

  (3)考虑是否支持延迟加载;

  (4)考虑 getInstance() 性能是否高(是否加锁)。

  问题:有些人认为单例是一种反模式(anti-pattern),并不推荐使用。

  (1)单例对 OOP 特性的支持不友好,对于其中的抽象、继承、多态都支持得不好。

  (2)单例会隐藏类之间的依赖关系,通过构造函数、参数传递等方式声明的类之间的依赖关系很容易分辨,但是,单例类不需要显示创建、不需要依赖参数传递。

  (3)单例对代码的扩展性不友好,单例类只能有一个对象实例。如果未来某一天,需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。

  (4)单例对代码的可测试性不友好,如果单例类依赖比较重的外部资源,比如 DB,由于单例类这种硬编码式的使用方式,导致无法实现 mock 替换。

  (5)单例不支持有参数的构造函数,比如创建一个连接池的单例对象,没法通过参数来指定连接池的大小。

  为了保证全局唯一,除了使用单例,还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。

2)工厂模式

  (1)简单工厂(Simple Factory)

  大部分工厂类都是以“Factory”这个单词结尾的,工厂类中创建对象的方法一般都是 create 开头。

public class RuleConfigParserFactory {
  public static IRuleConfigParser createParser(String configFormat) {
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(configFormat)) {
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(configFormat)) {
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(configFormat)) {
      parser = new YamlRuleConfigParser();
    } else if ("properties".equalsIgnoreCase(configFormat)) {
      parser = new PropertiesRuleConfigParser();
    }
    return parser;
  }
}


  (2)工厂方法(Factory Method)

  定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

  如果非得要将 if 分支逻辑去掉,那么比较经典处理方法就是利用多态。工厂方法模式比起简单工厂模式更加符合开闭原则。


public interface IRuleConfigParserFactory {
  IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new JsonRuleConfigParser();
  }
}
public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new XmlRuleConfigParser();
  }
}


  在工厂类的使用上,工厂类对象的创建逻辑又耦合进了 load() 函数中,跟最初的代码版本非常相似。

  可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。

  当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。

  而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。

3)建造者模式

  Builder 模式,即建造者模式、构建者模式或生成器模式。要解决下面这些问题,就需要建造者模式上场了。

  (1)如果可配置项逐渐增多,变成了 8 个、10 个,那么在使用构造函数的时候,就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。

  (2)如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。

  (3)如果配置项之间有约束条件,那么校验逻辑就无处安放了。

  (4)如果希望对象在创建好之后,就不能再修改内部的属性值,那么就不能暴露 set() 方法。

  可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后再使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。

  除此之外,把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样就只能通过建造者来创建 ResourcePoolConfig 类对象。并且,ResourcePoolConfig 没有提供任何 set() 方法,这样创建出来的对象就是不可变对象了。


public class ResourcePoolConfig {
  private String name;
  private int maxTotal;
  private ResourcePoolConfig(Builder builder) {
    this.name = builder.name;
    this.maxTotal = builder.maxTotal;
  }
  //...省略getter方法...
  //将Builder类设计成了ResourcePoolConfig的内部类。
  //也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
  public static class Builder {
    private static final int DEFAULT_MAX_TOTAL = 8;
    private String name;
    private int maxTotal = DEFAULT_MAX_TOTAL;
    public ResourcePoolConfig build() {
      // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
      if (StringUtils.isBlank(name)) {
        throw new IllegalArgumentException("...");
      }
      return new ResourcePoolConfig(this);
    }
    public Builder setName(String name) {
      if (StringUtils.isBlank(name)) {
        throw new IllegalArgumentException("...");
      }
      this.name = name;
      return this;
    }
    public Builder setMaxTotal(int maxTotal) {
      if (maxTotal <= 0) {
        throw new IllegalArgumentException("...");
      }
      this.maxTotal = maxTotal;
      return this;
    }
  }
}
// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
        .setName("dbconnectionpool")
        .setMaxTotal(16)
        .build();


  工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

  简单地说,工厂模式是根据不同的条件生成不同类的对象,建造者模式是根据不同参数生成一个类的不同对象。

4)原型模式

  如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。

  如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

  原型模式的实现方式:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)。80.jpg


  浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord 对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。

  实现深拷贝的两种方法:

  (1)第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。

  (2)第二种方法:先将对象序列化,然后再反序列化成新的对象。


二、结构型


  结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。

1)代理模式

  代理模式(Proxy Design Pattern)是指在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

  为了将框架代码和业务代码解耦,代理模式就派上用场了。

  UserController 类只负责业务功能。代理类 UserControllerProxy 负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码。


public interface IUserController {
  UserVo login(String telephone, String password);
  UserVo register(String telephone, String password);
}
public class UserController implements IUserController { }
public class UserControllerProxy implements IUserController {
  private MetricsCollector metricsCollector;
  private UserController userController;
  public UserControllerProxy(UserController userController) {
    this.userController = userController;
    this.metricsCollector = new MetricsCollector();
  }
  @Override
  public UserVo login(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    // 委托
    UserVo userVo = userController.login(telephone, password);
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    return userVo;
  }
}

  为了让代码改动尽量少,在刚刚的代理模式的代码实现中,代理类和原始类需要实现相同的接口。而对于外部类的扩展,一般都是采用继承的方式。

public class UserControllerProxy extends UserController { }

  所谓动态代理(Dynamic Proxy),就是不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。

  应用场景:

  (1)在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。

  (2)RPC 框架也可以看作一种代理模式,通过远程代理,将网络通信、数据编解码等细节隐藏起来。在 AOP 切面中完成接口缓存的功能。

2)桥接模式

  桥接模式(Bridge Design Pattern)也叫桥梁模式,对于这个模式有两种不同的理解方式。

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

  (2)一个类存在两个(或多个)独立变化的维度,通过组合的方式,让这两个(或多个)维度可以独立进行扩展。

  针对 Notification 的代码,将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender 相关类)。其中,Notification 类相当于抽象,MsgSender 类相当于实现,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,可以动态地去指定(比如,通过读取配置来获取对应关系)。


public interface MsgSender {
  void send(String message);
}
public class TelephoneMsgSender implements MsgSender {
  private List<String> telephones;
  public TelephoneMsgSender(List<String> telephones) {
    this.telephones = telephones;
  }
  @Override
  public void send(String message) {
    //...
  }
}
public class EmailMsgSender implements MsgSender {
  // 与TelephoneMsgSender代码结构类似,所以省略...
}
public abstract class Notification {
  protected MsgSender msgSender;
  public Notification(MsgSender msgSender) {
    this.msgSender = msgSender;
  }
  public abstract void notify(String message);
}
public class SevereNotification extends Notification {
  public SevereNotification(MsgSender msgSender) {
    super(msgSender);
  }
  @Override
  public void notify(String message) {
    msgSender.send(message);
  }
}
public class UrgencyNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}


3)装饰器模式

  装饰器模式(Decorator Design Pattern)相对于简单的组合关系,有两个比较特殊的地方。

  (1)装饰器类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰器类。

  (2)装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。

  代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

  代理模式偏重业务无关,高度抽象和稳定性较高的场景。装饰器模式偏重业务相关,定制化诉求高,改动较频繁的场景。

4)适配器模式

  适配器模式(Adapter Design Pattern)可将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

  适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。下面是使用的前提条件。

  (1)如果 Adaptee 接口并不多,那两种实现方式都可以。

  (2)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。

  (3)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那推荐使用对象适配器,因为组合结构相对于继承更加灵活。

  应用场景:

  适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。

  (1)封装有缺陷的接口设计。对外部系统提供的接口进行二次封装,抽象出更好的接口设计。

  (2)统一多个类的接口设计。将所有系统的接口适配为统一的接口定义。

  (3)替换依赖的外部系统。

  (4)兼容老版本接口。

  (5)适配不同格式的数据。

  代理、桥接、装饰器和适配器都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

  (1)代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

  (2)桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

  (3)装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

  (4)适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

5)门面模式

  门面模式(Facade Design Pattern)为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。子系统(subsystem)既可以是一个完整的系统,也可以是更细粒度的类或者模块。

  App 客户端的响应速度比较慢,排查之后发现,是因为过多的接口调用过多的网络通信。针对这种情况,就可以利用门面模式,让后端服务器提供一个包裹 a、b、d 三个接口调用的接口 x。App 客户端调用一次接口 x,来获取到所有想要的数据,将网络通信的次数从 3 次减少到 1 次,也就提高了 App 的响应速度。

  应用场景:

  (1)解决易用性问题,比如,Linux 系统调用函数、Shell 命令。

  (2)解决性能问题,如果门面接口特别多,并且很多都是跨多个子系统的,可将门面接口放到一个新的子系统中。

  (3)解决分布式事务问题,比如在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。

  与适配器模式的区别:

  (1)适配器模式是做接口转换,解决的是原接口和目标接口不匹配的问题。在代码结构上主要是继承加组合。

  (2)门面模式做接口整合,解决的是多接口调用带来的问题。在代码结构上主要是封装。

6)组合模式

  组合模式(Composite Design Pattern)跟面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。它主要是用来处理树形结构数据,其中数据可理解为一组对象集合。

  组合模式是将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(指代码的使用者)可以统一单个对象和组合对象的处理逻辑。

  对照着例子,重新定义:

  将一组对象(文件和目录)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(目录与子目录的嵌套结构)。组合模式让客户端可以统一单个对象(文件)和组合对象(目录)的处理逻辑(递归遍历)。

  实际上,组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树,业务需求可以通过在树上的递归遍历算法来实现。

7)享元模式

  所谓享元,顾名思义就是被共享的单元。享元模式(Flyweight Design Pattern)的意图是复用对象,节省内存,前提是享元对象是不可变对象。

  当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。对于相似对象,也可以将它相同的部分(字段)提取出来,设计成享元。

  “不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。

  所有的 ChessBoard 对象共享这 30 个 ChessPieceUnit 对象(因为象棋中只有 30 个棋子)。在使用享元模式之前,记录 1 万个棋局,要创建 30 万(30*1 万)个棋子的 ChessPieceUnit 对象。利用享元模式,只需要创建 30 个享元对象供所有棋局共享使用即可,大大节省了内存。


// 享元类
public class ChessPieceUnit {
  private int id;
  private String text;
  private Color color;
  public ChessPieceUnit(int id, String text, Color color) {
    this.id = id;
    this.text = text;
    this.color = color;
  }
  public static enum Color {
    RED, BLACK
  }
}
public class ChessPieceUnitFactory {
  private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
  static {
    pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
    pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
    //...省略摆放其他棋子的代码...
  }
  public static ChessPieceUnit getChessPiece(int chessPieceId) {
    return pieces.get(chessPieceId);
  }
}
public class ChessPiece {        //棋子
  private ChessPieceUnit chessPieceUnit;
  private int positionX;
  private int positionY;
  public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
    this.chessPieceUnit = unit;
    this.positionX = positionX;
    this.positionY = positionY;
  }
}
public class ChessBoard {        //棋局
  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
  public ChessBoard() {
    init();
  }
  private void init() {
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(1), 0,0));
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(2), 1,0));
    //...省略摆放其他棋子的代码...
  }
  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}


  实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。

  在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。


三、行为型


  创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。

  设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。

1)观察者模式

  观察者模式(Observer Design Pattern)也叫发布订阅模式(Publish-Subscribe Design Pattern),在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

  一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。观察者模式就是将观察者和被观察者代码解耦。


public interface Subject {
  void registerObserver(Observer observer);
  void removeObserver(Observer observer);
  void notifyObservers(Message message);
}
public interface Observer {
  void update(Message message);
}


  基于消息队列的实现方式,被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。

2)模板模式

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

  这里的“算法”,可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。


public abstract class AbstractClass {
  public final void templateMethod() {
    //...
    method1();
    //...
    method2();
    //...
  }
  protected abstract void method1();
  protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
  @Override
  protected void method1() {}
  @Override
  protected void method2() {}
}
public class ConcreteClass2 extends AbstractClass {
  @Override
  protected void method1() {}
  @Override
  protected void method2() {}
}


  模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。

  (1)作用一:复用

  模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。

  (2)作用二:扩展

  这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似之前讲到的控制反转。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。

3)策略模式

  策略模式(Strategy Design Pattern)定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(使用算法的代码)。

  策略模式解耦的是策略的定义、创建、使用这三部分。让每个部分都不至于过于复杂、代码量过多。

  (1)策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。

  (2)通过类型(type)来判断创建哪个策略。可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。

  (3)运行时动态确定使用哪种策略,即在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。

  利用策略模式避免分支判断。将不同类型订单的打折策略设计成策略类,并由工厂类来负责创建策略对象。


// 策略的定义
public interface DiscountStrategy {
  double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...
// 策略的创建
public class DiscountStrategyFactory {
  private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();
  static {
    strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
    strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
    strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
  }
  public static DiscountStrategy getDiscountStrategy(OrderType type) {
    return strategies.get(type);
  }
}
// 策略的使用
public class OrderService {
  public double discount(Order order) {
    OrderType type = order.getType();
    DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
    return discountStrategy.calDiscount(order);
  }
}


  策略模式侧重“策略”或“算法”这个特定的应用场景,用来解决根据运行时状态从一组策略中选择不同策略的问题,而工厂模式侧重封装对象的创建过程,这里的对象没有任何业务场景的限定,可以是策略,但也可以是其他东西。

4)职责链模式

  职责链模式(Chain Of Responsibility Design Pattern)是将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

  在职责链模式中,多个处理器(接收对象)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。


public abstract class Handler {        //模板模式
  protected Handler successor = null;
  public void setSuccessor(Handler successor) {
    this.successor = successor;
  }
  public final void handle() {
    boolean handled = doHandle();
    if (successor != null && !handled) {
      successor.handle();
    }
  }
  protected abstract boolean doHandle();
}
public class HandlerA extends Handler {
  @Override
  protected boolean doHandle() {
    boolean handled = false;
    //...
    return handled;
  }
}
public class HandlerB extends Handler {
  @Override
  protected boolean doHandle() {
    boolean handled = false;
    //...
    return handled;
  }
}
public class HandlerChain {
  private Handler head = null;
  private Handler tail = null;
  public void addHandler(Handler handler) {
    handler.setSuccessor(null);
    if (head == null) {
      head = handler;
      tail = handler;
      return;
    }
    tail.setSuccessor(handler);
    tail = handler;
  }
  public void handle() {
    if (head != null) {
      head.handle();
    }
  }
}
public class Application {        //使用举例
  public static void main(String[] args) {
    HandlerChain chain = new HandlerChain();
    chain.addHandler(new HandlerA());
    chain.addHandler(new HandlerB());
    chain.handle();
  }
}


  职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。这种变体也有两种实现方式:用链表存储处理器和用数组存储处理器。

  为什么非要使用职责链模式呢?这是不是过度设计呢?

  (1)应对代码的复杂性,用职责链模式把各个敏感词过滤函数继续拆分出来,设计成独立的类,进一步简化了 SensitiveWordFilter 类,让 SensitiveWordFilter 类的代码不会过多,过复杂。

  (2)满足开闭原则,当要扩展新的过滤算法时,只需要新添加一个 Filter 类,并且通过 addFilter() 函数将它添加到 FilterChain 中即可,其他代码完全不需要修改。

5)状态模式

  状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。

  有限状态机(Finite State Machine,FSM),简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。


81.jpg


  (1)状态机实现方式一:分支逻辑法

  参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以这种方法暂且命名为分支逻辑法。

  (2)状态机实现方式二:查表法

  把这两个二维数组存储在配置文件中,当需要修改状态机时,甚至可以不修改任何代码,只需要修改配置文件就可以了。

  (3)状态机实现方式三:状态模式

  如果要执行的动作是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),那么查表法就不合适了。

  状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。


public interface IMario {         //所有状态类的接口
  void obtainMushRoom();
  void obtainCape();
}
public class SmallMario implements IMario {
  private MarioStateMachine stateMachine;
  public SmallMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }
  @Override
  public void obtainMushRoom() {
    stateMachine.setCurrentState(new SuperMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 100);
  }
  @Override
  public void obtainCape() {
    stateMachine.setCurrentState(new CapeMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 200);
  }
}
public class SuperMario implements IMario {
  private MarioStateMachine stateMachine;
  public SuperMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }
  @Override
  public void obtainMushRoom() {
    // do nothing...
  }
  @Override
  public void obtainCape() {
    stateMachine.setCurrentState(new CapeMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 200);
  }
}
public class MarioStateMachine {
  private int score;
  private IMario currentState;         //不再使用枚举来表示状态
  public MarioStateMachine() {
    this.score = 0;
    this.currentState = new SmallMario(this);
  }
  public void obtainMushRoom() {
    this.currentState.obtainMushRoom();
  }
  public void obtainCape() {
    this.currentState.obtainCape();
  }
  public void setScore(int score) {
    this.score = score;
  }
  public void setCurrentState(IMario currentState) {
    this.currentState = currentState;
  }
}


6)迭代器模式

  迭代器模式(Iterator Design Pattern)也叫游标模式(Cursor Design Pattern)用来遍历集合对象。

  这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。

  迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。

  一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。

public interface Iterator<E> {
  boolean hasNext();
  void next();
  E currentItem();
}

  总结下来就三句话:迭代器中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。

  为什么还要用迭代器来遍历容器呢?为什么还要给容器设计对应的迭代器呢?

  (1)复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。

  (2)将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。

  (3)容器和迭代器都提供了抽象的接口,方便在开发时基于接口而非具体的实现编程。

  在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。

7)访问者模式

  访问者者模式(Visitor Design Pattern)允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。

  访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。

  在不同的应用场景下,需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。

  对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的。

8)备忘录模式

  备忘录模式(Memento Design Pattern)也叫快照(Snapshot)模式,在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

  备忘录模式主要是用来防丢失、撤销、恢复等。

  其一,定义一个独立的类(Snapshot 类)来表示快照,而不是复用 InputText 类。这个类只暴露 get() 方法,没有 set() 等任何修改内部状态的方法。

  其二,在 InputText 类中,把 setText() 方法重命名为 restoreSnapshot() 方法,用意更加明确,只用来恢复对象。


public class InputText {
  private StringBuilder text = new StringBuilder();
  public String getText() {
    return text.toString();
  }
  public void append(String input) {
    text.append(input);
  }
  public Snapshot createSnapshot() {
    return new Snapshot(text.toString());
  }
  public void restoreSnapshot(Snapshot snapshot) {
    this.text.replace(0, this.text.length(), snapshot.getText());
  }
}
public class Snapshot {
  private String text;
  public Snapshot(String text) {
    this.text = text;
  }
  public String getText() {
    return this.text;
  }
}
public class SnapshotHolder {
  private Stack<Snapshot> snapshots = new Stack<>();
  public Snapshot popSnapshot() {
    return snapshots.pop();
  }
  public void pushSnapshot(Snapshot snapshot) {
    snapshots.push(snapshot);
  }
}


9)命令模式

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

  在大部分编程语言中,函数没法作为参数传递给其他函数,也没法赋值给变量。借助命令模式,可以将函数封装成对象。设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。

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

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

10)解释器模式

  解释器模式(Interpreter Design Pattern)只在一些特定的领域会被用到,比如编译器、规则引擎、正则表达式。它能为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

11)中介模式

  中介模式(Mediator Design Pattern)定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

  中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。

  假设有一个比较复杂的对话框,对话框中有很多控件,比如按钮、文本框、下拉框等。当对某个控件进行操作的时候,其他控件会做出相应的反应,比如,在下拉框中选择“注册”,注册相关的控件就会显示在对话框中。


public interface Mediator {
  void handleEvent(Component component, String event);
}
public class LandingPageDialog implements Mediator {
  private Button loginButton;
  private Button regButton;
  private Selection selection;
  private Input usernameInput;
  private Input passwordInput;
  private Input repeatedPswdInput;
  private Text hintText;
  @Override
  public void handleEvent(Component component, String event) {
    if (component.equals(loginButton)) {
      String username = usernameInput.text();
      String password = passwordInput.text();
      //校验数据...
      //做业务处理...
    } else if (component.equals(regButton)) {
      //获取usernameInput、passwordInput、repeatedPswdInput数据...
      //校验数据...
      //做业务处理...
    } else if (component.equals(selection)) {
      String selectedItem = selection.select();
      if (selectedItem.equals("login")) {
        usernameInput.show();
        passwordInput.show();
        repeatedPswdInput.hide();
        hintText.hide();
        //...省略其他代码
      } else if (selectedItem.equals("register")) {
        //....
      }
    }
  }
}


  好处是简化了控件之间的交互,坏处是中介类有可能会变成大而复杂的“上帝类”(God Class)。所以,在使用中介模式的时候,要根据实际的情况,平衡对象之间交互的复杂度和中介类本身的复杂度。

  中介模式和观察者模式的区别在哪里呢?

  (1)在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。

  (2)而中介模式正好相反。参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
7月前
|
设计模式 SQL Java
设计模式之为什么要学好设计模式
设计模式之为什么要学好设计模式
23 1
|
8月前
|
设计模式 SQL Java
为什么一定要学习设计模式
先来看一个生活案例,当我们开心时,也许会寻求享乐。在学习设计模式之前,你可能会这样感叹:
37 0
|
9月前
|
设计模式 算法
设计模式学习
设计模式学习
|
10月前
|
设计模式
|
设计模式 自然语言处理 Java
重新学习设计模式一:什么是设计模式
一直以来,设计模式是一个令人头疼的课题,记得之前在A公司做智能客服项目时,刚开始只是一个小项目,不管什么设计模式,系统架构,全程直接上手敲业务代码,两三天时间就把所有的代码敲完上线使用,结果谁也没想到突然项目大起来了,十几个业务部门的业务一拥而上,开始招人,上手业务,结果。。。大家都是苦力干嘛,拼命加班,拼命填坑,十几个人的代码乱七八糟,大量重复业务,重复代码,单简单的一样表单业务查询就有三四不同的版本,新来的员工也在抱怨没学到任何技术,倒学会怎么跟业务吵架,那日子实在是不忍直视。。。
71 0
|
设计模式 数据可视化 程序员
设计模式日常学习(一)
设计模式 软件设计模式的概念 软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。 学习设计模式的必要性 设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。 正确使用设计模式具有以下优点。 ● 可以提高程序员的思维能力、编程能力和设计能力。 ● 使程序设计更加标准化、代
85 0
|
设计模式 存储 Java
设计模式日常学习(三)
创建者模式 二.工厂模式 在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。 ● 简单工厂模式(不属于GOF的23种经典设计模式) ● 工厂方法模式 ● 抽象工厂模式 1.简单工厂模式 简单工厂不是一种设计模式,反而比较像是一种编程习惯。 1.1 结构 简单工厂包含如下角色: ●
72 0
|
设计模式 算法 安全
设计模式日常学习(四)
三.原型模式 概述 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。 结构 原型模式包含如下角色: ● 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。 ● 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。 ● 访问类:使用具体原型类中的 clone() 方法来复制新的对象。 实现 原型模式的克隆分为浅克隆和深克隆。 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。 Ja
53 0
|
设计模式 存储 安全
设计模式日常学习(五)
五.结构型模式 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: ● 代理模式 ● 适配器模式 ● 装饰者模式 ● 桥接模式 ● 外观模式 ● 组合模式 ● 享元模式 5.1.代理模式 5.1.1 概述 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
93 0
|
存储 设计模式 算法
设计模式日常学习(六)
六.行为型模式 行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。 行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: ● 模板方法模式 ● 策略模式 ● 命令模式 ● 职责链模式 ● 状态模式 ● 观察者模式 ● 中介者模式 ● 迭代器模式 ● 访问者模式 ● 备忘录模式 ● 解释器模式 以上 11 种行为型模
59 0