设计模式之美学习-接口隔离原则(七)
客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。
看起来和单一职责很像 单一职责是约束类和模块 接口隔离原则是约束接口或函数
三种表现形式
"接口"理解为一组API
微服务接口比如dubbo Service
反例
public interface UserService { boolean register(String cellphone, String password); boolean login(String cellphone, String password); UserInfo getUserInfoById(long id); UserInfo getUserInfoByCellphone(String cellphone); boolean deleteUserByCellphone(String cellphone); boolean deleteUserById(long id); } public class UserServiceImpl implements UserService { //... }
比如我们铭感操作 如删除 只在当前服务能够使用,这个时候对外暴露 其他服务也会有这些铭感操作,因为我们接口涉及粒度过大
正例
因为我们对外暴露
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等方法... } /** * 实现max min average sum 的计算 * @param dataSet * @return */ public Statistics count(Collection<Long> dataSet) { Statistics statistics = new Statistics(); //...省略计算逻辑... return statistics; }
如果计算max min average 每一步都非常耗时 外部调用这个接口 只需要计算max 这个时候就强迫调用方依赖了 min average 违反原则
正例
public Long max(Collection<Long> dataSet) { //... } public Long min(Collection<Long> dataSet) { //... } public Long average(Colletion<Long> dataSet) { //... } // ...省略其他统计函数...
调用方根据需求调用对应的接口
把“接口”理解为 OOP 中的接口概念
我们有一组需求 将对应的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 () { //... } } } }
如果我们出于安全考虑mysqlConfig不能支持热部署以及输出到远端 因为我们接口粒度过粗 就会导致MySqlConfig强行依赖了它不需要实现的接口
正例
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(); } }
将热部署和推送配置到远端拆分成2个接口 实现类 根据需求选择实现
二种方式比较
正例设计思路更加灵活、易扩展、易复用。因为 Updater、Viewer 职责更加单一,单一就意味了通用、复用性好。比如,我们现在又有一个新的需求,开发一个 Metrics 性能统计模块,并且希望将 Metrics 也通过 SimpleHttpServer 显示在网页上,以方便查看。这个时候,尽管 Metrics 跟 RedisConfig 等没有任何关系,但我们仍然可以让 Metrics 类实现非常通用的 Viewer 接口
反例因为 Config 接口中包含两类不相关的接口,一类是 update(),一类是 output() 和 outputInPlainText()。理论上,KafkaConfig 只需要实现 update() 接口,并不需要实现 output() 相关的接口。同理,MysqlConfig 只需要实现 output() 相关接口,并需要实现 update() 接口。但第二种设计思路要求 RedisConfig、KafkaConfig、MySqlConfig 必须同时实现 Config 的所有接口函数(update、output、outputInPlainText)。除此之外,如果我们要往 Config 中继续添加一个新的接口,那所有的实现类都要改动。相反,如果我们的接口粒度比较小,那涉及改动的类就比较少。