接口隔離原則

1阵难、如何理解“接口隔離原則”掺栅?

  • 接口隔離原則的英文翻譯是“ Interface Segregation Principle”额衙,縮寫為 ISP
    Robert Martin 在 SOLID 原則中是這樣定義它的:“Clients should not be forced to depend upon interfaces that they do not use慎陵⌒壬玻”

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

實(shí)際上邻悬,“接口”這個名詞可以用在很多場合中症昏。生活中我們可以用它來指插座接口等。

在軟件開發(fā)中父丰,我們既可以把它看作一組抽象的約定肝谭,也可以具體指系統(tǒng)與系統(tǒng)之間的 API 接口,還可以特指面向?qū)ο缶幊陶Z言中的接口等础米。

前面我提到分苇,理解接口隔離原則的關(guān)鍵,就是理解其中的“接口”二字屁桑。在這條原則中医寿,我們可以把“接口”理解為下面三種東西:

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

2、把“接口”理解為一組 API 接口集合

微服務(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 {
  //...
}

們的后臺管理系統(tǒng)要實(shí)現(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è)計(jì)的層面,通過接口鑒權(quán)的方式來限制接口的調(diào)用宠默。

不過麸恍,如果暫時沒有鑒權(quán)框架來支持,我們還可以從代碼設(shè)計(jì)的層面搀矫,盡量避免接口被誤用抹沪。

我們參照接口隔離原則,調(diào)用者不應(yīng)該強(qiáng)迫依賴它不需要的接口瓤球,將刪除接口單獨(dú)放到另外一個接口 RestrictedUserService 中融欧,然后將 RestrictedUserService 只打包提供給后臺管理系統(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 interface RestrictedUserService {
  boolean deleteUserByCellphone(String cellphone);
  boolean deleteUserById(long id);
}

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

我們把接口隔離原則中的接口卦羡,理解為一組接口集合噪馏,它可以是某個微服務(wù)的接口,也可以是某個類庫的接口等等绿饵。

在設(shè)計(jì)微服務(wù)或者類庫接口的時候欠肾,如果部分接口只被部分調(diào)用者使用,那我們就需要將這部分接口隔離出來拟赊,單獨(dú)給對應(yīng)的調(diào)用者使用刺桃,而不是強(qiáng)迫其他調(diào)用者也依賴這部分不會被用到的接口遏暴。

3 把“接口”理解為單個 API 接口或函數(shù)

把接口理解為單個接口或函數(shù)

那接口隔離原則就可以理解為:函數(shù)的設(shè)計(jì)要功能單一昂灵,不要將多個不同的功能邏輯在一個函數(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();
  //...省略計(jì)算邏輯...
  return statistics;
}

在上面的代碼中仔雷,count() 函數(shù)的功能不夠單一,包含很多不同的統(tǒng)計(jì)功能葛碧,比如借杰,求最大值、最小值进泼、平均值等等蔗衡。按照接口隔離原則,我們應(yīng)該把 count() 函數(shù)拆成幾個更小粒度的函數(shù)缘琅,每個函數(shù)負(fù)責(zé)一個獨(dú)立的統(tǒng)計(jì)功能粘都。


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

你可能會說,在某種意義上講刷袍,count() 函數(shù)也不能算是職責(zé)不夠單一,畢竟它做的事情只跟統(tǒng)計(jì)相關(guān)樊展。

我們在講單一職責(zé)原則的時候呻纹,也提到過類似的問題。實(shí)際上专缠,判定功能是否單一雷酪,除了很強(qiáng)的主觀性,還需要結(jié)合具體的場景涝婉。

如果在項(xiàng)目中哥力,對每個統(tǒng)計(jì)需求,Statistics 定義的那幾個統(tǒng)計(jì)信息都有涉及墩弯,那 count() 函數(shù)的設(shè)計(jì)就是合理的吩跋。相反,如果每個統(tǒng)計(jì)需求只涉及 Statistics 羅列的統(tǒng)計(jì)信息中一部分渔工,比如锌钮,有的只需要用到 max、min引矩、average 這三類統(tǒng)計(jì)信息梁丘,有的只需要用到 average、sum旺韭。而 count() 函數(shù)每次都會把所有的統(tǒng)計(jì)信息計(jì)算一遍氛谜,就會做很多無用功,勢必影響代碼的性能区端,特別是在需要統(tǒng)計(jì)的數(shù)據(jù)量很大的時候值漫。所以,在這個應(yīng)用場景下珊燎,count() 函數(shù)的設(shè)計(jì)就有點(diǎn)不合理了惭嚣,我們應(yīng)該按照第二種設(shè)計(jì)思路遵湖,將其拆分成粒度更細(xì)的多個統(tǒng)計(jì)函數(shù)。

不過晚吞,你應(yīng)該已經(jīng)發(fā)現(xiàn)延旧,接口隔離原則跟單一職責(zé)原則有點(diǎn)類似,不過稍微還是有點(diǎn)區(qū)別槽地。

單一職責(zé)原則針對的是模塊迁沫、類、接口的設(shè)計(jì)捌蚊。

如果調(diào)用者只使用部分接口或接口的部分功能集畅,那接口的設(shè)計(jì)就不夠職責(zé)單一。

4缅糟、把“接口”理解為 OOP 中的接口概念

我們還可以把“接口”理解為 OOP 中的接口概念挺智,比如 Java 中的 interface。

假設(shè)我們的項(xiàng)目中用到了三個外部系統(tǒng):Redis窗宦、MySQL赦颇、Kafka。

每個系統(tǒng)都對應(yīng)一系列配置信息赴涵,比如地址媒怯、端口、訪問超時時間等髓窜。

為了在內(nèi)存中存儲這些配置信息扇苞,供項(xiàng)目中的其他模塊來使用,我們分別設(shè)計(jì)實(shí)現(xiàn)了三個 Configuration 類:RedisConfig寄纵、MysqlConfig鳖敷、KafkaConfig。


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 { //...省略... }

我們有一個新的功能需求哄陶,希望支持 Redis 和 Kafka 配置信息的熱更新。

但是哺壶,因?yàn)槟承┰蛭荻郑覀儾⒉幌M麑?MySQL 的配置信息進(jìn)行熱更新。

為了實(shí)現(xiàn)這樣一個功能需求山宾,我們設(shè)計(jì)實(shí)現(xiàn)了一個 ScheduledUpdater 類至扰,以固定時間頻率(periodInSeconds)來調(diào)用 RedisConfig、KafkaConfig 的 update() 方法更新配置信息资锰。


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

現(xiàn)在敢课,我們又有了一個新的監(jiān)控功能需求。
我們可以在項(xiàng)目中開發(fā)一個內(nèi)嵌的 SimpleHttpServer,輸出項(xiàng)目的配置信息到一個固定的 HTTP 地址直秆,比如:http://127.0.0.1:2389/config 濒募。

我們只需要在瀏覽器中輸入這個地址,就可以顯示出系統(tǒng)的配置信息圾结。
不過瑰剃,出于某些原因,我們只想暴露 MySQL 和 Redis 的配置信息筝野,不想暴露 Kafka 的配置信息晌姚。


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)控的需求我們就都實(shí)現(xiàn)了歇竟。我們來回顧一下這個例子的設(shè)計(jì)思想挥唠。

我們設(shè)計(jì)了兩個功能非常單一的接口:Updater 和 Viewer。

ScheduledUpdater 只依賴 Updater 這個跟熱更新相關(guān)的接口焕议,不需要被強(qiáng)迫去依賴不需要的 Viewer 接口宝磨,滿足接口隔離原則。

同理盅安,SimpleHttpServer 只依賴跟查看信息相關(guān)的 Viewer 接口懊烤,不依賴不需要的 Updater 接口,也滿足接口隔離原則宽堆。

如果我們不遵守接口隔離原則,不設(shè)計(jì) Updater 和 Viewer 兩個小接口茸习,而是設(shè)計(jì)一個大而全的 Config 接口畜隶,讓 RedisConfig、KafkaConfig号胚、MysqlConfig 都實(shí)現(xiàn)這個 Config 接口籽慢,并且將原來傳遞給 ScheduledUpdater 的 Updater 和傳遞給 SimpleHttpServer 的 Viewer,都替換為 Config猫胁,那會有什么問題呢箱亿?


public interface Config {
  void update();
  String outputInPlainText();
  Map<String, String> output();
}

public class RedisConfig implements Config {
  //...需要實(shí)現(xiàn)Config的三個接口update/outputIn.../output
}

public class KafkaConfig implements Config {
  //...需要實(shí)現(xiàn)Config的三個接口update/outputIn.../output
}

public class MysqlConfig implements Config {
  //...需要實(shí)現(xiàn)Config的三個接口update/outputIn.../output
}

public class ScheduledUpdater {
  //...省略其他屬性和方法..
  private Config config;

  public ScheduleUpdater(Config config, long initialDelayInSeconds, long periodInSeconds) {
      this.config = config;
      //...
  }
  //...
}

public class SimpleHttpServer {
  private String host;
  private int port;
  private Map<String, List<Config>> viewers = new HashMap<>();
 
  public SimpleHttpServer(String host, int port) {//...}
  
  public void addViewer(String urlDirectory, Config config) {
    if (!viewers.containsKey(urlDirectory)) {
      viewers.put(urlDirectory, new ArrayList<Config>());
    }
    viewers.get(urlDirectory).add(config);
  }
  
  public void run() { //... }
}
  • 首先,第一種設(shè)計(jì)思路更加靈活弃秆、易擴(kuò)展届惋、易復(fù)用
    因?yàn)?Updater菠赚、Viewer 職責(zé)更加單一脑豹,單一就意味了通用、復(fù)用性好衡查。

  • 其次瘩欺,第二種設(shè)計(jì)思路在代碼實(shí)現(xiàn)上做了一些無用功

因?yàn)?Config 接口中包含兩類不相關(guān)的接口,一類是 update()俱饿,一類是 output() 和 outputInPlainText()歌粥。

理論上,KafkaConfig 只需要實(shí)現(xiàn) update() 接口拍埠,并不需要實(shí)現(xiàn) output() 相關(guān)的接口失驶。

同理,MysqlConfig 只需要實(shí)現(xiàn) output() 相關(guān)接口械拍,并需要實(shí)現(xiàn) update() 接口突勇。

但第二種設(shè)計(jì)思路要求 RedisConfig、KafkaConfig坷虑、MySqlConfig 必須同時實(shí)現(xiàn) Config 的所有接口函數(shù)(update甲馋、output、outputInPlainText)迄损。

除此之外定躏,如果我們要往 Config 中繼續(xù)添加一個新的接口,那所有的實(shí)現(xiàn)類都要改動芹敌。相反痊远,如果我們的接口粒度比較小,那涉及改動的類就比較少氏捞。

參考

18 | 理論四:接口隔離原則有哪三種應(yīng)用碧聪?原則中的“接口”該如何理解?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末液茎,一起剝皮案震驚了整個濱河市逞姿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捆等,老刑警劉巖滞造,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異栋烤,居然都是意外死亡谒养,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門明郭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來买窟,“玉大人,你說我怎么就攤上這事达址∶锼睿” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵沉唠,是天一觀的道長疆虚。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么径簿? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任罢屈,我火速辦了婚禮,結(jié)果婚禮上篇亭,老公的妹妹穿的比我還像新娘缠捌。我一直安慰自己,他們只是感情好译蒂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布曼月。 她就那樣靜靜地躺著,像睡著了一般柔昼。 火紅的嫁衣襯著肌膚如雪哑芹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天捕透,我揣著相機(jī)與錄音聪姿,去河邊找鬼。 笑死乙嘀,一個胖子當(dāng)著我的面吹牛末购,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播虎谢,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盟榴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了婴噩?” 一聲冷哼從身側(cè)響起曹货,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讳推,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玩般,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡银觅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了坏为。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片究驴。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖匀伏,靈堂內(nèi)的尸體忽然破棺而出洒忧,到底是詐尸還是另有隱情,我是刑警寧澤够颠,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布熙侍,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蛉抓。R本人自食惡果不足惜庆尘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巷送。 院中可真熱鬧驶忌,春花似錦、人聲如沸笑跛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽飞蹂。三九已至几苍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晤柄,已是汗流浹背擦剑。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留芥颈,地道東北人惠勒。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像爬坑,于是被迫代替她去往敵國和親纠屋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

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