设计模式之美-课程笔记10-设计原则4-接口隔离
理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
如何理解”接口隔离原则“
- Interface Segregation Principle: ISP. Clients should not be force to depend upon interfaces that they not use. 客户端(调用者、使用者)应该被强迫依赖他不需要的接口。
- 关键在于如何理解接口
- 一组API接口集合
- 单个API接口或函数
- OOP中的接口概念
把接口理解为一组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 { //... }
-
如果现在要实现删除用户的功能,我们可能会想到的是在接口中增加一个方法
deleteUserById
.-
问题在于,删除用户的权限最好是后台管理系统执行。如果放进去UserService,那所有实现这个接口的类都可以使用这个接口。不加限制的被其他业务系统调用,有可能导致误删用户。
-
最好的设计是从架构层面,增加鉴权系统来限制接口的调用。但是目前没有支持的话,我们也可从代码层面去做。
-
参照接口隔离原则,调用者不应该强迫依赖他不需要的接口。我们可以将删除接口单独放到另一个接口
RestrictedUserService
中,然后将这个Service只打包给后台管理系统使用: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 { // ...省略实现代码... }
-
-
接口在这里被理解为一组接口集合。在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。
把接口理解为单个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,还算了min max等,可以拆成更细的粒度。
public Long max(Collection<Long> dataSet) { //... } public Long min(Collection<Long> dataSet) { //... } public Long average(Colletion<Long> dataSet) { //... } // ...省略其他统计函数...
-
是否功能单一,还是要参照业务场景。
如果在项目中,对每个统计需求,Statistics 定义的那几个统计信息都有涉及,那 count() 函数的设计就是合理的。相反,如果每个统计需求只涉及 Statistics 罗列的统计信息中一部分,比如,有的只需要用到 max、min、average 这三类统计信息,有的只需要用到 average、sum。而 count() 函数每次都会把所有的统计信息计算一遍,就会做很多无用功,势必影响代码的性能,特别是在需要统计的数据量很大的时候。所以,在这个应用场景下,count() 函数的设计就有点不合理了,我们应该按照第二种设计思路,将其拆分成粒度更细的多个统计函数。
-
ISP和SRP有点区别的: SPR针对的是类和模块的设计,而ISP更侧重于接口的设计。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接的判定。(是否是部分使用)。
把接口理解为OOP中的接口
-
举个例子:假设我们的项目中用到了三个外部系统: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 implements 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); kafkaConfigUpdater.run(); } }
-
刚刚的热更新的需求我们已经搞定了。现在,我们又有了一个新的监控功能需求。通过命令行来查看 Zookeeper 中的配置信息是比较麻烦的。所以,我们希望能有一种更加方便的配置信息查看方式。我们可以在项目中开发一个内嵌的 SimpleHttpServer,输出项目的配置信息到一个固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 。我们只需要在浏览器中输入这个地址,就可以显示出系统的配置信息。不过,出于某些原因,我们只想暴露 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(); } }
-
在viewer和updater这两个例子中,ScheduledUpdater只依赖Updater,不需要被强迫去依赖viewer。满足接口隔离原则。
-
如果不遵循ISP,设计出来的代码可能是:一个大的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() { //... } }
-
也能实现,但是对比之前的设计,明显遵循了ISP的代码**扩展性更好,更灵活。**因为Updater和Viewer职责更单一,单一意味着通用、复用性好。比如我们又上了一个Metrics性能统计模块,并希望Metrics也通过SimpleHttpServer显示,此时他就可以使用Viewer接口:
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(); } }
-
第二种设计也有一些无用功,正如我前面说的, 必须要实现所有方法(尽管他不需要用)。