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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 《设计模式之美》是极客时间上的一个代码学习系列,在学习之后特在此做记录和总结。

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

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

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


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日志并进行多维度分析。
相关文章
|
6月前
|
设计模式 算法 架构师
【搞懂设计模式】设计模式与面向对象原则
【搞懂设计模式】设计模式与面向对象原则
65 1
|
6月前
|
设计模式 算法 Java
【设计模式系列笔记】设计模式与设计原则
设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。 设计原则是一些通用的设计指导方针,它们提供了如何设计一个优秀的软件系统的基本思想和规则。指导着设计者如何组织代码以实现高内聚、低耦合、易扩展和易维护的软件系统。
83 4
|
6月前
|
设计模式 开发者
【设计模式】设计模式综述
【1月更文挑战第12天】【设计模式】设计模式综述
|
设计模式 算法 安全
深入浅出设计模式 - 策略模式
深入浅出设计模式 - 策略模式
126 0
深入浅出设计模式 - 策略模式
|
设计模式 SQL 缓存
【设计模式】【第九章】【设计模式小结】
单一职责原则:一个类只负责一个功能领域中的相应职责 开闭原则:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展
|
设计模式 存储 安全
设计模式日常学习(五)
五.结构型模式 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: ● 代理模式 ● 适配器模式 ● 装饰者模式 ● 桥接模式 ● 外观模式 ● 组合模式 ● 享元模式 5.1.代理模式 5.1.1 概述 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
117 0
|
设计模式 数据可视化 程序员
设计模式日常学习(一)
设计模式 软件设计模式的概念 软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。 学习设计模式的必要性 设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。 正确使用设计模式具有以下优点。 ● 可以提高程序员的思维能力、编程能力和设计能力。 ● 使程序设计更加标准化、代
114 0
|
设计模式 算法 安全
设计模式日常学习(四)
三.原型模式 概述 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。 结构 原型模式包含如下角色: ● 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。 ● 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。 ● 访问类:使用具体原型类中的 clone() 方法来复制新的对象。 实现 原型模式的克隆分为浅克隆和深克隆。 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。 Ja
74 0
|
存储 设计模式 算法
设计模式日常学习(七)
6.5 状态模式 6.5.1 概述 【例】通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。 类图如下: public interface ILift { //电梯的4个状态 //开门状态 public final static int OPENING_STATE = 1; //关门状态 public final static int CLOSING_STATE = 2;
108 0
|
设计模式 自然语言处理 Java
重新学习设计模式一:什么是设计模式
一直以来,设计模式是一个令人头疼的课题,记得之前在A公司做智能客服项目时,刚开始只是一个小项目,不管什么设计模式,系统架构,全程直接上手敲业务代码,两三天时间就把所有的代码敲完上线使用,结果谁也没想到突然项目大起来了,十几个业务部门的业务一拥而上,开始招人,上手业务,结果。。。大家都是苦力干嘛,拼命加班,拼命填坑,十几个人的代码乱七八糟,大量重复业务,重复代码,单简单的一样表单业务查询就有三四不同的版本,新来的员工也在抱怨没学到任何技术,倒学会怎么跟业务吵架,那日子实在是不忍直视。。。
101 0