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)類都要改動芹敌。相反痊远,如果我們的接口粒度比較小,那涉及改動的類就比較少氏捞。