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)从“原语”这一词的含义看,其实也是同时、原子性地做了一件“完整”的事情,因此,考虑这一点,是可以判定它符合单一职责的。 这其实正是单一职责判定往往见仁见智的原因:基于不同的角度,不同的立场,不同的业务理解,往往可以得到不同的判定结果,但不必纠结,判定过程中用到的思想才是精髓。
评论