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é)單一熔酷。