设计模式之观察者模式:实现配置更新实时推送

背景

最近在做应用配置化的事情。 应用配置化,就是将应用中一些频繁变化的变量通过配置比如JSON串的形式存储到配置平台。当新业务需要增加新的枚举时,只要在配置平台修改对应配置,就能实时推送给应用更新,无需应用重新发布。频繁变化的变量包括文案、分流比例、枚举等。

配置更新的实时推送机制,让人很容易就想到观察者模式。 观察者模式的要点在于:

  • 在对象之间建立一对多的关系,当某一个对象发生变化的时候,相关对象都能得到通知并更新自己。
  • 有利于【模型/控制/视图】的分离,即著名的MVC框架。

设计结构

观察者模式的设计结构如下:

  • Observable: 被观察者抽象。当被观察者持有的某个状态改变时,将通知所有关注此状态的观察者。Observable 的方法主要有:添加、删除、通知观察者对象;设置或清除更新状态。这里 Observable 大多数方法使用了同步关键字 synchronized,因为很可能在多线程环境下访问。
  • Observer: 观察者抽象。 当被观察者的某个关注状态更新时,观察者将得到通知。观察者的基本方法是 update 。
  • Config : 配置,具体的被观察者。
  • Application: 应用,具体的观察者。

极简实现

这里借助 jdk 的Observable, Observer 提供观察者的一个极简实现。

package zzz.study.patterns.observer.realconfig.simple;

import java.util.Observable;

public class Config<T> extends Observable {

  private T conf;

  public Config(T conf) {
    this.conf = conf;
  }

  public T getConf() {
    return conf;
  }

  public void update(T config) {
    this.conf = config;
    setChanged();
  }

}
package zzz.study.patterns.observer.realconfig.simple;

import java.util.Observable;
import java.util.Observer;

public class Application implements Observer {

  public void update(Observable o, Object arg) {
    Config obj = (Config)o;
    Object conf = obj.getConf();
    System.out.println("conf updated: " + conf);
  }
}
package zzz.study.patterns.observer.realconfig.simple;

public class ConfigRealUpdating {

  public static void main(String[] args) {
    Config config = new Config<>(5);
    Application app = new Application();
    config.addObserver(app);

    config.update(6);
    config.notifyObservers();
  }

}

扩展优化

多个配置与应用

极简实现提供了对观察者模式基本思想的直接理解。但极简实现是远远不够的。它只有一个被观察者和一个应用对象。假设有两个应用 A,B以及两个配置 aconf, bconf , A 关注 aconf 的变化, B 关注 bconf 的变化。怎么办呢? 有两种方案:

  • 在 Conf 中同时添加 aconf, bconf ,这样需要做一个从 Conf 到 Application 的映射关系。而且 aconf, bconf 的配置对象很可能不一样,需要做些特殊处理。 这样无疑会让 Conf 类变得更复杂。
  • 分别建立子类 AConf, BConf ,以及 AApplication , BApplication ,AConf 添加 AApplication 观察者; BConf 添加 BApplication 观察者; Application 变成 AbstractApplication。这样,就将关注的配置与应用分离开。美中不足的是,这样的子类会更多。

代码实现如下:

public class AConfig extends Config<String> {

  public AConfig(String conf) {
    super(conf);
  }

}

public class BConfig extends Config<Long> {

  public BConfig(Long conf) {
    super(conf);
  }
}

public abstract class AbstractApplication implements Observer {

  public void update(Observable o, Object arg) {
    Config obj = (Config)o;
    Object conf = obj.getConf();
    handle(conf);
  }

  public abstract void handle(Object conf);

}

public class AApplication extends AbstractApplication {

  @Override
  public void handle(Object conf) {
    System.out.println("A Conf updated: " + conf);
  }

}

public class BApplication extends AbstractApplication {

  @Override
  public void handle(Object conf) {
    Long num = (Long)conf;
    if (num != null && num > 0) {
      String info = String.format("factor(%d) = %d", num, factor(num));
      System.out.println(info);
    }
  }

  public Long factor(Long num) {
    if (num < 0) { return 0L; }
    if (num == 0) {return 1L; }
    return num * factor(num-1);
  }
}

public class ConfigRealUpdating {

  public static void main(String[] args) {
    Config aConfig = new AConfig("Haha");
    Config bConfig = new BConfig(-1L);
    AbstractApplication aApp = new AApplication();
    AbstractApplication bApp = new BApplication();
    aConfig.addObserver(aApp);
    bConfig.addObserver(bApp);

    aConfig.update("I am changed");
    aConfig.notifyObservers();
    bConfig.update(9L);
    bConfig.notifyObservers();
  }

}

设计结构变成:

停下来重新思考下,通过扩展更多子类来实现是否有些冲动?需要思考两个问题:

  • 配置的差异究竟是什么?为什么需要用不同子类来实现? 显然,如果都是原子变量,用泛型就能搞定; 配置的差异体现在:单个配置还是组合关联配置。具有很大差异的配置,才值得用不同子类来表达。
  • 应用的差异究竟是什么?为什么需要用不同子类来实现? 这是由于不同应用对同一配置的处理很可能是不一样的。这里用子类代表的是监听配置的多个应用。

交互行为管理

现在,思考一个更加实际的问题:假设应用A监听配置a的变化,做HAa的处理,监听配置b的变化,做HAb的处理,监听配置c的变化,做HAc的处理,……;应用B监听配置a的变化,做HBa的处理,监听配置b的变化,做HBb的处理,监听配置c的变化,做HBc的处理,……。 也就是说,有多个应用,同时监听多个配置,做不同的处理,该怎么办?

此时,不能将观察者列表放在 Observable 了。 实际上,jdk 里的 Observable 担负了两个责任:(1) 状态变化; (2) 通知观察者。 现在,希望将职责(2) 移到专门负责交互的一个对象。这个对象可采用中介者模式来实现。希望达成效果:

  • 配置仅负责状态变化;
  • 应用仅负责根据相应状态执行相应逻辑;
  • 增加一个中介者,监听配置变化,并通知相应应用。中介者还需要提供注册和销除配置与应用映射关系的职责。

现在,需要造点轮子了。 要解决这个交互,需要构造一个三元组<config, app, updateFunc>集合。由于 java 没有提供元组功能,因此,可以将后两者包装成一个对象ConfigObserver,配置与应用行为可抽象为一个Map[Config, List[ConfigObserver]] 。注意,这里 Config, ConfigObserver 都需要跟集合交互,为保证存取正确,必须覆写 equals 和 hashCode 方法。 这里配置和应用都采用一个ID 来标识(因为配置变更时,值已经改变了,所以不能用值作为equals的依据)。

代码实现如下(所有类都放在包 zzz.study.patterns.observer.realconfig.interact 下):

ID.java 封装了 id 相关的功能

package zzz.study.patterns.observer.realconfig.interact;

import java.util.concurrent.atomic.AtomicLong;

/**
 * 封装了ID以及根据ID来比较的功能
 */
public abstract class ID {

  private static AtomicLong gloalId = new AtomicLong(0);

  // 通过id字段标识配置及应用
  protected Long id;

  public void setId() {
    this.id = gloalId.addAndGet(1);
  }

  public <T> boolean equals(Object obj, Class<T> cls) {
    if (obj == null || ! (cls.isInstance(obj))) {
      return false;
    }
    return ((ID)obj).id.equals(this.id);
  }

  public int hashCode() {
    return id.hashCode();
  }


}

应用类:

package zzz.study.patterns.observer.realconfig.interact;

public abstract class Application extends ID {

  @Override
  public boolean equals(Object c) {
    return equals(c, Application.class);
  }

  @Override
  public int hashCode() {
    return super.hashCode();
  }

}

public class AApp extends Application {

  public AApp() {
    super.setId();
  }

  public Object haa(Config c) {
    System.out.println("haa: " + c.getConf());
    return c;
  }

  public Object hab(Config c) {
    System.out.println("hab: " + c.getConf());
    return c;
  }

  public Object hac(Config c) {
    System.out.println("hac: " + c.getConf());
    return c;
  }

}

public class BApp extends Application {

  public BApp() {
    super.setId();
  }

  public Object hba(Config c) {
    System.out.println("hba: " + c.getConf());
    return c;
  }

  public Object hbb(Config c) {
    System.out.println("hbb: " + c.getConf());
    return c;
  }

  public Object hbc(Config c) {
    System.out.println("hbc: " + c.getConf());
    return c;
  }

}

配置类:

package zzz.study.patterns.observer.realconfig.interact;

import lombok.Getter;

@Getter
public class Config<T> extends ID {

  private T conf;
  private ObserverMediator mediator;

  public Config(T conf, ObserverMediator mediator) {
    super.setId();
    this.conf = conf;
    this.mediator = mediator;
  }

  public void update(T config) {
    this.conf = config;
    mediator.notifyAll(this);
  }

  @Override
  public boolean equals(Object c) {
    return equals(c, Config.class);
  }

  @Override
  public int hashCode() {
    return super.hashCode();
  }

}
package zzz.study.patterns.observer.realconfig.interact;

import java.util.function.Function;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ConfigObserver {

  private Application app;
  private Function<Config, Object> updateFunc;

  @Override
  public boolean equals(Object c) {
    if (c == null || ! (c instanceof ConfigObserver)) {
      return false;
    }
    ConfigObserver cmp = (ConfigObserver) c;
    return cmp.getApp().equals(this.getApp());
  }

  @Override
  public int hashCode() {
    return app.hashCode();
  }
}

中介者:

package zzz.study.patterns.observer.realconfig.interact;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 中介者模式,管理观察者与被观察者的交互
 * 实际应用中,可作为一个Spring Singleton Bean来管理
 */
public class ObserverMediator {

  private Map<Config, List<ConfigObserver>> mediator = new HashMap<>();

  /**
   * 应用启动时初始化
   */
  public void init() {
  }

  public synchronized boolean register(Config config, ConfigObserver configObserver) {
    List<ConfigObserver> oberverList = mediator.get(config);
    if (oberverList == null) {
      oberverList = new ArrayList<>();
    }
    oberverList.add(configObserver);
    mediator.put(config, oberverList);
    return true;
  }

  public synchronized boolean unregister(Config config, ConfigObserver configObserver) {
    List<ConfigObserver> oberverList = mediator.get(config);
    if (oberverList == null) {
      return false;
    }
    oberverList.remove(configObserver);
    mediator.put(config, oberverList);
    return true;
  }

  public synchronized boolean notifyAll(Config config) {
    List<ConfigObserver> configObservers = mediator.get(config);
    configObservers.forEach(
        observer -> observer.getUpdateFunc().apply(config)
    );
    return true;
  }
}

客户端使用:

package zzz.study.patterns.observer.realconfig.interact;


import java.util.concurrent.TimeUnit;

public class ConfigRealUpdating {

  public static void main(String[] args) {

    // 中介者是全局管理者,是最先存在的
    ObserverMediator mediator = new ObserverMediator();

    // 这一步在配置平台实现, 可以采用注解的方式注入到应用中,配置仅与中介者交互
    Config aConfig = new Config("Haha", mediator);
    Config bConfig = new Config(-1L, mediator);
    Config cConfig = new Config(true, mediator);
    AApp a = new AApp();
    BApp b = new BApp();

    // 这一步可以通过应用启动时注册到分布式服务发现系统上来完成
    mediator.register(aConfig, new ConfigObserver(a, conf -> a.haa(conf)));
    mediator.register(bConfig, new ConfigObserver(a, conf -> a.hab(conf)));
    mediator.register(cConfig, new ConfigObserver(a, conf -> a.hac(conf)));

    mediator.register(aConfig, new ConfigObserver(b, conf -> b.hba(conf)));
    mediator.register(bConfig, new ConfigObserver(b, conf -> b.hbb(conf)));
    mediator.register(cConfig, new ConfigObserver(b, conf -> b.hbc(conf)));

    // 核心: 更新与通知
    aConfig.update("I am changed");
    sleep(2000);

    bConfig.update(9L);
    sleep(2000);

    mediator.unregister(cConfig, new ConfigObserver(b, conf -> b.hbc(conf)));

    cConfig.update(false);
    sleep(2000);
  }

  private static void sleep(long millis) {
    try {
      TimeUnit.MILLISECONDS.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

}

设计结构变为:

设计说明:

  • 实际应用中, Config, Application 不应该继承 ID 类,而应采用委托的方式: ID 为单例组件注入到 Config, Application 中提供ID功能。
  • 如果通知功能足够复杂,那么应从中介者中抽离出来,专门作为通知者角色; 中介者仅负责配置与应用的注册与销毁,供通知者使用。
  • ConfigObserver 的功能可以通过应用注册监听器来实现;它的本质就是监听器。

实际配置系统

业界开源的动态配置平台有携程的Apollo配置。可以想象一下主流程:

STEP1: 在配置平台,首先要注册应用。这样就建立 Application 对象;

STEP2: 在配置平台的某个应用下,新建某个配置,就会在 Apollo 系统内将 Config 与 Application 联系起来;

STPE3: 当应用启动时,通过SpringXML配置的方式, 告知 Apollo 该应用所需要的配置,实际上形成了该应用的配置订阅; Apollo 在运行时将配置与应用关联起来;

STEP4: 当用户在配置平台更改某个配置时,通过观察者模式,就可以将配置改动通知到连接上的应用;

STEP5: 应用可以通过注册配置的监听器,来处理配置改动。

实际中,可以支持更多功能,比如更灵活的配置对象类型、配置的名字空间、配置的共享、通过注解的方式实时推送更新的配置等。

小结

本文讲解了通过观察者模式来实现配置动态更新实时推送的基本原理和实现。观察者模式是解耦变化与关联变化的设计结构,即:当一个对象的某个状态变化后,需要通知关注该对象该状态的所有对象。关注者不需要知道其他关注者,被观察者也不需要知道观察者。

posted @ 2018-08-11 11:56  琴水玉  阅读(2124)  评论(2编辑  收藏  举报