前几节课,我们学习了SOLID原则中的单一职责原则,开闭原则,里氏替换原则,今天则是接口隔离原则
如何理解呢?
其直译含义为 客户端不应该强迫依赖它不需要的接口(只给客户端他需要的功能)
那么对应到我们实际开发中,接口这个概念,可以理解为三种,一组API接口或者集合,单个API接口或者方法函数,OOP中的接口概念
1.一组接口或者集合
微服务系统提供了一组和用户相关的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) } |
一个标准的用户登录登出系统,方便其他的微服务使用,但是,如果后台管理系统需要一个删除用户的功能,希望用户系统提供一个删除用户的功能
如果只是在UserServcie中暴露这个新功能,那么是直接解决了,但是对于很多本来不应该提供删除用户功能的微服务,也能直接使用这个删除用户的功能了,
最好的方式,是使用接口鉴权的方式,进行限制接口的调用工作,但是如果不使用接口鉴权,而是从代码设计的角度来说,调用者不应该强迫其依赖不需要的接口,那么将删除接口放到单独的接口中
RemoveUserServcie中,然后讲这个接口只提供给后天管理系统使用,即可
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 { // …省略实现代码… } |
上面的例子中,将接口隔离原则的一个接口,理解为一组接口集合,如果某类接口只被一部分调用者使用,那么就应该隔离起来,单独的给调用者使用,而不是强迫其他调用者也依赖这部分不需要的接口
2.把接口理解为单个API或者方法函数
现在我们换一种理解方式,理解为单个接口或者函数,那么这个接口隔离原则就可以认为是函数的设计功能要单一,不要讲多个不同的功能逻辑放到一个函数中实现
假如有一个如下的count函数
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中,会一口气的计算 求最大值 最小值 平均值等,按照单一职责原则,似乎有点冗余
但如果在项目中,这几个统计信息一同显示的话,这就并不显的冗余了,相反,如果每个统计需求都只涉及一部分数据,那么就显得多余了
于是,我们应该将其拆分成多个,粒度更细的统计函数
接口隔离原则和单一职责原则,接口有类似,但是单一职责原则针对的是类,模块,接口的设计,接口隔离原则是单一职责原则,更侧重于接口的设计
3.把接口理解为OOP的接口概念
加入项目中有了三个外部系统,Redis,MySQL,Kafka,每个系统都对应一系列的配置信息,为了存储着写配置信息,每个系统都对应着一系列的配置信息
分别设计了三个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配置信息的热更新,即为可以更改了配置信息,不直接重启系统的情况下,直接将最新的信息加载到内存中
为了实现这个功能需求,设计实现了一个ScheduledUpdate类,定期调用RedisConfig和KafkaConfig的update()方法更新配置信息
那么我们就设计一个Update接口,然后让RedisConfig和KafkaConfig来进行实现
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(); } } |
刚刚我们的需求已经搞定了,现在有了一个新的监控功能需求,只针对MySQL和Resdis,通过命令行来查看Zookeeper中的配置化信息比较麻烦,于是我们设计了一个Viewer接口
然后RedisCong和MySQLConfig实现着这个Viewer接口
如果不遵循单一职责原则和接口隔离原则,将所有Update和Viewer接口放到了一个Config接口中,让KafkaConfig和RedisConfig和MySQLConfig实现这个Config接口
将ScheduledUpdate的Update和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() { //… } } |
这样就意味着,需要实现的代码量子增加了,所有的都需要实现Config接口
而且,复用性不如拆分的好,开发一个Metrics性能统计模块,并且希望将Metrics也通过Metrics显示到SimpleHttpServer显示到页面上,这样可以复用Viewer接口
如果是使用了Config接口,还需要实现Update方法
那么总结一下
如何理解接口隔离原则
从三个层面上理解
如果理解为一组接口集合,那么可以将这部分接口隔离出来,单独的给调用者使用
如果理解为单一一个接口的话,如果调用者只需要部分功能,可以将一个函数拆分为粒度更细的函数
如果理解为OOP中的接口的话,可以理解为面向对象编程语言中的抽象,设计更加单一
接口隔离原则和单一职责原则的区别
接口隔离原则相对于单一职责原则,更加侧重于接口逇设计,接口隔离原则提供了一种判断接口职责是否单一的标准,通过调用者如何使用接口来判定,如果只使用接口的部分功能,那么可以认为是职责不单一
Java.util.concurrent提供了AtomicInteger原子类,其中有一个函数getAndIncrement(),是定义为,给整数增加一,并且返回未增之前的值,是否符合单一职责原则和接口隔离原则
针对这个问题,我的理解是,对于单一职责原则和接口隔离原则,是看一个方法的功能是否单一,能不能再继续拆分为粒度更小的接口,而这个方法,其粒度已经足够小了,而且其内部是使用CAS指令来进行替换的,对于CPU来说,是一个没法再拆分的原子操作,而且AtomicInteger内部也提供了get()和set()方法,也针对不同场景提供了incrementAndGet()等方法,方法多而不杂,都是对CAS指令的封装,足够应对多种场景下的使用,我认为是可以的