Clients should not be forced to depend upon interfaces that they do not use
理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解.
把接口理解为一组 API 接口集合时
在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。
例子
假设当前有个用户管理系统,当前实现了注册和登录功能
public interface Service { boolean register(String cellphone, String password); boolean login(String cellphone, String password);}public class UserService impl Service{ .... }
复制代码
如果需要添加删除用户的功能,且用户后台管理系统才可以执行。为了限制其他业务系统误用,还有其他业务系统也不需要这个功能,所以将删除操作放到 Service 中不合适;参照接口隔离原则,调用者不强制依赖它不需要的接口,将删除接口单独放到另外一个接口 RestrictedUserService 中,RestrictedUserService 只供后台管理系统使用。示例代码:
public interface Service { boolean register(String cellphone, String password); boolean login(String cellphone, String password);}
public interface RestrictedUserService { boolean deleteUserByCellphone(String cellphone); boolean deleteUserById(long id);}
public class UserService implements UserService, RestrictedUserService { // ...省略实现代码...}
复制代码
把“接口”理解为单个 函数时
函数使用者只需要使用函数部分功能时,需要把函数拆分成更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
例子
假设有一个数据统计类,负责计算各种统计数据(平均值、最大最小值等),如下:
public class Statistics { private Long max; private Long min; private Long average; private Long sum; private Long percentile99; private Long percentile999; //...省略constructor/getter/setter等方法...}
class App{ public Statistics count(Collection<Long> dataSet) { Statistics statistics = new Statistics(); //...省略计算statistics所有成员的值的逻辑... return statistics; } }
复制代码
// 将函数count删掉,拆分如下:
public Long max(Collection<Long> dataSet) { //... }public Long min(Collection<Long> dataSet) { //... } public Long average(Colletion<Long> dataSet) { //... }// ...省略其他统计函数...
复制代码
把“接口”理解为 OOP 中的接口
要求接口的设计尽量单一,不要让接口的实现者和调用者,依赖不需要的接口函数。
例子
STEP 1:假设当前有一个配置系统,在内存中维护了 redis、mysql、kafka 3 个组件的配置信息,对应的类实现:
public class RedisConfig { // 省略方法、属性}
public class KafkaConfig { //...省略方法、属性... }public class MysqlConfig { //...省略方法、属性... }
复制代码
STEP 2:当前有一个新需求,让 redis、kafka 支持配置热更,而 mysql 不需要,那么实现代码如下:
public interface Updater { void update();}
public class RedisConfig implemets Updater { //...省略其他属性和方法... @Override public void update() { //... }}
public class KafkaConfig implements Updater { //...省略其他属性和方法... @Override public void update() { //... }}
// 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 { // ... public static void main(String[] args) { ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig, 300, 300); redisConfigUpdater.run(); ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig, 60, 60); kafkaConfigUpdater.run(); }}
复制代码
STEP 3: 又有一个新需求,需要通过 http 方式查看 Mysql、Redis 配置方式,kafka 的配置信息则不需要。
// 新增Viewer接口public interface Viewer { String outputInPlainText(); Map output();}
// 新增Viewer实现public class RedisConfig implemets Updater, Viewer { //...省略其他属性和方法... @Override public void update() { //... } @Override public String outputInPlainText() { /*...*/ } @Override public Map output() { /*...*/ }} public class MysqlConfig implement Viewer{ //...省略方法、属性... @Override public String outputInPlainText() { /*...*/ } @Override public Map output() { /*...*/ }} public class SimpleHttpServer { private String host; private int port; private Map> viewers = new HashMap<>(); public void addViewers(String urlDirectory, Viewer viewer) { if (!viewers.containsKey(urlDirectory)) { viewers.put(urlDirectory, new ArrayList()); } this.viewers.get(urlDirectory).add(viewer); } public void run() { /*...*/ }}
复制代码
Updater, Viewer 两个接口,让不相关的功能实现了隔离
STEP4:假如不遵守接口隔离,将 Updater, 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 ApiMetrics implements Viewer {//...}public class DbMetrics implements Viewer {//...}
public class Application { // ... public static void main(String[] args) { SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389); simpleHttpServer.addViewer("/redis-config", redisConfig); simpleHttpServer.addViewer("/mysql-config", mySqlConfig); simpleHttpServer.addViewer("/api-metrics", new ApiMetrics()); simpleHttpServer.addViewer("/db-metrics", new DbMetrics()); simpleHttpServer.run(); }}
复制代码
其次,如果不使用接口隔离,所有接口都放到 Config,那么:
实现 Config 接口的类,需要实现一些和本功能毫不相干的接口,非常之怪。
如果往 Config 添加新接口,那么所有实现 Config 到类,都要改一遍。
接口隔离原则和单一职责原则区别
单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
思考
java.util.concurrent 并发包提供了 AtomicInteger 这样一个原子类,其中有一个函数 getAndIncrement() 是这样定义的:给整数增加一,并且返回未増之前的值。我的问题是,这个函数的设计是否符合单一职责原则和接口隔离原则?为什么?
/** * Atomically increments by one the current value. * @return the previous value */public final int getAndIncrement() {//...}
复制代码
可以从此方法的设计,来理解单一职责和接口隔离区别:
接口隔离,强调的是调用方,是否只使用了接口中的部分功能?若是,则违反接口隔离,应当细粒度拆分接口,从这个例子看,调用方诉求与方法名完全一致,通过方法内部封装两个操作,实现原子性,达成了调用方的最终目的,不多不少。满足!
单一职责,不强调是否为调用方,只要能某一角度观察出,一个模块/类/方法,负责了多于一件事情,就可判定其破坏了单一职责,基于此经典理论,不假以深层次思考的角度出发,从方法本身的命名(做两件事)就可断定,它一定是破坏了单一职责的,应该拆分为两个操作。 判定职责是否单一,要懂得结合业务场景,业务需求,此方法,其实就是要通过 JDK 提供的 CAS 乐观自选锁(方法最终依赖硬件指令集原语,Compare And Swap)从“原语”这一词的含义看,其实也是同时、原子性地做了一件“完整”的事情,因此,考虑这一点,是可以判定它符合单一职责的。 这其实正是单一职责判定往往见仁见智的原因:基于不同的角度,不同的立场,不同的业务理解,往往可以得到不同的判定结果,但不必纠结,判定过程中用到的思想才是精髓。
评论