CS《设计模式之美》笔记

  1. SOLID设计原则
  2. 创建型
    1. 单例模式
      1. 使用用途
      2. 实现方案
      3. 示例代码1:饿汉式
      4. 示例代码2:懒汉式
      5. 示例代码3:双检锁
      6. 示例代码4:静态内部类
      7. 示例代码5:枚举
    2. 工厂模式
      1. 使用用途
      2. 实现方案
      3. 代码示例1:简单工厂
      4. 代码示例2:工厂方法
      5. 代码示例3:抽象工厂
    3. 建造者模式
      1. 使用用途
      2. 实现方案
      3. 示例代码
    4. 原型模式
      1. 使用用途
      2. 实现方案
      3. 示例代码1:更新缓存
      4. 示例代码2:简单的克隆
  3. 结构型
    1. 代理模式
      1. 代码实现
      2. 静态代理
        1. 使用用途
        2. 实现方案1:接口方式
        3. 使用举例1:接口方式
        4. 实现方案2:继承方式
        5. 使用举例2:继承方式
      3. 动态代理
        1. 使用用途
        2. 实现方案1:jdk动态代理
        3. 使用示例1:jdk动态代理
        4. 实现方案2:动态字节码
        5. 使用示例2:动态字节码
      4. 扩展:常用代理方式总结
    2. 桥接模式
      1. 代码实现
      2. 理解方式1:让多个维度根据抽象类及接口独立进行扩展
        1. 使用用途
        2. 设计目标
        3. 实现方案
        4. 使用举例1
        5. 使用举例2
      3. 理解方式2:将抽象和实现解耦,让它们可以独立变化
        1. 使用举例
    3. 装饰器模式
      1. 使用用途
      2. 代码实现
      3. 实现方案
      4. 使用示例
    4. 适配器模式
      1. 使用用途
      2. 代码实现
      3. 实现方案
      4. 使用示例:
    5. Wrapper 模式:代理、装饰器、桥接、适配器对比
    6. 门面模式
      1. 使用用途
      2. 实现方案
      3. 使用示例
    7. 组合模式
      1. 使用用途
      2. 实现方案
      3. 代码示例
    8. 享元模式
      1. 使用用途
      2. 实现方案
      3. 代码示例1:象棋大厅
      4. 代码示例2:文本编辑器
      5. 享元模式 vs 单例、缓存、对象池
      6. 扩展Integer和String的享元
  4. 行为型
    1. 观察者模式
      1. 使用用途
      2. 实现方案
      3. 代码示例1:同步阻塞方式
      4. 代码示例2:异步非阻塞方式
    2. 模板模式
      1. 使用用途
      2. 实现方式
      3. 代码示例
    3. 策略模式
      1. 使用用途
      2. 实现方案
      3. 使用示例
    4. 责任链模式
      1. 使用用途
      2. 实现方案
      3. 代码示例1:链表方式
      4. 代码示例2:数组方式
    5. 状态模式
      1. 使用用途
      2. 实现方案
      3. 代码示例1:状态模式
      4. 代码示例2:分支逻辑法
      5. 代码示例3:查表法
    6. 迭代器模式
      1. 使用用途
      2. 实现方案
      3. 代码示例
    7. 访问者模式
      1. 使用用途
      2. 实现方案
      3. 示例代码:用各种逻辑处理各种格式文本
      4. 扩展:双分派语言
    8. 备忘录模式
      1. 使用用途
      2. 实现方案
      3. 代码示例
    9. 命令模式
      1. 使用用途
      2. 实现方案
      3. 代码示例1
      4. 代码示例2
    10. 解释器模式
      1. 使用用途
      2. 实现方案
      3. 代码示例
    11. 中介模式
      1. 使用用途
      2. 实现方案
      3. 代码示例
      4. 扩展:中介模式 VS 观察者模式

主要参考资料

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

SOLID设计原则

  • S:单一职责。
  • O:开闭原则。“对扩展开放、修改关闭”。
  • L:里氏代换。子类可替代父类。跟多态不同的是,子类不会改变父类的行为。
  • I:接口隔离。不依赖不需要的接口。例如将函数或类拆为更细粒度的接口。单一职责侧重于接口设计,接口隔离则从调用者角度考虑
  • D:依赖反转。控制权由程序员交给系统。

创建型

主要解决“对象的创建”问题。焦点是如何创建对象。用于取代new来创建对象

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

单例模式

一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。

使用用途

  • 处理资源访问冲突:如日志多线程向同一个文件写数据,加类级别锁可以解决冲突,但是每个线程都要新建一个对象,太浪费了。
  • 表示全局唯一类:比如,配置信息类,唯一递增 ID 号码生成器。

单例模式的缺点是对扩展性支持不好,因为是写死的,将来要扩展的话很难扩展,测试时也很难mock单例模式可以用IOC、工厂等方式替代。

实现方案

  • 1.私有化构造方法,并提供获取对象的方法getInstance()
  • 2.考虑是否支持延迟加载:饿汉式。直接new()
  • 3.考虑对象创建时的线程安全问题:懒汉式。直接在getInstance()上加类级锁
  • 4.考虑 getInstance() 性能是否高(是否加锁):双检锁。减小锁粒度,两次判null。其他:内部类,枚举。

示例代码1:饿汉式

最简单的,对象在初始化时就new好。不支持延迟加载。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

示例代码2:懒汉式

支持延迟加载,加了判空操作,并加类级别锁。

缺点是加了类级别锁,性能很差。如果这个对象是频繁使用的,那么相当于串行操作了。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance;
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

示例代码3:双检锁

用双检锁提高了性能。双检的意思是判断为空判断了两次,以此不加锁的判断,一次加锁的判断。

判断空时不加锁,判断为空后新建对象时才加类级别的锁。

注意加了volatile关键字。因为老版本的java,new一个对象不是原子操作。新版本已解决该问题,不用加volatile

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static volatile final IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) {
        synchronized (IdGenerator.class) {
           if (instance == null) { 
               instance = new IdGenerator();
           } 
        }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

示例代码4:静态内部类

达到了双检锁延迟加载、高性能的效果,但是比双检锁更简单。

把对象放到了静态内部类里。这样就实现了懒加载。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {}
  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

示例代码5:枚举

最简单优雅的方式

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

工厂模式

分为简单工厂、工厂、抽象工厂。

使用用途

创建一组相似对象。

实现方案

  • 简单工厂:也叫静态工厂。很简单的把创建逻辑封装到工厂方法即可。创建方法是静态的。原始类可以用接口抽象。
  • 工厂方法:对工厂再进行一次抽象,使用时先用工厂的工厂对象取得工厂,再用工厂取得原始类对象。工厂方法最主要的还是进一步解耦创建逻辑,避免所有逻辑都丢到简单工厂,让简单工厂过于庞大。
  • 抽象工厂:用得比较少。一个抽象工厂类,多个生成方法,每个生成方法对应一种生成对象的类别(接口)。其实就是,原本是每个类型对应一个工厂,每个工厂有一个create()方法。现在是把多个类型的工厂合并成一个工厂,这个工厂也就对应了多个ctreate()方法。具体实现:一个抽象工厂,有多个create()方法,每个create()方法对应一种对象

下面是把配置文件加载到内存的代码。配置文件有多种格式,需要不同格式的转换器。

代码示例1:简单工厂

public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
    if (parser == null) {
      throw new InvalidRuleConfigException(
              "Rule config file format is not supported: " + ruleConfigFilePath);
    }
    String configText = "";
    //从ruleConfigFilePath文件中读取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }
  private String getFileExtension(String filePath) {
    //...解析文件名获取扩展名,比如rule.json,返回json
    return "json";
  }
}
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;
  }
}
// 也可以实现把原始类的对象缓存起来
public class RuleConfigParserFactory {
  private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
  static {
    cachedParsers.put("json", new JsonRuleConfigParser());
    cachedParsers.put("xml", new XmlRuleConfigParser());
    cachedParsers.put("yaml", new YamlRuleConfigParser());
    cachedParsers.put("properties", new PropertiesRuleConfigParser());
  }
  public static IRuleConfigParser createParser(String configFormat) {
    if (configFormat == null || configFormat.isEmpty()) {
      return null;//返回null还是IllegalArgumentException全凭你自己说了算
    }
    IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
    return parser;
  }
}

以上代码,如果要增加新类型的Parser,那么需要改代码。

那么我们可以利用多态,进一步抽象工厂。这样带有新的Parser时,我们扩展新的工厂类就行,不用改代码。然后由于工厂也进行了抽象,所以再新建一个工厂的工厂,用来获取具体的工厂。也就是下面的工厂方法。

代码示例2:工厂方法

// 抽象工厂类
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();
  }
}
public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new YamlRuleConfigParser();
  }
}
public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new PropertiesRuleConfigParser();
  }
}
// 增加工厂的工厂
//因为工厂类只包含方法,不包含成员变量,完全可以复用,
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
public class RuleConfigParserFactoryMap { //工厂的工厂
  private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
  static {
    cachedFactories.put("json", new JsonRuleConfigParserFactory());
    cachedFactories.put("xml", new XmlRuleConfigParserFactory());
    cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
    cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
  }
  public static IRuleConfigParserFactory getParserFactory(String type) {
    if (type == null || type.isEmpty()) {
      return null;
    }
    IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
    return parserFactory;
  }
}
// 最终使用:
public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);	// 注意这里,获取工厂
    if (parserFactory == null) {
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    }
    IRuleConfigParser parser = parserFactory.createParser(); // 注意这里,获取对象
    String configText = "";
    //从ruleConfigFilePath文件中读取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }
  private String getFileExtension(String filePath) {
    //...解析文件名获取扩展名,比如rule.json,返回json
    return "json";
  }
}

当我们需要添加新的规则配置解析器的时候,我们只需要创建新的 parser 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。

代码示例3:抽象工厂

用的比较少。可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。

public interface IConfigParserFactory {
  IRuleConfigParser createRuleParser();
  ISystemConfigParser createSystemParser();
  //此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
  @Override
  public IRuleConfigParser createRuleParser() {
    return new JsonRuleConfigParser();
  }
  @Override
  public ISystemConfigParser createSystemParser() {
    return new JsonSystemConfigParser();
  }
}
public class XmlConfigParserFactory implements IConfigParserFactory {
  @Override
  public IRuleConfigParser createRuleParser() {
    return new XmlRuleConfigParser();
  }
  @Override
  public ISystemConfigParser createSystemParser() {
    return new XmlSystemConfigParser();
  }
}
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码

建造者模式

也叫生成器模式。

使用用途

用于对象的建造及初始化过程比较复杂时。例如对属性字段的必填/非必填、默认值要求很多,需要初始化的属性字段也很多,而且可能各个字段的值还有相互制约关系等等。此时,构造函数的参数列表会很长,而且各种默认值的设置逻辑也很多,各个属性间还相互有联系,整个构建过程比较复杂。

实际上,使用建造者模式创建对象,还能避免对象存在无效状态。我再举个例子解释一下。比如我们定义了一个长方形类,如果不使用建造者模式,采用先创建后 set 的方式,那就会导致在第一个 set 之后,对象处于无效状态。为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,我们就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态

典型的lombok插件的@Build就是建造者模式。

实现方案

专门新写一个建造者类,将原始类的初始化逻辑放到**build()方法**,调用build()方法以取代原始类的用new调用构造方法。

调用build()前,先调用**set()方法**设置各种属性初始化参数,然后最后调用build()方法一次完成所有的初始化。

建造者类可以独立写,也可以作为原始类的内部类。

使用方式:

new Builder().setXX(xxx).setXX(xxx).setXX(xxx).setXX(xxx).build();

示例代码

以下是对资源池的建造:

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

原型模式

使用用途

用于快速创建成本比较大的对象。分为深拷贝浅拷贝两种方式。

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

创建成本大说的是,对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO中读取等。

实现方案

具体实现:实现cloneable接口,然后重写clone()方法即可(不重写会报错)。用super.clone()调用父类clone()方法。根据浅拷贝或深拷贝需求,按需重写clone()逻辑。

Object的clone()方法是一个native方法,不实现Cloneable接口而直接调用它的话会抛出一样。另外Cloneable接口只是一个声明接口,内部没有实现任何方法。

在 Java 中,Object 类的 clone() 是浅拷贝。即copy()对象时,对象的非基本类型字段是用的浅拷贝。

深拷贝需要手动重写clone()方法,并递归拷贝对象至基本数据类型。*或者先将对象序列化,然后再反序列化成新的对象(流或Json轮子)*。深拷贝apache.common包也有现成的轮子。

示例代码1:更新缓存

以下是更新缓存的示例,有浅拷贝、深拷贝(递归/反序列化及序列化)几种方式。

浅拷贝:

public class Demo {
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;
  public void refresh() {
    // 原型模式就这么简单,拷贝已有对象的数据,更新少量差值
    HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (newKeywords.containsKey(searchWord.getKeyword())) {
        SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
        oldSearchWord.setCount(searchWord.getCount());
        oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
      } else {
        newKeywords.put(searchWord.getKeyword(), searchWord);
      }
    }
    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }
  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

深拷贝:

递归拷贝对象方式:

public class Demo {
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;
  public void refresh() {
    // Deep copy
    HashMap<String, SearchWord> newKeywords = new HashMap<>();
    for (HashMap.Entry<String, SearchWord> e : currentKeywords.entrySet()) {
      SearchWord searchWord = e.getValue();
      SearchWord newSearchWord = new SearchWord(
              searchWord.getKeyword(), searchWord.getCount(), searchWord.getLastUpdateTime());
      newKeywords.put(e.getKey(), newSearchWord);
    }
    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (newKeywords.containsKey(searchWord.getKeyword())) {
        SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
        oldSearchWord.setCount(searchWord.getCount());
        oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
      } else {
        newKeywords.put(searchWord.getKeyword(), searchWord);
      }
    }
    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }
  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

序列化及反序列化方式:

public Object deepCopy(Object object) {
  ByteArrayOutputStream bo = new ByteArrayOutputStream();
  ObjectOutputStream oo = new ObjectOutputStream(bo);
  oo.writeObject(object);
  
  ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
  ObjectInputStream oi = new ObjectInputStream(bi);
  
  return oi.readObject();
}

示例代码2:简单的克隆

实现Cloneable接口,重写clone()方法,用super.clone()调用父类克隆方法:

public abstract class Shape implements Cloneable {
   
   private String id;
   protected String type;
   
   abstract void draw();
   
   public String getType(){
      return type;
   }
   
   public String getId() {
      return id;
   }
   
   public void setId(String id) {
      this.id = id;
   }
   
   public Object clone() {
      Object clone = null;
      try {
         clone = super.clone();
      } catch (CloneNotSupportedException e) {
         e.printStackTrace();
      }
      return clone;
   }
}

结构型

主要解决“类或对象的组合或组装”问题焦点是如何用接口、抽象类进行多态、继承、组合,以实现良好的扩展性

代理模式

用于在不改变原始类的情况下,给原始类附加功能。具体使用时,用代理类替代原始类即可。

代码实现

代理/装饰器:

  • 代理和被代理对象实现统一的接口,代理对象组合被代理对象。
  • 代理可动态代理。
  • 装饰可嵌套装饰。

静态代理

使用用途

在不改变原始类的情况下,给原始类附加功能

实现方案1:接口方式
  • 原始类和代理类实现同一个接口
  • 代理类持有原始类对象
  • 代理类在具体实现的方法里面,加入自己的代理逻辑,原始逻辑可引用原始类的对应的方法。
  • 具体使用时用代理类代替原始类即可
使用举例1:接口方式

Controller层,在登录和注册方法中,加上接口调用计时功能。

//===== 1.共同接口
public interface IUserController {
  UserVo login(String telephone, String password);
  UserVo register(String telephone, String password);
}

//===== 2.原始类实现共同接口
public class UserController implements IUserController {
  //...省略其他属性和方法...
  @Override
  public UserVo login(String telephone, String password) {
    //...省略login逻辑...
    //...返回UserVo数据...
  }
  @Override
  public UserVo register(String telephone, String password) {
    //...省略register逻辑...
    //...返回UserVo数据...
  }
}

//===== 3.代理类实现共同接口,并在相应方法里引用原始类方法,以及重写逻辑。
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;
  }
  @Override
  public UserVo register(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    UserVo userVo = userController.register(telephone, password);
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    return userVo;
  }
}
//UserControllerProxy使用举例
//因为原始类和代理类实现相同的接口,是基于接口而非实现编程
//将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userController = new UserControllerProxy(new UserController());
实现方案2:继承方式
  • 有时候原始类是已经写好的jar包,我们无法使用接口。这个时候我们就用继承来取代接口。
  • 代理类继承原始类,并重写原始类的方法即可。在重写的方法里加入自己的代理逻辑,原始逻辑可用super引用原始类的方法。
  • 具体使用时用代理类代替原始类即可
使用举例2:继承方式

与接口方式大同小异,思想是相通的。

public class UserControllerProxy extends UserController {
  private MetricsCollector metricsCollector;
  public UserControllerProxy() {
    this.metricsCollector = new MetricsCollector();
  }
  public UserVo login(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    UserVo userVo = super.login(telephone, password);
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    return userVo;
  }
  public UserVo register(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    UserVo userVo = super.register(telephone, password);
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    return userVo;
  }
}
//UserControllerProxy使用举例
UserController userController = new UserControllerProxy();

动态代理

动态代理内容可参照AOP相关笔记。以下内容基本上是从AOP笔记直接搬过来的。

使用用途

用来简化静态代理。静态代理有以下缺点:1.需要为每个原始类都创建一个代理类。假设有一万个原始类需要代理,就要创建一万个代理类。2.代理类需要把原始类中的所有的方法都重新实现一遍,假设原始类中有一万个方法需要代理,就要重新实现一万个方法。

不事先为原始类添加代理类。而是运行的时候,动态添加代理类,然后用代理类替换原始类

实现方案1:jdk动态代理

实现时底层就是使用Java的jdk动态代理包,更底层则是Java的反射。

具体实现为:1.实现InvocationHandler接口的invoke(Object proxy, Method method, Object[] args)方法来封装增强(method.invoke(target, args)调用原始方法)。2.用Proxy:newProxyInstance(ClassLoader cl, Class[] Interface.Class, InvocationHandler InvocationHandlerImpl)生成新的代理对象

使用示例1:jdk动态代理

假设我们要代理两个类,他们的方法0点到6点的时候禁止访问。我们只需要统一实现一个invocationHandler就够了。

首先我们用InvocationHandler接口来实现我们的增强逻辑:

// 增强逻辑:0点到6点期间不让访问
@Slf4j
public class RequestCtrlInvocationHandler implements InvocationHandler {
    private Object target;
    
    public RequestCtrlInvocationHandler(Object target) {this.target = target;}
    
    // 增强逻辑写到invoke()方法里
    // 参数:代理对象,所有的方法,方法的入参
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只增强request()方法
        if (!method.getName.equals("request")) {
            return null;
        }
        // 进行增强
        TimeOfDay startTime = new TimeOfDay(0, 0, 0);
        TimeOfDay endTime = new TimeOfDay(5, 59, 59);
        TimeOfDay currentTime = new TimeOfDay();
        
        if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
            log.warn("Service is not available now!");
            return null;
        }
        
        return method.invoke(target, args);
    }
}

然后我们在用Proxy类,根据RequestCtrlInvocationHandler的增强逻辑,为ISubjectIRequestable两种类型生成相应的代理对象实例即可。

public class Demo {
    public void demo() {
        // 用InvocationHandler增强生成第一个动态代理对象
        ISubject subject = (ISubject)Proxy.newProxyInstance(
           ProxyRunner.class.getClassLoader(),	// 类加载器
           new Class[] {ISubject.class},		// 被代理对象实现的接口
           new RequestCtrlInvocationHandler(new SubjectImpl)); // InvocationHandler增强
        subject.request();

        // 用InvocationHandler增强生成第二个动态代理对象
        IRequestable requestable = (IRequestable)Proxy.newProxyInstance(
            ProxyRunner.class.getClassLoader(),
            new Class[] {IRequestable.class},
            new RequestCtrlInvocationHandler(new RequestableImpl()));
        requestable.request();
    }
}

上述的InvocationHandler就相当于我们的Advice,我们还可以在InvocationHandler的基础上细化结构,后续会看到。然后上述动态代理实现的方式是有缺点的,就是被代理的类一定要实现某个接口。

默认情况下,如果SpringAOP发现目标对象实现了相应的Interface,那么就采用动态代理机制生成代理对象实例。否则,Spring AOP会尝试用CGLIB(Code Generation Library)的开源动态字节码生成类库,为目标生成动态代理的对象。

这也是下一节的内容。

实现方案2:动态字节码

具体实现为:1.实现CallbackMethodInterceptor接口的intercept()方法来封装增强(methodProxy.invokeSuper()调用原始方法)。2.用Enhancer:create()来生成新的代理对象。

使用示例2:动态字节码

假设下面是被代理的类:

public class Requestable{
    public void request {
        System.out.println("rq in Requestable without implement any interface");
    }
}

和动态代理类似,首先第一步,封装增强逻辑到CallbackMethodInterceptor接口。封装到子类里需要实现一个net.sf.cglib.proxy.Callback接口。不过我们一般直接使用net.sf.cglib.proxy.MethodInterceptor接口就行了,它是CallBack接口的扩展。

@Slf4j
public class RequestCtrlCallBack implements MethodInterceptor {
    public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 只增强request()方法
        if (!method.getName.equals("request")) {
            return null;
        }
        // 进行增强
        TimeOfDay startTime = new TimeOfDay(0, 0, 0);
        TimeOfDay endTime = new TimeOfDay(5, 59, 59);
        TimeOfDay currentTime = new TimeOfDay();
        
        if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
            log.warn("Service is not available now!");
            return null;
        }
        return proxy.invokeSuper(object, args);
    }
}

然后通过CGLIB的Enhancer为目标对象动态生成一个子类,并把RequestCtrlCallBack封装的逻辑织入子类即可。

public class Demo {
    public void demo() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperClass(Requestable.class);
        enhancer.setCallback(new RequestCtrlCallback());
        
        Requestable proxy = (Requestable)enhancer.create();
        proxy.request();
    }
}

使用CGLIB唯一的限制就是无法对final方法进行覆写。

扩展:常用代理方式总结

代理的实现方式有很多,例如:

  • 源代码阶段:Java代码生成。专用代码生成工具。早期的EJB容器用的多。现在已经退休不用了。
  • 编译阶段:AOL扩展。也就是上面说的静态AOP方式。用专门的AOL语言描述切面信息,然后用专用的编译器织入后直接生成字节码
  • 加载阶段:自定义类加载器,通过读取外部文件规定的织入规则和必要信息,在加载class文件的时候,就可以把横切逻辑织入现有逻辑中,然后把改动后的class交给JVM运行。来个偷梁换柱
  • 执行阶段:动态字节码,jdk动态代理

桥接模式

桥接模式(Bridge Design Pattern)有两种不同的理解方式:

  • 一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
  • 将抽象和实现解耦,让它们可以独立变化。这个是GOF的标准理解方式,也是对前者的升华。

代码实现

桥接模式抽象类组合接口,两个抽象维度都可以独立变化

下面就两种理解方式进行讲解,其实他们最后的实现示例代码是一样的:

理解方式1:让多个维度根据抽象类及接口独立进行扩展

使用用途

一个类需要在多个维度进行扩展,此时如果采用继承的话会使子类数量过多。变化的维度都可以封装到接口里。

设计目标

将抽象部分与实现部分解耦。所谓的抽象部分,就是抽象类及其子类。实现部分,就是接口及其实现。那么此时,所谓的桥也就是抽象类。

实现方案

抽象类持有接口,然后抽象类自己的各个子类方法里可以使用接口的各个方法。这里抽象类的方法是一个抽象,接口的方法又是两一个抽象,这两个方法可以通过子类独立变化,抽象类和接口的各个方法又可以交叉使用,这样就完成了多个变化维度的解耦,以及抽象部分和实现部分的解耦。

使用举例1

我们吃烧烤,有种类、风味两个扩展维度。种类可以选鸡翅、鸡腿等,风味可以选微辣、中辣、特辣等。

image-20201016154711669

实现部分变化

风味类Spicy

 
public interface Spicy{
       String taste();
 }

微辣SlightSpicy

public class SlightSpicy implements Spicy{
     public String taste(){
          return "微辣";
     }}

特辣ExtraSpicy

public class ExtraSpicy implements Spicy{
     public String taste(){
          return "特辣";
     }
}

抽象部分变化

烧烤类Barbecue

public abstract class Barbecue{
     protected Spicy mSpicy;    //实现部分实例
     public Barbecue(Spicy spicy){    桥接模式的关键,组合实现和抽象
          this.mSpicy= spicy;
     }
     public abstract void eat();
}

鸡翅类ChickenWing

public class ChickenWing extends Barbecue{
    public ChickenWing (Spicy spicy){
         super(spicy);
    }
     public void eat(){
           System.out.println( "鸡翅:"+super.mSpicy.taste());
     }
}

鸡腿类ChickenLeg

public class ChickenLeg extends Barbecue{
    public ChickenLeg (Spicy spicy){
         super(spicy);
    }
     public void eat(){
           System.out.println( "鸡腿:"+super.mSpicy.taste());
     }
}

测试

public class Test{
     public static void main(String[] args){
         Barbecue barbecue = new ChickenWing(new SlightSpicy());
         barbecue .eat();
         Barbecue barbecue2 = new ChickenLeg(new ExtraSpicy());
         barbecue2.eat();
     }
}

输出

鸡翅:微辣
鸡腿:特辣
使用举例2

这个例子和例1的形式是一模一样的:我们服务器发送一条消息,有发送方式、消息级别两个扩展维度。

// ===== 实现:发送方式
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 class WechatMsgSender 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代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}

理解方式2:将抽象和实现解耦,让它们可以独立变化

在这种理解方式的定义中,定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是的一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。

使用举例

Java中JDBC的实现:

image-20201016155636813

JDBC 本身就相当于“抽象”。注意,这里所说的“抽象”,指的并非“抽象类”或“接口”,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。注意,这里所说的“实现”,也并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。

装饰器模式

使用用途

用组合代替继承,解决继承关系过于复杂的问题,它主要的作用是给原始类添加增强功能。

代码实现

和代理模式类似。参加代理模式。

实现方案

  • 装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
  • 装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。

代码的结构和代理模式一样,也可以用抽象类取代接口。与代理模式的区别是:代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

使用示例

以下是核心代码。此外装饰器典型的运用就是Java的BIO,这里略过

// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}
public class A impelements IA {
  public void f() { //... }
}
public class AProxy impements IA {
  private IA a;
  public AProxy(IA a) {
    this.a = a;
  }
  
  public void f() {
    // 新添加的代理逻辑
    a.f();
    // 新添加的代理逻辑
  }
}
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}
public class A impelements IA {
  public void f() { //... }
}
public class ADecorator impements IA {
  private IA a;
  public ADecorator(IA a) {
    this.a = a;
  }
  
  public void f() {
    // 功能增强代码
    a.f();
    // 功能增强代码
  }
}

适配器模式

使用用途

将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。例如 USB 转接头。

代码实现

适配器模式适配器实现目标接口,并组合老对象。如果目标接口和看对象相同的方法逻辑较多,也可以继承老对象。

实现方案

有两种实现方案,类适配器和对象适配器。区别就是一个是通过继承引用老接口,一个是通过组合引用老接口,仅此而已。前者可以复用很多老接口的方法,而后者的话后续扩展可以更灵活。

假如ITarget 表示要转化成的新接口定义、Adaptee 是一组不兼容 ITarget 接口定义的老接口、Adaptor 是连接两者的适配器,那么实现方式很明显了。用适配器连接新老接口,新接口通过实现连接,老接口通过继承或组合连接,Adaptee –继承/组合–> Adaptor –实现–> ITarget ,就那么简单。。以下为代码解析:

// 类适配器: 基于继承
public interface ITarget {
  void f1();
  void f2();
  void fc();
}
public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}
public class Adaptor extends Adaptee implements ITarget {
  public void f1() {
    super.fa();
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
// 对象适配器:基于组合
public interface ITarget {
  void f1();
  void f2();
  void fc();
}
public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}
public class Adaptor implements ITarget {
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {
    this.adaptee = adaptee;
  }
  
  public void f1() {
    adaptee.fa(); //委托给Adaptee
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  public void fc() {
    adaptee.fc();
  }
}

使用示例:

  • 封装有缺陷的接口设计:假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了
  • 统一多个类的接口设计:某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
  • 替换依赖的外部系统:当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。
  • 兼容老版本升级:在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
  • 适配不同格式的数据:比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。

具体的使用场景例如:

  • 自己wind-cloud项目里面使用的adaptor微服务,用于匹配数据格式和外部接口。
  • Slf4j日志框架统一log4j和logback等多个日志框架。

Wrapper 模式:代理、装饰器、桥接、适配器对比

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类

尽管代码结构相似,但这 4 种设计模式的用意完全不同,它们之间的区别:

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
  • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
  • 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

代码结构上的个人理解:

  • 代理模式和装饰器模式代码结构一致,都是实现共同的接口,然后在方法里调用原始类的同一个方法。涉及原始类、接口、接口实现类(也就是代理类或装饰类)。使用时具体用的是接口实现类(也就是代理类或装饰类),它包裹了老逻辑和代理逻辑。

  • 桥接模式,抽象类和接口没有实现同一个接口。可以在不同的方法里调用原始类的不同方法。抽象类和接口两个维度可以独立扩展。然后抽象类就是桥。涉及抽象类、抽象类实现类、接口、接口实现类。使用时具体用的是抽象类的实现类,它包裹了所有维度的功能扩展。

  • 适配器模式。很简单,涉及老接口、适配器、新接口。然后用组合或继承连接即可,就那么简单。使用时具体用的是适配器包裹了新老功能的集成。

结构型设计模式,涉及了类之间的结构关系。而类之间的结构关系,无非也就是抽象、接口、继承、组合。那么各种结构型设计模式的组合也就是这四种关系,掌握好设计模式的思想,然后用这四个关系组合即可。

门面模式

使用用途

用来做接口整合,封装不同大小粒度的接口,提供统一的接口给外部使用。这样就不用外部直接调用我们内部的接口,且可以合理控制接口的粒度大小,而且提供了封装,还更好扩展。

门面模式也叫外观模式。如系统有a、b、c接口,用起来非常不方便。用门面模式直接提供接口x,整合a、b、c

实现方案

用统一的对外接口封装自己内部子系统的各种接口功能即可。各种封装,然后提供统一访问入口

使用示例

门面模式很简单,代码就不写了,就是各种封装,然后提供统一访问入口。以下是几个常用场景:

  • 让接口更好用:比如Linux的命令就可以看成门面。它屏蔽了内部细节。
  • 让性能更强:前端访问后端,只访问一次肯定比多次要好。
  • 分布式事务:屏蔽了分布式事务内部处理的复杂细节,将一个事务的多个接口直接同意封装成一个。

适配器和门面的区别:适配器做接口转换,门面做接口整合。

组合模式

使用用途

用来处理树形结构数据。将其表示成一种“部分 - 整体”的层次结构,然后就可以用一样的逻辑处理单个对象和组合对象。

使用场景较少。这里的“组合”和继承中的“组合”不是一个东西。

实现方案

其实就是自己持有自己类型的List字段组成树形结构。

代码示例

比如遍历文件夹和文件(略)。

遍历一个公司的所有人,并计算他们的工资:

public abstract class HumanResource {
  protected long id;
  protected double salary;
  public HumanResource(long id) {
    this.id = id;
  }
  public long getId() {
    return id;
  }
  public abstract double calculateSalary();
}
public class Employee extends HumanResource {
  public Employee(long id, double salary) {
    super(id);
    this.salary = salary;
  }
  @Override
  public double calculateSalary() {
    return salary;
  }
}
public class Department extends HumanResource {
  private List<HumanResource> subNodes = new ArrayList<>();
  public Department(long id) {
    super(id);
  }
  @Override
  public double calculateSalary() {
    double totalSalary = 0;
    for (HumanResource hr : subNodes) {
      totalSalary += hr.calculateSalary();
    }
    this.salary = totalSalary;
    return totalSalary;
  }
  public void addSubNode(HumanResource hr) {
    subNodes.add(hr);
  }
}
// 构建组织架构的代码
public class Demo {
  private static final long ORGANIZATION_ROOT_ID = 1001;
  private DepartmentRepo departmentRepo; // 依赖注入
  private EmployeeRepo employeeRepo; // 依赖注入
  public void buildOrganization() {
    Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
    buildOrganization(rootDepartment);
  }
  private void buildOrganization(Department department) {
    List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId());
    for (Long subDepartmentId : subDepartmentIds) {
      Department subDepartment = new Department(subDepartmentId);
      department.addSubNode(subDepartment);
      buildOrganization(subDepartment);
    }
    List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
    for (Long employeeId : employeeIds) {
      double salary = employeeRepo.getEmployeeSalary(employeeId);
      department.addSubNode(new Employee(employeeId, salary));
    }
  }
}

享元模式

使用用途

用来复用对象,节省内存,前提是享元对象是不可变对象。所谓享元,就是被共享的单元。

后续会讲它和单例、缓存、对象池的区别和联系。

具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用

不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

实现方案

其实就是工厂类容器字段缓存的不可变对象使用工厂类,加一个static final Map来缓存享元,然后在static{}静态块中初始化享元的map,把享元放进去即可。当然也可以动态的加入使用享元时从工厂拿。

代码示例1:象棋大厅

要构造n个棋盘,象棋的颜色和文字都是一样的,唯有坐标不同,所以颜色文字这部分可以用享元。

// 享元类
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
  }
  // ...省略其他属性和getter方法...
}
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;
  }
  // 省略getter、setter方法
}
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) {
    //...省略...
  }
}

代码示例2:文本编辑器

文字的格式是有限且固定的,可以用享元。享元动态加入。

public class CharacterStyle {
  private Font font;
  private int size;
  private int colorRGB;
  public CharacterStyle(Font font, int size, int colorRGB) {
    this.font = font;
    this.size = size;
    this.colorRGB = colorRGB;
  }
  @Override
  public boolean equals(Object o) {
    CharacterStyle otherStyle = (CharacterStyle) o;
    return font.equals(otherStyle.font)
            && size == otherStyle.size
            && colorRGB == otherStyle.colorRGB;
  }
}
public class CharacterStyleFactory {
  private static final List<CharacterStyle> styles = new ArrayList<>();
  public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
    CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
    for (CharacterStyle style : styles) {
      if (style.equals(newStyle)) {
        return style;
      }
    }
    styles.add(newStyle);
    return newStyle;
  }
}
public class Character {
  private char c;
  private CharacterStyle style;
  public Character(char c, CharacterStyle style) {
    this.c = c;
    this.style = style;
  }
}
public class Editor {
  private List<Character> chars = new ArrayList<>();
  public void appendCharacter(char c, Font font, int size, int colorRGB) {
    Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
    chars.add(character);
  }
}

享元模式 vs 单例、缓存、对象池

  • 享元模式VS单例:实际上,享元模式有点类似于之前讲到的单例的变体:多例。单例模式中,一个类只能创建一个对象。享元模式中,一个类可以创建多个对象。当然他们的思想不一样,前者是为了共享,后者是为了限制个数。
  • 享元模式VS缓存:缓存是为了提供访问效率,享元是为了复用。思想不一样。且缓存可变,享元不可变。
  • 享元模式VS对象池:都是复用,对象池是为了“重复使用”,目的是节省时间,此时对象只会被一个使用者独占。而享元的复用是“共享复用‘’,目的是节省空间,所有使用者都可以共享

享元模式不要过度使用,因为享元不会被JVM回收。

扩展Integer和String的享元

相关概念

// 1.自动装箱
Integer i = 59;底层执行了:Integer i = Integer.valueOf(59);
// 2.自动拆箱
int j = i; 底层执行了:int j = i.intValue();
// 3.“==” 比较,其实就是地址比较。
// 实际上是在判断两个局部变量存储的地址是否相同,换句话说,是在判断两个局部变量是否指向相同的对象

Long、 Integer、Short、Byte等类型中,-128 到 127 是享元。所以:

Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);	// true
System.out.println(i3 == i4);	// false

String中,某个字符串常量第一次被用到的时候会被存储到常量池中,即享元JVM 开辟一块存储区专门存储字符串常量,这块存储区叫作字符串常量池。所以:

String s1 = "打工人";
String s2 = "打工人";
String s3 = new String("打工人");
System.out.println(s1 == s2);	// true
System.out.println(s1 == s3);	// false

行为型

主要解决的就是“类或对象之间的交互”问题。焦点是对象之间如何进行通信

观察者模式

使用用途

解耦观察者和被观察者。一个对象状态改变时,通知其他对象。

实现方案

也叫“发布订阅”模式、“生产者消费者”等。定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

发布者持有订阅者的list。发布者有新消息时,直接调用所有订阅者的update()方法即可。订阅者需要提前注册到发布者。

观察者模式只是一种思想,根据应用场景的不同,观察者模式会对应不同的实现方式:有同步阻塞、异步非阻塞的实现方式,也有进程内、进程间的实现方式

代码示例1:同步阻塞方式

最经典的实现方式:

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

public interface Observer {
  void update(Message message);
}

public class ConcreteSubject implements Subject {
  private List<Observer> observers = new ArrayList<Observer>();
  @Override
  public void registerObserver(Observer observer) {
    observers.add(observer);
  }
  @Override
  public void removeObserver(Observer observer) {
    observers.remove(observer);
  }
  @Override
  public void notifyObservers(Message message) {
    for (Observer observer : observers) {
      observer.update(message);
    }
  }
}

public class ConcreteObserverOne implements Observer {
  @Override
  public void update(Message message) {
    //TODO: 获取消息通知,执行自己的逻辑...
    System.out.println("ConcreteObserverOne is notified.");
  }
}

public class ConcreteObserverTwo implements Observer {
  @Override
  public void update(Message message) {
    //TODO: 获取消息通知,执行自己的逻辑...
    System.out.println("ConcreteObserverTwo is notified.");
  }
}

public class Demo {
  public static void main(String[] args) {
    ConcreteSubject subject = new ConcreteSubject();
    subject.registerObserver(new ConcreteObserverOne());
    subject.registerObserver(new ConcreteObserverTwo());
    subject.notifyObservers(new Message());
  }

以上主要是反应思想,实际应用时一般不会照搬。

其他使用举例:Google Guava EventBus 框架、MQ等。

代码示例2:异步非阻塞方式

EventBus,后续补充。

模板模式

模板、策略、责任链都有相同的作用:复用和扩展。在实际的项目开发中比较常用,特别是框架开发中,我们可以利用它们来提供框架的扩展点,能够让框架的使用者在不修改框架源码的情况下,基于扩展点定制化框架的功能。

使用用途

在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

实现方式

用抽象类定义好算法逻辑骨架,可变的部分使用抽象方法封装,让子类去实现。当然也不一定用抽象方法,可以定义空方法、或抛出异常的方法,等待子类实现,具体代码比较灵活,核心思想是提供骨架。

代码示例

Java Servlet、JUnit TestCase、Java AbstractList都有用到。

以下是典型的代码:

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() {
    //...
  }
}
AbstractClass demo = ConcreteClass1();
demo.templateMethod();

扩展:模板方法和回调的区别:模板方法和回调应用场景是一致的,都是定义好算法骨架,并对外开放扩展点,符合开闭原则;两者的却别是代码的实现上不同,模板方法是通过继承来实现,是自己调用自己;回调是类之间的组合。

策略模式

使用用途

解耦算法和使用算法的代码,可以根据情况动态使用不同的算法。准确的来说,是解耦策略的定义、创建、使用这三部分。

实现方案

策略定义:包含一个策略接口和一组实现这个接口的策略类。

策略创建:通过类型(type)来判断创建哪个策略来使用。用工厂封装判断逻辑。逻辑可以是静态或动态,静态直接用享元缓存好,动态的话直接new。然后策略还可以通过反射根据注解自动注入。

策略使用:直接调用工厂获取策略,然后执行方法即可。

使用示例

// ========== 策略的定义
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);
  }
}
// 如果业务场景需要每次都创建不同的策略对象,我们就要直接new
public class DiscountStrategyFactory {
  public static DiscountStrategy getDiscountStrategy(OrderType type) {
    if (type == null) {
      throw new IllegalArgumentException("Type should not be null.");
    }
    if (type.equals(OrderType.NORMAL)) {
      return new NormalDiscountStrategy();
    } else if (type.equals(OrderType.GROUPON)) {
      return new GrouponDiscountStrategy();
    } else if (type.equals(OrderType.PROMOTION)) {
      return new PromotionDiscountStrategy();
    }
    return null;
  }
}
// ========== 策略的使用
public class OrderService {
  public double discount(Order order) {
    OrderType type = order.getType();
    DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
    return discountStrategy.calDiscount(order);
  }
}

责任链模式

使用用途

实现方案

多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责。

在 GoF 的定义中,如果处理器链上的某个节点能够处理这个请求,那就不会继续往下传递请求。实际上,职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况

职责链模式有两种常用的实现。一种是使用链表来存储处理器,另一种是使用数组来存储处理器,后面一种实现方式更加简单:

链表方式实现每个节点保存它的后节点。责任链保存首节点和尾节点。使用时责任链直接调用首节点的处理方法进行处理即可,处理方法中处理完之后自动调用下一节点处理方法。增加节点则从尾节点处增加。

数组方式实现责任链中直接用数组保存。使用时责任链直接从头到尾遍历这个数据进行处理即可

Servlet Filter、Spring Interceptor、日志框架等。都会用到责任链。

代码示例1:链表方式

这里使用了模板模式对处理方法进行了一点小优化。

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();
  }
}

代码示例2:数组方式

public interface IHandler {
  boolean handle();
}
public class HandlerA implements IHandler {
  @Override
  public boolean handle() {
    boolean handled = false;
    //...
    return handled;
  }
}
public class HandlerB implements IHandler {
  @Override
  public boolean handle() {
    boolean handled = false;
    //...
    return handled;
  }
}
public class HandlerChain {
  private List<IHandler> handlers = new ArrayList<>();
  public void addHandler(IHandler handler) {
    this.handlers.add(handler);
  }
  public void handle() {
    for (IHandler handler : handlers) {
      boolean handled = handler.handle();
      if (handled) {
        break;
      }
    }
  }
}
// 使用举例
public class Application {
  public static void main(String[] args) {
    HandlerChain chain = new HandlerChain();
    chain.addHandler(new HandlerA());
    chain.addHandler(new HandlerB());
    chain.handle();
  }
}

状态模式

使用用途

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

状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法

在实际开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上来看,它有点像我们之前讲到的组合模式。

实现方案

状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。动作不是必须执行的。

状态模式:定义一个状态接口,保存状态字段、以及所有事件(每个事件一个处理方法)。用该状态接口封装状态转移的逻辑。状态维度根据接口的各种实现类扩展,事件维度根据接口的方法数量进行扩展。状态转移的逻辑及动作写到方法里就OK。定义一个状态机。状态机持有当前的状态,然后遇到每个事件直接调用当前状态的事件处理方法处理即可。状态类里也持有状态机的引用,以获取当前的状态及业务字段等。

状态机的三种实现方式总结:

  • 分支逻辑法。利用 if-else 或者 switch-case 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。
  • 查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
  • 状态模式。对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。

代码示例1:状态模式

以下是超级玛丽的示例:

public interface IMario { //所有状态类的接口
  State getName();
  //以下是定义的事件
  void obtainMushRoom();
  void obtainCape();
  void obtainFireFlower();
  void meetMonster();
}
public class SmallMario implements IMario {
  private MarioStateMachine stateMachine;
  public SmallMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }
  @Override
  public State getName() {
    return State.SMALL;
  }
  @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);
  }
  @Override
  public void obtainFireFlower() {
    stateMachine.setCurrentState(new FireMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 300);
  }
  @Override
  public void meetMonster() {
    // do nothing...
  }
}
public class SuperMario implements IMario {
  private MarioStateMachine stateMachine;
  public SuperMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }
  @Override
  public State getName() {
    return State.SUPER;
  }
  @Override
  public void obtainMushRoom() {
    // do nothing...
  }
  @Override
  public void obtainCape() {
    stateMachine.setCurrentState(new CapeMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 200);
  }
  @Override
  public void obtainFireFlower() {
    stateMachine.setCurrentState(new FireMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 300);
  }
  @Override
  public void meetMonster() {
    stateMachine.setCurrentState(new SmallMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() - 100);
  }
}
// 省略CapeMario、FireMario类...
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 obtainFireFlower() {
    this.currentState.obtainFireFlower();
  }
  public void meetMonster() {
    this.currentState.meetMonster();
  }
  public int getScore() {
    return this.score;
  }
  public State getCurrentState() {
    return this.currentState.getName();
  }
  public void setScore(int score) {
    this.score = score;
  }
  public void setCurrentState(IMario currentState) {
    this.currentState = currentState;
  }
}

可以用单例模式把上述代码重构下,因为状态时不变的:

public interface IMario {
  State getName();
  void obtainMushRoom(MarioStateMachine stateMachine);
  void obtainCape(MarioStateMachine stateMachine);
  void obtainFireFlower(MarioStateMachine stateMachine);
  void meetMonster(MarioStateMachine stateMachine);
}
public class SmallMario implements IMario {
  private static final SmallMario instance = new SmallMario();
  private SmallMario() {}
  public static SmallMario getInstance() {
    return instance;
  }
  @Override
  public State getName() {
    return State.SMALL;
  }
  @Override
  public void obtainMushRoom(MarioStateMachine stateMachine) {
    stateMachine.setCurrentState(SuperMario.getInstance());
    stateMachine.setScore(stateMachine.getScore() + 100);
  }
  @Override
  public void obtainCape(MarioStateMachine stateMachine) {
    stateMachine.setCurrentState(CapeMario.getInstance());
    stateMachine.setScore(stateMachine.getScore() + 200);
  }
  @Override
  public void obtainFireFlower(MarioStateMachine stateMachine) {
    stateMachine.setCurrentState(FireMario.getInstance());
    stateMachine.setScore(stateMachine.getScore() + 300);
  }
  @Override
  public void meetMonster(MarioStateMachine stateMachine) {
    // do nothing...
  }
}
// 省略SuperMario、CapeMario、FireMario类...
public class MarioStateMachine {
  private int score;
  private IMario currentState;
  public MarioStateMachine() {
    this.score = 0;
    this.currentState = SmallMario.getInstance();
  }
  public void obtainMushRoom() {
    this.currentState.obtainMushRoom(this);
  }
  public void obtainCape() {
    this.currentState.obtainCape(this);
  }
  public void obtainFireFlower() {
    this.currentState.obtainFireFlower(this);
  }
  public void meetMonster() {
    this.currentState.meetMonster(this);
  }
  public int getScore() {
    return this.score;
  }
  public State getCurrentState() {
    return this.currentState.getName();
  }
  public void setScore(int score) {
    this.score = score;
  }
  public void setCurrentState(IMario currentState) {
    this.currentState = currentState;
  }
}

代码示例2:分支逻辑法

最简单粗暴的if-else:

public class MarioStateMachine {
  private int score;
  private State currentState;
  public MarioStateMachine() {
    this.score = 0;
    this.currentState = State.SMALL;
  }
  public void obtainMushRoom() {
    if (currentState.equals(State.SMALL)) {
      this.currentState = State.SUPER;
      this.score += 100;
    }
  }
  public void obtainCape() {
    if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
      this.currentState = State.CAPE;
      this.score += 200;
    }
  }
  public void obtainFireFlower() {
    if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
      this.currentState = State.FIRE;
      this.score += 300;
    }
  }
  public void meetMonster() {
    if (currentState.equals(State.SUPER)) {
      this.currentState = State.SMALL;
      this.score -= 100;
      return;
    }
    if (currentState.equals(State.CAPE)) {
      this.currentState = State.SMALL;
      this.score -= 200;
      return;
    }
    if (currentState.equals(State.FIRE)) {
      this.currentState = State.SMALL;
      this.score -= 300;
      return;
    }
  }
  public int getScore() {
    return this.score;
  }
  public State getCurrentState() {
    return this.currentState;
  }
}

代码示例3:查表法

直接把状态转移存到表里:

public enum Event {
  GOT_MUSHROOM(0),
  GOT_CAPE(1),
  GOT_FIRE(2),
  MET_MONSTER(3);
  private int value;
  private Event(int value) {
    this.value = value;
  }
  public int getValue() {
    return this.value;
  }
}
public class MarioStateMachine {
  private int score;
  private State currentState;
  private static final State[][] transitionTable = {
          {SUPER, CAPE, FIRE, SMALL},
          {SUPER, CAPE, FIRE, SMALL},
          {CAPE, CAPE, CAPE, SMALL},
          {FIRE, FIRE, FIRE, SMALL}
  };
  private static final int[][] actionTable = {
          {+100, +200, +300, +0},
          {+0, +200, +300, -100},
          {+0, +0, +0, -200},
          {+0, +0, +0, -300}
  };
  public MarioStateMachine() {
    this.score = 0;
    this.currentState = State.SMALL;
  }
  public void obtainMushRoom() {
    executeEvent(Event.GOT_MUSHROOM);
  }
  public void obtainCape() {
    executeEvent(Event.GOT_CAPE);
  }
  public void obtainFireFlower() {
    executeEvent(Event.GOT_FIRE);
  }
  public void meetMonster() {
    executeEvent(Event.MET_MONSTER);
  }
  private void executeEvent(Event event) {
    int stateValue = currentState.getValue();
    int eventValue = event.getValue();
    this.currentState = transitionTable[stateValue][eventValue];
    this.score = actionTable[stateValue][eventValue];
  }
  public int getScore() {
    return this.score;
  }
  public State getCurrentState() {
    return this.currentState;
  }
}

迭代器模式

也叫作游标模式(Cursor Design Pattern)。

使用用途

用来遍历集合对象。很多编程语言都将迭代器作为一个基础的类库,直接提供出来了,平时很少自己去实现一个迭代器。

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

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

实际上foreach就是迭代器的语法糖。迭代器可以针对复杂容器遍历,各种深度广度优先遍历等,具有很好的扩展性。

实现方案

容器部分:根据容器结构实现。

迭代器部分:定义好游标,并持有容器对象即可。然后定义hasNext(),next(),currentItem()即可,具体就是对游标的操作。

如何避免遍历时容器增删元素带来的异常:

  • 遍历时禁止增删元素:比较难实现,因为不知道什么时候遍历结束。
  • 增删后遍历器报错:Java的方案,在容器里面加一个修改次数记录字段或版本号,之后每次迭代器调用方法时检查下这个值即可。

代码示例

// 接口定义方式一
// 游标移动和返回当前元素分开,更灵活一些。
public interface Iterator<E> {
  boolean hasNext();
  void next();
  E currentItem();
}
// 接口定义方式二
// 游标移动和返回当前元素合并为一步。
public interface Iterator<E> {
  boolean hasNext();
  E next();	
}
// ArrayList迭代器示例
public class ArrayIterator<E> implements Iterator<E> {
  private int cursor;
  private ArrayList<E> arrayList;
  public ArrayIterator(ArrayList<E> arrayList) {
    this.cursor = 0;
    this.arrayList = arrayList;
  }
  @Override
  public boolean hasNext() {
    return cursor != arrayList.size(); //注意这里,cursor在指向最后一个元素的时候,hasNext()仍旧返回true。
  }
  @Override
  public void next() {
    cursor++;
  }
  @Override
  public E currentItem() {
    if (cursor >= arrayList.size()) {
      throw new NoSuchElementException();
    }
    return arrayList.get(cursor);
  }
}
public class Demo {
  public static void main(String[] args) {
    ArrayList<String> names = new ArrayList<>();
    names.add("xzg");
    names.add("wang");
    names.add("zheng");
    
    Iterator<String> iterator = new ArrayIterator(names);
    while (iterator.hasNext()) {
      System.out.println(iterator.currentItem());
      iterator.next();
    }
  }
}
// 还可以把迭代器的实现细节封装一下,并放到容器里
public interface List<E> {
  Iterator iterator();
  //...省略其他接口函数...
}
public class ArrayList<E> implements List<E> {
  //...
  public Iterator iterator() {
    return new ArrayIterator(this);
  }
  //...省略其他代码
}
public class Demo {
  public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("xzg");
    names.add("wang");
    names.add("zheng");
    
    Iterator<String> iterator = names.iterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.currentItem());
      iterator.next();
    }
  }
}

访问者模式

使用用途

允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。对象就是抽象类,操作就是访问者接口。

弥补单分派语言无法根据形参类型进行方法重载的不足。

访问者模式比较复杂,使用它会让代码可读性、可维护性变差,实际开发中很少用。在没必要的情况下,不推荐使用。

实现方案

定义一个访问者来作为入参访问自己。这里的访问者是一个接口,自己即抽象类的实现类。

抽象类封装了要处理的各种对象,访问者封装了对应的各种操作。

核心代码是抽象类访问方法的子类实现让访问者访问自己,以及访问方法的入参是一个访问者接口。这样就提供了两个扩展维度,抽象方法的实现是一个维度,访问者接口的实现是另外一个维度不同类型的访问者会对不同类型的自己做出不同的逻辑处理。核心代码:抽象类及其各种实现类,抽象类及其实现类的accept方法绑定自己的访问者。访问者及其实现类,对不同的被访问者做出各种不同的反应。

访问者有点像一个外挂。通过抽象类的形参建立联系。

示例代码:用各种逻辑处理各种格式文本

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
  // 整个访问者模式的关键代码:在抽象接待方法里,让访问者访问自己。从而提供了自己、访问者的两个扩展维度。或者说对象和操作的两个扩展维度。
  abstract public void accept(Visitor vistor);
}
public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }
  @Override
  public void accept(Visitor visitor) {
    visitor.visit(this);	// 整个访问者模式的关键代码:在接待方法里,让访问者访问自己。
  }
  //...
}
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...
public interface Visitor {
  void visit(PdfFile pdfFile);
  void visit(PPTFile pdfFile);
  void visit(WordFile pdfFile);
}
public class Extractor implements Visitor {
  @Override
  public void visit(PPTFile pptFile) {
    //...
    System.out.println("Extract PPT.");
  }
  @Override
  public void visit(PdfFile pdfFile) {
    //...
    System.out.println("Extract PDF.");
  }
  @Override
  public void visit(WordFile wordFile) {
    //...
    System.out.println("Extract WORD.");
  }
}
public class Compressor implements Visitor {
  @Override
  public void visit(PPTFile pptFile) {
    //...
    System.out.println("Compress PPT.");
  }
  @Override
  public void visit(PdfFile pdfFile) {
    //...
    System.out.println("Compress PDF.");
  }
  @Override
  public void visit(WordFile wordFile) {
    //...
    System.out.println("Compress WORD.");
  }
}
public class ToolApplication {
  public static void main(String[] args) {
    Extractor extractor = new Extractor();
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(extractor);
    }
    Compressor compressor = new Compressor();
    for(ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(compressor);
    }
  }
  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

扩展:双分派语言

Single Dispatch 之所以称为“Single”,是因为执行哪个对象的哪个方法,只跟“对象”的运行时类型有关。Double Dispatch 之所以称为“Double”,是因为执行哪个对象的哪个方法,跟“对象”和“方法参数”两者的运行时类型有关。Java只支持单分派。

  • 单分派语言:执行哪个对象的方法,由对象的实际类型决定
  • 双分派语言:执行对象的哪个方法,由参数对象的声明类型决定

单分派、双分派差别示意:

public class ParentClass {
  public void f() {
    System.out.println("I am ParentClass's f().");
  }
}
public class ChildClass extends ParentClass {
  public void f() {
    System.out.println("I am ChildClass's f().");
  }
}
public class SingleDispatchClass {
  public void polymorphismFunction(ParentClass p) {
    p.f();
  }
  public void overloadFunction(ParentClass p) {
    System.out.println("I am overloadFunction(ParentClass p).");
  }
  public void overloadFunction(ChildClass c) {
    System.out.println("I am overloadFunction(ChildClass c).");
  }
}
public class DemoMain {
  public static void main(String[] args) {
    SingleDispatchClass demo = new SingleDispatchClass();
    ParentClass p = new ChildClass();
    demo.polymorphismFunction(p);//执行哪个对象的方法,由对象的实际类型决定
    demo.overloadFunction(p);//执行对象的哪个方法,由参数对象的声明类型决定
  }
}
//代码执行结果:
I am ChildClass's f().
I am overloadFunction(ParentClass p).

访问者模式是为了弥补单分派语言的不足,因为拿到一个父类声明的对象时,语言本身无法判断这个对象的子类类型,只能用访问者模式来巧妙的实现。如果是双分配语言,直接用对象执行即可,不用再用访问者绕一圈。

双分派不需要使用访问者模式,因为可以识别方法形参的实际类型,以此进行方法重载:

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
}
public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }
  //...
}
//...PPTFile、WordFile代码省略...
public class Extractor {
  public void extract2txt(PPTFile pptFile) {
    //...
    System.out.println("Extract PPT.");
  }
  public void extract2txt(PdfFile pdfFile) {
    //...
    System.out.println("Extract PDF.");
  }
  public void extract2txt(WordFile wordFile) {
    //...
    System.out.println("Extract WORD.");
  }
}
public class ToolApplication {
  public static void main(String[] args) {
    Extractor extractor = new Extractor();
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
        // 关键代码:双分派语言可以自动感知形参是什么类型的子类,以此定位要执行的方法。而单分派具体执行哪个对象的哪个方法,只跟对象的运行时类型有关,跟参数的运行时类型无关。即只支持编译期的方法重载。这里就直接报错了。
      extractor.extract2txt(resourceFile);	
    }
  }
  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

备忘录模式

也叫快照(Snapshot)模式。

使用用途

用于保存对象状态并后续恢复。

就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

实现方案

  • 一个snapshot类,用于封装快照
  • 一个SnapshotHolder类,用于存储快照。存储的容器用栈或队列都可以。
  • 一个操作类,用于操作快照类和holder类存储快照,以及业务处理。

代码示例

编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“:list”,程序在命令行中输出内存文本的内容;用户输入“:undo”,程序会撤销上一次输入的文本,也就是从内存文本中将上次输入的文本删除掉:

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);
  }
}
public class ApplicationMain {
  public static void main(String[] args) {
    InputText inputText = new InputText();
    SnapshotHolder snapshotsHolder = new SnapshotHolder();
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNext()) {
      String input = scanner.next();
      if (input.equals(":list")) {
        System.out.println(inputText.toString());
      } else if (input.equals(":undo")) {
        Snapshot snapshot = snapshotsHolder.popSnapshot();
        inputText.restoreSnapshot(snapshot);
      } else {
        snapshotsHolder.pushSnapshot(inputText.createSnapshot());
        inputText.append(input);
      }
    }
  }
}

命令模式

命令模式、解释器模式、中介模式。这 3 个模式使用频率低、理解难度大,只在非常特定的应用场景下才会用到,所以,不是我们学习的重点,你只需要稍微了解,见了能认识就可以了。

使用用途

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

命令模式用的最核心的实现手段,是将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。

命令模式和策略模式的不同:前者不同命令有不同的目的,对应不同处理逻辑,不可替换。后者不同策略有相同的目的,不过实现的方式不一样而已,可以替换。两种模式虽然代码相近,但设计模式最重要的是处理思路,两者的处理思路完全不同。

实现方案

把请求包含的数据和处理逻辑封装为命令对象,在程序中直接拿来执行即可。

把函数封装成对象,以实现函数的传递。

代码示例1

处理一个游戏的的各种指令:

public interface Command {
  void execute();
}
public class GotDiamondCommand implements Command {
  // 省略成员变量
  public GotDiamondCommand(/*数据*/) {
    //...
  }
  @Override
  public void execute() {
    // 执行相应的逻辑
  }
}
//GotStartCommand/HitObstacleCommand/ArchiveCommand类省略
public class GameApplication {
  private static final int MAX_HANDLED_REQ_COUNT_PER_LOOP = 100;
  private Queue<Command> queue = new LinkedList<>();
  public void mainloop() {
    while (true) {
      List<Request> requests = new ArrayList<>();
      
      //省略从epoll或者select中获取数据,并封装成Request的逻辑,
      //注意设置超时时间,如果很长时间没有接收到请求,就继续下面的逻辑处理。
      
      for (Request request : requests) {
        Event event = request.getEvent();
        Command command = null;
        if (event.equals(Event.GOT_DIAMOND)) {
          command = new GotDiamondCommand(/*数据*/);	// 核心代码
        } else if (event.equals(Event.GOT_STAR)) {
          command = new GotStartCommand(/*数据*/);
        } else if (event.equals(Event.HIT_OBSTACLE)) {
          command = new HitObstacleCommand(/*数据*/);
        } else if (event.equals(Event.ARCHIVE)) {
          command = new ArchiveCommand(/*数据*/);
        } // ...一堆else if...
        queue.add(command);
      }
      int handledCount = 0;
      while (handledCount < MAX_HANDLED_REQ_COUNT_PER_LOOP) {
        if (queue.isEmpty()) {
          break;
        }
        Command command = queue.poll();
        command.execute();							// 核心代码
      }
    }
  }
}

代码示例2

买卖股票:

// 命令
public interface Command {
   void execute();
}

// 请求类
public class StockReq {
   private String name = "ABC";
   private int quantity = 10;
 
   public void buy(){
      System.out.println("Stock [ Name: "+name+", 
         Quantity: " + quantity +" ] bought");
   }
   public void sell(){
      System.out.println("Stock [ Name: "+name+", 
         Quantity: " + quantity +" ] sold");
   }
}

// 命令实现类
public class BuyStockCommand implements Command {
   private StockReq stockReq;
 
   public BuyStockCommand(Stock stockReq){
      this.stockReq = stockReq;
   }
 
   public void execute() {
      stockReq.buy();
   }
}
public class SellStockCommand implements Command {
   private StockReq stockReq;
 
   public SellStock(StockReq stockReq){
      this.stockReq = stockReq;
   }
 
   public void execute() {
      stockReq.sell();
   }
}
                         
// 命令调用类
import java.util.ArrayList;
import java.util.List;
 
public class CommendExcuter {
   private List<Command> commandList = new ArrayList<Command>(); 
 
   public void addCommand(Command command){
      commandList.add(command);      
   }
 
   public void executeCommands(){
      for (Command command : commandList) {
         command.execute();
      }
      commandList.clear();
   }
}
                         
// 使用
public class CommandPatternDemo {
   public static void main(String[] args) {
      Stock abcStockReq = new Stock();
 
      BuyStockCommand buyStockCommand = new BuyCommand(abcStockReq);
      SellStockCommand sellStockCommand = new SellCommand(abcStockReq);
 
      CommendExcuter commendExcuter = new CommendExcuter();
      commendExcuter.addCommand(buyStockOrder);
      commendExcuter.addCommand(sellStockOrder);
 
      commendExcuter.executeCommands();
   }
}
                         
// 结果
Stock [ Name: ABC, Quantity: 10 ] bought
Stock [ Name: ABC, Quantity: 10 ] sold

解释器模式

使用用途

用来描述如何构建一个简单的“语言”解释器。比如编译器、规则引擎、正则表达式,或者自己定义的规则语言等。

平时用的很少,运用场景很固定,了解就行。Java中已经有现成的expression4J 。

实现方案

解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

解释器模式的代码实现比较灵活,没有固定的模板。一般的做法是,将语法规则拆分成一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。

1. 定义一个表达式接口,以及它的解释方法。解释方法的入参是表达式。

2. 实现表达式接口,从而派生出多种表达式及其解析。

3. 使用Expression 表达式类来创建各种新的表达式规则,并解析它们。也可以直接调用之前的表达式实现类解析。

代码示例

// 1.定义一个表达式接口
public interface Expression {
   public boolean interpret(String context);	// 注意这里的解释器
}

// 2.实现表达式接口
public 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;
   }
}

public 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);
   }
}

public 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);
   }
}

// 3.使用Expression 类来创建新的Expression,并解析它们
public class InterpreterPatternDemo {
 
   //规则: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"));
   }
}

中介模式

使用用途

中介模式也很少用。

中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。(注意和观察者模式的区别。)通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。

比如UI控制,点击一个按钮,出发和它相关的所有组件,就可以用中介模式(类似总线?)。好处是简化了控件之间的交互,坏处是中介类有可能会变成大而复杂的“上帝类”(God Class)。

实现方案

  1. 定义一个中介者接口。定义处理方法,入参是原始类和消息。

  2. 中介者接口的实现类实现接口即处理方法。处理方法针对不同的原始类、不同的消息,做不同的处理。

  3. 大家按需注册到中介接口实现类,方便中介调用资源。

  4. 发送事件时,把自己的引用和事件发给中介,中介去处理即可。

各个控件只跟中介对象交互,中介对象负责所有业务逻辑的处理。

  • 观察者模式,观察者持有被观察者的引用,并调用被观察者的update方法,业务处理逻辑在被观察者处。
  • 中介者模式:中介者持有原始类的对象。原始类主动调用中介者的处理方法,将自己的引用和事件通过形参传给中介者。由中间来调用各个原始类的对象和资源,业务处理逻辑。重点看下下面示例代码的Interface`就明白了。

代码示例

以下是UI操作示例:

// 注意到这个重点的类:各原始类吧自己
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")) {
        //....
      }
    }
  }
}
public class UIControl {
  private static final String LOGIN_BTN_ID = "login_btn";
  private static final String REG_BTN_ID = "reg_btn";
  private static final String USERNAME_INPUT_ID = "username_input";
  private static final String PASSWORD_INPUT_ID = "pswd_input";
  private static final String REPEATED_PASSWORD_INPUT_ID = "repeated_pswd_input";
  private static final String HINT_TEXT_ID = "hint_text";
  private static final String SELECTION_ID = "selection";
  public static void main(String[] args) {
    Button loginButton = (Button)findViewById(LOGIN_BTN_ID);
    Button regButton = (Button)findViewById(REG_BTN_ID);
    Input usernameInput = (Input)findViewById(USERNAME_INPUT_ID);
    Input passwordInput = (Input)findViewById(PASSWORD_INPUT_ID);
    Input repeatedPswdInput = (Input)findViewById(REPEATED_PASSWORD_INPUT_ID);
    Text hintText = (Text)findViewById(HINT_TEXT_ID);
    Selection selection = (Selection)findViewById(SELECTION_ID);
    Mediator dialog = new LandingPageDialog();
    dialog.setLoginButton(loginButton);
    dialog.setRegButton(regButton);
    dialog.setUsernameInput(usernameInput);
    dialog.setPasswordInput(passwordInput);
    dialog.setRepeatedPswdInput(repeatedPswdInput);
    dialog.setHintText(hintText);
    dialog.setSelection(selection);
    loginButton.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        dialog.handleEvent(loginButton, "click");
      }
    });
    regButton.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        dialog.handleEvent(regButton, "click");
      }
    });
    //....
  }
}

扩展:中介模式 VS 观察者模式

观察者模式:在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是,大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。也就是说,在观察者模式的应用场景中,参与者之间的交互关系比较有条理。

中介模式:中介模式正好相反。只有当参与者之间的交互关系错综复杂,维护成本很高的时候,我们才考虑使用中介模式,但有可能会产生大而复杂的上帝类。除此之外,如果一个参与者状态的改变,其他参与者执行的操作有一定先后顺序的要求,这个时候,中介模式就可以利用中介类,通过先后调用不同参与者的方法,来实现顺序的控制。观察者则没有这个功能。


转载请注明来源