写点什么

设计模式之美—接口隔离

作者:GalaxyCreater
  • 2023-03-05
    广东
  • 本文字数:4185 字

    阅读完需:约 14 分钟

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; } }
复制代码
  • 如果业务上,如果每个统计,的确需要 Statistics 定义的所有信息都要涉及,那这个设计没问题。

  • 如果 count 其实只用到了部分,比如 average、max、min,那需要将函数功能拆分成更小粒度的。如:

// 将函数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}
复制代码
  • 首先,第一种设计思路更加灵活、易扩展、易复用。因为 Updater、Viewer 职责更加单一,单一就意味了通用、复用性好。假如又新增了个需求,开发一个 Metrics 性能统计功能,并将统计结果通过 SimpleHttpServer 方式查看,那 Metrics 可以实现 Viewer 接口,复用 SimpleHttpServer 的查看代码。

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

发布于: 刚刚阅读数: 4
用户头像

GalaxyCreater

关注

还未添加个人签名 2019-04-21 加入

还未添加个人简介

评论

发布
暂无评论
设计模式之美—接口隔离_设计模式_GalaxyCreater_InfoQ写作社区