設計模式之觀察者模式:實現配置更新實時推送

背景

最近在作應用配置化的事情。 應用配置化,就是將應用中一些頻繁變化的變量經過配置好比JSON串的形式存儲到配置平臺。當新業務須要增長新的枚舉時,只要在配置平臺修改對應配置,就能實時推送給應用更新,無需應用從新發布。頻繁變化的變量包括文案、分流比例、枚舉等。java

配置更新的實時推送機制,讓人很容易就想到觀察者模式。 觀察者模式的要點在於:多線程

  • 在對象之間創建一對多的關係,當某一個對象發生變化的時候,相關對象都能獲得通知並更新本身。
  • 有利於【模型/控制/視圖】的分離,即著名的MVC框架。

設計結構

觀察者模式的設計結構以下:
app

  • 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。這樣,就將關注的配置與應用分離開。美中不足的是,這樣的子類會更多。

代碼實現以下:ide

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

}

設計結構變成:
優化

停下來從新思考下,經過擴展更多子類來實現是否有些衝動?須要思考兩個問題:this

  • 配置的差別到底是什麼?爲何須要用不一樣子類來實現? 顯然,若是都是原子變量,用泛型就能搞定; 配置的差別體如今:單個配置仍是組合關聯配置。具備很大差別的配置,才值得用不一樣子類來表達。
  • 應用的差別到底是什麼?爲何須要用不一樣子類來實現? 這是因爲不一樣應用對同一配置的處理極可能是不同的。這裏用子類表明的是監聽配置的多個應用。

交互行爲管理

如今,思考一個更加實際的問題:假設應用A監聽配置a的變化,作HAa的處理,監聽配置b的變化,作HAb的處理,監聽配置c的變化,作HAc的處理,……;應用B監聽配置a的變化,作HBa的處理,監聽配置b的變化,作HBb的處理,監聽配置c的變化,作HBc的處理,……。 也就是說,有多個應用,同時監聽多個配置,作不一樣的處理,該怎麼辦?atom

此時,不能將觀察者列表放在 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: 應用能夠經過註冊配置的監聽器,來處理配置改動。

實際中,能夠支持更多功能,好比更靈活的配置對象類型、配置的名字空間、配置的共享、經過註解的方式實時推送更新的配置等。

小結

本文講解了經過觀察者模式來實現配置動態更新實時推送的基本原理和實現。觀察者模式是解耦變化與關聯變化的設計結構,即:當一個對象的某個狀態變化後,須要通知關注該對象該狀態的全部對象。關注者不須要知道其餘關注者,被觀察者也不須要知道觀察者。

相關文章
相關標籤/搜索