設(shè)計原則之接口隔離原則

1. 定義

客戶端不應(yīng)該被強(qiáng)迫依賴它不需要的接口恶守。其中的“客戶端”,可以理解為接口的調(diào)用者或者使用者。

在這條原則中倔监,我們可以把“接口”理解為下面三種東西:

  • 一組 API 接口集合
  • 單個 API 接口或函數(shù)
  • OOP 中的接口概念

接下來席覆,我就按照這三種理解方式來詳細(xì)講一下史辙,在不同的場景下,這條原則具體是如何解讀和應(yīng)用的佩伤。

2. 一組 API 接口集合

我們還是結(jié)合一個例子來講解聊倔。微服務(wù)用戶系統(tǒng)提供了一組跟用戶相關(guān)的 API 給其他系統(tǒng)使用,比如:注冊生巡、登錄耙蔑、獲取用戶信息等。具體代碼如下所示:

public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

public class UserServiceImpl implements UserService {
  //...
}

現(xiàn)在孤荣,我們的后臺管理系統(tǒng)要實現(xiàn)刪除用戶的功能甸陌,希望用戶系統(tǒng)提供一個刪除用戶的接口须揣。最簡單粗暴的方法就是直接在UserService中新添加一個deleteUserByCellphone() 或 deleteUserById() 接口就可以了。但是這樣做會有安全隱患邀层。

刪除用戶是一個非常慎重的操作返敬,我們只希望通過后臺管理系統(tǒng)來執(zhí)行,所以這個接口只限于給后臺管理系統(tǒng)使用寥院。如果我們把它放到 UserService 中劲赠,那所有使用到 UserService 的系統(tǒng),都可以調(diào)用這個接口秸谢。不加限制地被其他業(yè)務(wù)系統(tǒng)調(diào)用凛澎,就有可能導(dǎo)致誤刪用戶。

當(dāng)然估蹄,最好的解決方案是從架構(gòu)設(shè)計的層面塑煎,通過接口鑒權(quán)的方式來限制接口的調(diào)用。不過臭蚁,如果暫時沒有鑒權(quán)框架來支持最铁,我們還可以從代碼設(shè)計的層面,盡量避免接口被誤用垮兑。我們參照接口隔離原則冷尉,調(diào)用者不應(yīng)該強(qiáng)迫依賴它不需要的接口,將刪除接口單獨放到另外一個接口 RestrictedUserService 中系枪,然后將 RestrictedUserService 只打包提供給后臺管理系統(tǒng)來使用雀哨。具體的代碼實現(xiàn)如下所示:

public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

public interface RestrictedUserService {
  boolean deleteUserByCellphone(String cellphone);
  boolean deleteUserById(long id);
}

public class UserServiceImpl implements UserService, RestrictedUserService {
  // ...省略實現(xiàn)代碼...
}

在剛剛的這個例子中,我們把接口隔離原則中的接口私爷,理解為一組接口集合雾棺,它可以是某個微服務(wù)的接口,也可以是某個類庫的接口等等衬浑。在設(shè)計微服務(wù)或者類庫接口的時候捌浩,如果部分接口只被部分調(diào)用者使用,那我們就需要將這部分接口隔離出來嚎卫,單獨給對應(yīng)的調(diào)用者使用嘉栓,而不是強(qiáng)迫其他調(diào)用者也依賴這部分不會被用到的接口。

3. 單個API接口或函數(shù)

現(xiàn)在我們再換一種理解方式拓诸,把接口理解為單個接口或函數(shù)(以下為了方便講解,我都簡稱為“函數(shù)”)麻昼。那接口隔離原則就可以理解為:函數(shù)的設(shè)計要功能單一奠支,不要將多個不同的功能邏輯在一個函數(shù)中實現(xiàn)。接下來抚芦,我們還是通過一個例子來解釋一下倍谜。

public class Statistics {
  private Long max;
  private Long min;
  private Long average;
  private Long sum;
  private Long percentile99;
  private Long percentile999;
  //...省略constructor/getter/setter等方法...
}

public Statistics count(Collection<Long> dataSet) {
  Statistics statistics = new Statistics();
  //...省略計算邏輯...
  return statistics;
}

在上面的代碼中迈螟,count() 函數(shù)的功能不夠單一,包含很多不同的統(tǒng)計功能尔崔,比如答毫,求最大值、最小值季春、平均值等等洗搂。按照接口隔離原則,我們應(yīng)該把 count() 函數(shù)拆成幾個更小粒度的函數(shù)载弄,每個函數(shù)負(fù)責(zé)一個獨立的統(tǒng)計功能耘拇。拆分之后的代碼如下所示:

public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... } 
public Long average(Colletion<Long> dataSet) { //... }
// ...省略其他統(tǒng)計函數(shù)...

4. OOP中的接口

除了剛講過的兩種理解方式,我們還可以把“接口”理解為 OOP 中的接口概念宇攻,比如 Java 中的 interface惫叛。

假設(shè)我們的項目中用到了三個外部系統(tǒng):Redis、MySQL逞刷、Kafka嘉涌。每個系統(tǒng)都對應(yīng)一系列配置信息,比如地址夸浅、端口仑最、訪問超時時間等。為了在內(nèi)存中存儲這些配置信息题篷,供項目中的其他模塊來使用词身,我們分別設(shè)計實現(xiàn)了三個 Configuration 類:RedisConfig、MysqlConfig番枚、KafkaConfig法严。具體的代碼實現(xiàn)如下所示。注意葫笼,這里只給出了 RedisConfig 的代碼實現(xiàn)深啤,另外兩個都是類似的,這里就不貼了路星。

public class RedisConfig {
    private ConfigSource configSource; //配置中心(比如zookeeper)
    private String address;
    private int timeout;
    private int maxTotal;
    //省略其他配置: maxWaitMillis,maxIdle,minIdle...

    public RedisConfig(ConfigSource configSource) {
        this.configSource = configSource;
    }

    public String getAddress() {
        return this.address;
    }
    //...省略其他get()溯街、init()方法...

    public void update() {
      //從configSource加載配置到address/timeout/maxTotal...
    }
}

public class KafkaConfig { //...省略... }
public class MysqlConfig { //...省略... }

現(xiàn)在,我們有一個新的功能需求洋丐,希望支持 Redis 和 Kafka 配置信息的熱更新呈昔。所謂“熱更新(hot update)”就是,如果在配置中心中更改了配置信息友绝,我們希望在不用重啟系統(tǒng)的情況下堤尾,能將最新的配置信息加載到內(nèi)存中(也就是 RedisConfig、KafkaConfig 類中)迁客。但是郭宝,因為某些原因辞槐,我們并不希望對 MySQL 的配置信息進(jìn)行熱更新。

為了實現(xiàn)這樣一個功能需求粘室,我們設(shè)計實現(xiàn)了一個 ScheduledUpdater 類榄檬,以固定時間頻率(periodInSeconds)來調(diào)用 RedisConfig、KafkaConfig 的 update() 方法更新配置信息衔统。具體的代碼實現(xiàn)如下所示:

public interface Updater {
  void update();
}

public class RedisConfig implemets Updater {
  //...省略其他屬性和方法...
  @Override
  public void update() { //... }
}

public class KafkaConfig implements Updater {
  //...省略其他屬性和方法...
  @Override
  public void update() { //... }
}

public class MysqlConfig { //...省略其他屬性和方法... }

public class ScheduledUpdater {
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();;
    private long initialDelayInSeconds;
    private long periodInSeconds;
    private Updater updater;

    public ScheduleUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) {
        this.updater = updater;
        this.initialDelayInSeconds = initialDelayInSeconds;
        this.periodInSeconds = periodInSeconds;
    }

    public void run() {
        executor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                updater.update();
            }
        }, this.initialDelayInSeconds, this.periodInSeconds, TimeUnit.SECONDS);
    }
}

public class Application {
  ConfigSource configSource = new ZookeeperConfigSource(/*省略參數(shù)*/);
  public static final RedisConfig redisConfig = new RedisConfig(configSource);
  public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
  public static final MySqlConfig mysqlConfig = new MysqlConfig(configSource);

  public static void main(String[] args) {
    ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig, 300, 300);
    redisConfigUpdater.run();
    
    ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig, 60, 60);
    redisConfigUpdater.run();
  }
}

剛剛的熱更新的需求我們已經(jīng)搞定了÷拱瘢現(xiàn)在,我們又有了一個新的監(jiān)控功能需求缰冤。通過命令行來查看 Zookeeper 中的配置信息是比較麻煩的犬缨。所以,我們希望能有一種更加方便的配置信息查看方式棉浸。

我們可以在項目中開發(fā)一個內(nèi)嵌的 SimpleHttpServer怀薛,輸出項目的配置信息到一個固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 迷郑。我們只需要在瀏覽器中輸入這個地址枝恋,就可以顯示出系統(tǒng)的配置信息。不過嗡害,出于某些原因焚碌,我們只想暴露 MySQL 和 Redis 的配置信息,不想暴露 Kafka 的配置信息霸妹。

為了實現(xiàn)這樣一個功能十电,我們還需要對上面的代碼做進(jìn)一步改造。改造之后的代碼如下所示:

public interface Updater {
  void update();
}

public interface Viewer {
  String outputInPlainText();
  Map<String, String> output();
}

public class RedisConfig implemets Updater, Viewer {
  //...省略其他屬性和方法...
  @Override
  public void update() { //... }
  @Override
  public String outputInPlainText() { //... }
  @Override
  public Map<String, String> output() { //...}
}

public class KafkaConfig implements Updater {
  //...省略其他屬性和方法...
  @Override
  public void update() { //... }
}

public class MysqlConfig implements Viewer {
  //...省略其他屬性和方法...
  @Override
  public String outputInPlainText() { //... }
  @Override
  public Map<String, String> output() { //...}
}

public class SimpleHttpServer {
  private String host;
  private int port;
  private Map<String, List<Viewer>> viewers = new HashMap<>();
  
  public SimpleHttpServer(String host, int port) {//...}
  
  public void addViewers(String urlDirectory, Viewer viewer) {
    if (!viewers.containsKey(urlDirectory)) {
      viewers.put(urlDirectory, new ArrayList<Viewer>());
    }
    this.viewers.get(urlDirectory).add(viewer);
  }
  
  public void run() { //... }
}

public class Application {
    ConfigSource configSource = new ZookeeperConfigSource();
    public static final RedisConfig redisConfig = new RedisConfig(configSource);
    public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
    public static final MySqlConfig mysqlConfig = new MySqlConfig(configSource);
    
    public static void main(String[] args) {
        ScheduledUpdater redisConfigUpdater =
            new ScheduledUpdater(redisConfig, 300, 300);
        redisConfigUpdater.run();
        
        ScheduledUpdater kafkaConfigUpdater =
            new ScheduledUpdater(kafkaConfig, 60, 60);
        redisConfigUpdater.run();
        
        SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389);
        simpleHttpServer.addViewer("/config", redisConfig);
        simpleHttpServer.addViewer("/config", mysqlConfig);
        simpleHttpServer.run();
    }
}

至此叹螟,熱更新和監(jiān)控的需求我們就都實現(xiàn)了鹃骂。我們來回顧一下這個例子的設(shè)計思想。我們設(shè)計了兩個功能非常單一的接口:Updater 和 Viewer罢绽。ScheduledUpdater 只依賴 Updater 這個跟熱更新相關(guān)的接口畏线,不需要被強(qiáng)迫去依賴不需要的 Viewer 接口,滿足接口隔離原則良价。同理寝殴,SimpleHttpServer 只依賴跟查看信息相關(guān)的 Viewer 接口,不依賴不需要的 Updater 接口明垢,也滿足接口隔離原則蚣常。

5. 與單一職責(zé)區(qū)別

單一職責(zé)原則針對的是模塊、類痊银、接口的設(shè)計史隆。而接口隔離原則相對于單一職責(zé)原則,一方面它更側(cè)重于接口的設(shè)計曼验,另一方面它的思考的角度不同泌射。它提供了一種判斷接口是否職責(zé)單一的標(biāo)準(zhǔn):通過調(diào)用者如何使用接口來間接地判定。如果調(diào)用者只使用部分接口或接口的部分功能鬓照,那接口的設(shè)計就不夠職責(zé)單一熔酷。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市豺裆,隨后出現(xiàn)的幾起案子拒秘,更是在濱河造成了極大的恐慌,老刑警劉巖臭猜,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躺酒,死亡現(xiàn)場離奇詭異,居然都是意外死亡蔑歌,警方通過查閱死者的電腦和手機(jī)羹应,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來次屠,“玉大人园匹,你說我怎么就攤上這事〗僭睿” “怎么了裸违?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長本昏。 經(jīng)常有香客問我供汛,道長,這世上最難降的妖魔是什么涌穆? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任怔昨,我火速辦了婚禮,結(jié)果婚禮上蒲犬,老公的妹妹穿的比我還像新娘朱监。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布渊额。 她就那樣靜靜地躺著蛤奢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪擂送。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天唯欣,我揣著相機(jī)與錄音嘹吨,去河邊找鬼。 笑死境氢,一個胖子當(dāng)著我的面吹牛蟀拷,可吹牛的內(nèi)容都是我干的碰纬。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼问芬,長吁一口氣:“原來是場噩夢啊……” “哼悦析!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起此衅,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤强戴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后挡鞍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骑歹,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年墨微,在試婚紗的時候發(fā)現(xiàn)自己被綠了道媚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡欢嘿,死狀恐怖衰琐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情炼蹦,我是刑警寧澤羡宙,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站掐隐,受9級特大地震影響狗热,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜虑省,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一匿刮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧探颈,春花似錦熟丸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至怀大,卻和暖如春纱兑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背化借。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工潜慎, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓铐炫,卻偏偏與公主長得像垒手,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子驳遵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容