介面隔離原則介紹
阿新 • • 發佈:2022-12-04
目錄介紹
- 00.問題思考分析
- 01.前沿簡單介紹
- 02.如何理解介面隔離原則
- 03.介面理解為一組API介面集合
- 04.介面理解為單個API介面或函式
- 05.介面理解為OOP中的介面概念
- 06.總結一下分享
- 07.思考一道課後題
00.問題思考分析
- 01.什麼叫作介面隔離法則,它和麵向物件中的介面有何區別?
01.前沿簡單介紹
- 學習了 SOLID 原則中的單一職責原則、開閉原則和裡式替換原則,今天我們學習第四個原則,介面隔離原則。它對應 SOLID 中的英文字母“I”。
- 對於這個原則,最關鍵就是理解其中“介面”的含義。那針對“介面”,不同的理解方式,對應在原則上也有不同的解讀方式。
- 除此之外,介面隔離原則跟我們之前講到的單一職責原則還有點兒類似,所以今天我也會具體講一下它們之間的區別和聯絡。
02.如何理解介面隔離原則
- 介面隔離原則的英文翻譯是“ Interface Segregation Principle”,縮寫為 ISP。Robert Martin 在 SOLID 原則中是這樣定義它的:“Clients should not be forced to depend upon interfaces that they do not use。”直譯成中文的話就是:客戶端不應該強迫依賴它不需要的介面。其中的“客戶端”,可以理解為介面的呼叫者或者使用者。
- 實際上,“介面”這個名詞可以用在很多場合中。生活中我們可以用它來指插座介面等。在軟體開發中,我們既可以把它看作一組抽象的約定,也可以具體指系統與系統之間的 API 介面,還可以特指面向物件程式語言中的介面等。
- 前面我提到,理解介面隔離原則的關鍵,就是理解其中的“介面”二字。在這條原則中,我們可以把“介面”理解為下面三種東西:
- 一組 API 介面集合
- 單個 API 介面或函式
- OOP 中的介面概念
03.介面理解為一組API介面集合
- 還是結合一個例子來講解。微服務使用者系統提供了一組跟使用者相關的 API 給其他系統使用,比如:註冊、登入、獲取使用者資訊等。具體程式碼如下所示:
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 { //... }
- 現在,我們的後臺管理系統要實現刪除使用者的功能,希望使用者系統提供一個刪除使用者的介面。這個時候我們該如何來做呢?你可能會說,這不是很簡單嗎,我只需要在 UserService 中新新增一個 deleteUserByCellphone() 或 deleteUserById() 介面就可以了。這個方法可以解決問題,但是也隱藏了一些安全隱患。
- 刪除使用者是一個非常慎重的操作,我們只希望通過後臺管理系統來執行,所以這個介面只限於給後臺管理系統使用。如果我們把它放到 UserService 中,那所有使用到 UserService 的系統,都可以呼叫這個介面。不加限制地被其他業務系統呼叫,就有可能導致誤刪使用者。
- 當然,最好的解決方案是從架構設計的層面,通過介面鑑權的方式來限制介面的呼叫。不過,如果暫時沒有鑑權框架來支援,我們還可以從程式碼設計的層面,儘量避免介面被誤用。我們參照介面隔離原則,呼叫者不應該強迫依賴它不需要的介面,將刪除介面單獨放到另外一個介面 RestrictedUserService 中,然後將 RestrictedUserService 只打包提供給後臺管理系統來使用。具體的程式碼實現如下所示:
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 { // ...省略實現程式碼... }
- 在剛剛的這個例子中,我們把介面隔離原則中的介面,理解為一組介面集合,它可以是某個微服務的介面,也可以是某個類庫的介面等等。在設計微服務或者類庫介面的時候,如果部分介面只被部分呼叫者使用,那我們就需要將這部分介面隔離出來,單獨給對應的呼叫者使用,而不是強迫其他呼叫者也依賴這部分不會被用到的介面。
04.介面理解為單個API介面或函式
- 現在我們再換一種理解方式,把介面理解為單個介面或函式(以下為了方便講解,我都簡稱為“函式”)。那介面隔離原則就可以理解為:函式的設計要功能單一,不要將多個不同的功能邏輯在一個函式中實現。接下來,我們還是通過一個例子來解釋一下。
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() 函式的功能不夠單一,包含很多不同的統計功能,比如,求最大值、最小值、平均值等等。按照介面隔離原則,我們應該把 count() 函式拆成幾個更小粒度的函式,每個函式負責一個獨立的統計功能。拆分之後的程式碼如下所示:
public Long max(Collection<Long> dataSet) { //... } public Long min(Collection<Long> dataSet) { //... } public Long average(Colletion<Long> dataSet) { //... } // ...省略其他統計函式...
- 不過,你可能會說,在某種意義上講,count() 函式也不能算是職責不夠單一,畢竟它做的事情只跟統計相關。我們在講單一職責原則的時候,也提到過類似的問題。實際上,判定功能是否單一,除了很強的主觀性,還需要結合具體的場景。
- 如果在專案中,對每個統計需求,Statistics 定義的那幾個統計資訊都有涉及,那 count() 函式的設計就是合理的。相反,如果每個統計需求只涉及 Statistics 羅列的統計資訊中一部分,比如,有的只需要用到 max、min、average 這三類統計資訊,有的只需要用到 average、sum。而 count() 函式每次都會把所有的統計資訊計算一遍,就會做很多無用功,勢必影響程式碼的效能,特別是在需要統計的資料量很大的時候。所以,在這個應用場景下,count() 函式的設計就有點不合理了,我們應該按照第二種設計思路,將其拆分成粒度更細的多個統計函式。
- 介面隔離原則跟單一職責原則有點類似,不過稍微還是有點區別。
- 單一職責原則針對的是模組、類、介面的設計。
- 介面隔離原則相對於單一職責原則,一方面它更側重於介面的設計,另一方面它的思考的角度不同。它提供了一種判斷介面是否職責單一的標準:通過呼叫者如何使用介面來間接地判定。如果呼叫者只使用部分介面或介面的部分功能,那介面的設計就不夠職責單一。
05.介面理解為OOP中的介面概念
- 還可以把“介面”理解為 OOP 中的介面概念,比如 Java 中的 interface。我還是通過一個例子來給你解釋。假設我們的專案中用到了三個外部系統:Redis、MySQL、Kafka。每個系統都對應一系列配置資訊,比如地址、埠、訪問超時時間等。為了在記憶體中儲存這些配置資訊,供專案中的其他模組來使用,我們分別設計實現了三個 Configuration 類:RedisConfig、MysqlConfig、KafkaConfig。具體的程式碼實現如下所示。注意,這裡我只給出了 RedisConfig 的程式碼實現,另外兩個都是類似的,我這裡就不貼了。
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 配置資訊的熱更新。所謂“熱更新(hot update)”就是,如果在配置中心中更改了配置資訊,我們希望在不用重啟系統的情況下,能將最新的配置資訊載入到記憶體中(也就是 RedisConfig、KafkaConfig 類中)。但是,因為某些原因,我們並不希望對 MySQL 的配置資訊進行熱更新。
- 為了實現這樣一個功能需求,我們設計實現了一個 ScheduledUpdater 類,以固定時間頻率(periodInSeconds)來呼叫 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(/*省略引數*/); 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(); } }
- 剛剛的熱更新的需求我們已經搞定了。現在,我們又有了一個新的監控功能需求。通過命令列來檢視 Zookeeper 中的配置資訊是比較麻煩的。所以,我們希望能有一種更加方便的配置資訊檢視方式。為了實現這樣一個功能,我們還需要對上面的程式碼做進一步改造。改造之後的程式碼如下所示:
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(); } }
- 至此,熱更新和監控的需求我們就都實現了。我們來回顧一下這個例子的設計思想。
- 設計了兩個功能非常單一的介面:Updater 和 Viewer。ScheduledUpdater 只依賴 Updater 這個跟熱更新相關的介面,不需要被強迫去依賴不需要的 Viewer 介面,滿足介面隔離原則。同理,SimpleHttpServer 只依賴跟檢視資訊相關的 Viewer 介面,不依賴不需要的 Updater 介面,也滿足介面隔離原則。
- 你可能會說,如果我們不遵守介面隔離原則,不設計 Updater 和 Viewer 兩個小介面,而是設計一個大而全的 Config 介面,讓 RedisConfig、KafkaConfig、MysqlConfig 都實現這個 Config 介面,並且將原來傳遞給 ScheduledUpdater 的 Updater 和傳遞給 SimpleHttpServer 的 Viewer,都替換為 Config,那會有什麼問題呢?我們先來看一下,按照這個思路來實現的程式碼是什麼樣的。
public interface Config { void update(); String outputInPlainText(); Map<String, String> output(); } public class RedisConfig implements Config { //...需要實現Config的三個介面update/outputIn.../output } public class KafkaConfig implements Config { //...需要實現Config的三個介面update/outputIn.../output } public class MysqlConfig implements Config { //...需要實現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() { //... } }
- 這樣的設計思路也是能工作的,但是對比前後兩個設計思路,在同樣的程式碼量、實現複雜度、同等可讀性的情況下,第一種設計思路顯然要比第二種好很多。為什麼這麼說呢?主要有兩點原因。
- 首先,第一種設計思路更加靈活、易擴充套件、易複用。因為 Updater、Viewer 職責更加單一,單一就意味了通用、複用性好。比如,我們現在又有一個新的需求,開發一個 Metrics 效能統計模組,並且希望將 Metrics 也通過 SimpleHttpServer 顯示在網頁上,以方便檢視。這個時候,儘管 Metrics 跟 RedisConfig 等沒有任何關係,但我們仍然可以讓 Metrics 類實現非常通用的 Viewer 介面,複用 SimpleHttpServer 的程式碼實現。具體的程式碼如下所示:
public class ApiMetrics implements Viewer {//...} public class DbMetrics implements Viewer {//...} 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 final ApiMetrics apiMetrics = new ApiMetrics(); public static final DbMetrics dbMetrics = new DbMetrics(); public static void main(String[] args) { SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389); simpleHttpServer.addViewer("/config", redisConfig); simpleHttpServer.addViewer("/config", mySqlConfig); simpleHttpServer.addViewer("/metrics", apiMetrics); simpleHttpServer.addViewer("/metrics", dbMetrics); simpleHttpServer.run(); } }
- 第二種設計思路在程式碼實現上做了一些無用功。因為 Config 介面中包含兩類不相關的介面,一類是 update(),一類是 output() 和 outputInPlainText()。理論上,KafkaConfig 只需要實現 update() 介面,並不需要實現 output() 相關的介面。同理,MysqlConfig 只需要實現 output() 相關介面,並需要實現 update() 介面。但第二種設計思路要求 RedisConfig、KafkaConfig、MySqlConfig 必須同時實現 Config 的所有介面函式(update、output、outputInPlainText)。除此之外,如果我們要往 Config 中繼續新增一個新的介面,那所有的實現類都要改動。相反,如果我們的介面粒度比較小,那涉及改動的類就比較少。
06.總結一下分享
- 1.如何理解“介面隔離原則”?
- 理解“介面隔離原則”的重點是理解其中的“介面”二字。這裡有三種不同的理解。
- 如果把“介面”理解為一組介面集合,可以是某個微服務的介面,也可以是某個類庫的介面等。如果部分介面只被部分呼叫者使用,我們就需要將這部分介面隔離出來,單獨給這部分呼叫者使用,而不強迫其他呼叫者也依賴這部分不會被用到的介面。
- 如果把“介面”理解為單個 API 介面或函式,部分呼叫者只需要函式中的部分功能,那我們就需要把函式拆分成粒度更細的多個函式,讓呼叫者只依賴它需要的那個細粒度函式。
- 如果把“介面”理解為 OOP 中的介面,也可以理解為面向物件程式語言中的介面語法。那介面的設計要儘量單一,不要讓介面的實現類和呼叫者,依賴不需要的介面函式。
- 2.介面隔離原則與單一職責原則的區別
- 單一職責原則針對的是模組、類、介面的設計。介面隔離原則相對於單一職責原則,一方面更側重於介面的設計,另一方面它的思考角度也是不同的。
- 介面隔離原則提供了一種判斷介面的職責是否單一的標準:通過呼叫者如何使用介面來間接地判定。如果呼叫者只使用部分介面或介面的部分功能,那介面的設計就不夠職責單一。
07.思考一道課後題
- java.util.concurrent 併發包提供了 AtomicInteger 這樣一個原子類,其中有一個函式 getAndIncrement() 是這樣定義的:給整數增加一,並且返回未増之前的值。
- 我的問題是,這個函式的設計是否符合單一職責原則和介面隔離原則?為什麼?
/** * Atomically increments by one the current value. * @return the previous value */ public final int getAndIncrement() {//...}