设计模式之美学习-接口隔离原则(七)

客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

看起来和单一职责很像  单一职责是约束类和模块  接口隔离原则是约束接口或函数

三种表现形式

"接口"理解为一组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 中继续添加一个新的接口,那所有的实现类都要改动。相反,如果我们的接口粒度比较小,那涉及改动的类就比较少。

posted @ 2020-01-02 15:42  意犹未尽  阅读(254)  评论(0编辑  收藏  举报