设计原则:接口隔离原则(ISP)

接口隔离原则的英文是Interface Segregation Principle,缩写就是ISP。与里氏替换原则一样其定义同样有两种

定义1:

Clients should not be forced to depend upon interfaces that they don'tuse.

(客户端被强迫不应该依赖它不需要的接口。)

定义2:

The dependency of one class to another one should depend on the smallest possibleinterface.

(类间的依赖关系应该建立在最小的接口上。)

这两种定义相比较,我更喜欢它的第一种定义。其中最重要的概念就是“接口”,这里的接口其实不仅仅是指OOP概念中的接口,其小到类所暴露出来的public方法、所提供的公共属性,大到业务上一组API的组合,甚至系统对外所提供的服务,都可以称之为接口,接口是一种抽象的约定。而客户端可以理解为接口的调用者或者使用者。

这个表述看起来很容易,但是在真正设计中却很难,在大多数的项目里,也是经常被违背的原则之一,因为设计者往往很难站在使用者的角度上去看待问题,甚至有很多设计者根本没有接口的概念,他们往往只从类的角度上去思考问题,而在类设计完毕后,而为了使用接口象征性的增加一个接口,然后把类的方法签名搬到接口里而已。(我们可以想想,自己在项目中是不是也是先写类,后写接口呢?)

2.如何理解并运用好接口隔离原则

接口隔离原则要求我们尽量提供小而美的接口,而不是一个庞大臃肿的接口,以试图满足所有的调用者使用,它是对接口的的一种规范和约束。

其实在设计中想要运用好接口隔离原则,有一个好的办法,就是需要我们站在使用者的角度上去思考问题,按需去设计接口,我们可以通过几个例子来看一下

2.1 OOP中的接口隔离原则

现在,我们有一个商品系统,我想绝对多数的系统中都会按照下面这种方式进行接口的设计

image-20210317082659780

它提供了CRUD操作供客户端调用。随着业务的不断发展壮大,我们发现商品访问的性能越来越差,数据库的压力也越来越大,这时我们需要对商品系统增加缓存的功能,但是有些场景下我们又需要能够实时的查询到商品系统,这种场景下应该怎么办?


public ProductInfo get(String id) {
	if(cache.contains(key)){
		return cache.get(key);
	}
	return productRepository.get()
}

public ProductInfo get(String id,boolean isCache) {
	if(cache.contains(key)&&isCache){
		return cache.get(key);
	}
	return productRepository.get()
}

这时许多人的做法可能是增加一个参数isCache由客户端传入来标记是否需要读取缓存,不得不说这真是一个馊主意,因为这违背了一个最基础的原则——开闭原则,它会给我们后续维护带来很大的灾难。

也有一些人可能会想到我提供一个CacheProductService 也实现一下IProductService 在这个服务里面做缓存的功能,这样需要缓存的客户端就实例化CacheProductService 不需要缓存的客户端就还是实例化原来的 ProductService

public class CacheProductService implements IProductService {
    @Override
    public ProductInfo get(String id) {
        if(cache.contains(id)){
            return cache.get(id);
        }
        return productRepository.get(id);
    }

    @Override
    public List<ProductInfo> getList() {
        if(cache.contains("product-key")){
            return cache.get("product-key");
        }
        return productRepository.getList();
    }

    @Override
    public void create(ProductInfo productInfo) {    }

    @Override
    public void modify(ProductInfo productInfo) {    }

    @Override
    public void delete(String id) {    }
}

就像我上面写的这样,但这样又有一些问题,首先这样的设计违背了里式替换原则,再者增删改操作并不需要缓存。

那么到底应该如何去做呢?这个时候我们可以利用接口隔离原则,

  1. 把原来的IProductService 拆分成两个IReadProductService IOperProductService
  2. 然后我们的ProductService实现这两个接口,而CacheProductService只实现IReadProductService
  3. 需要缓存的客户端使用IReadProductService ,不需要缓存的客户端使用IReadProductService IOperProductService

image-20210318084901258

public class ProductService implements IReadProductService,IOperProductService {
    @Override
    public ProductInfo get(String id) {
        return productRepository.get(id);
    }
    
    @Override
    public List<ProductInfo> getList() {
        return productRepository.getList();
    }
    
    @Override
    public void create(ProductInfo productInfo) {
        productRepository.create(productInfo);
    }
    
    @Override
    public void modify(ProductInfo productInfo) {
        productRepository.modify(productInfo);
    }
    
    @Override
    public void delete(String id) {
        productRepository.delete(id);
    }
}

public class CacheProductService implements IReadProductService {
    @Override
    public ProductInfo get(String id) {
        if(cache.contains(id)){
            return cache.get(id);
        }
        return productRepository.get(id);
    }

    @Override
    public List<ProductInfo> getList() {
        if(cache.contains("product-key")){
            return cache.get("product-key");
        }
        return productRepository.getList();
    }
}

甚至在客户端和设计需要的情况下我们可以把简单的CRUD接口拆分最喜欢的拆分成但方法接口ICreate IModify IDelete IRead。接口的规模越小,其复用性和灵活性也就越高,但我们必须注意一点那就是按需设计,因为复用性和灵活性增加的同时,必然也会带来增加系统的复杂度,降低可读性等问题,因此我们必须要掌握好设计的度。

public interface ICreate<T> {
    void create(T t);
}

public interface IModify<T> {
    void modify(T t);
}

public interface IDelete {
    void delete(String id);
}

public interface IRead<T> {
    T get(String id);

    List<T> getList();
}

public class ProductService implements ICreate<ProductInfo>,IModify<ProductInfo>,IDelete,IRead<ProductInfo>{

}

2.3 类设计中的接口隔离原则

其实接口隔离原则的应用不应该局限于OOP中的"接口",我们不应被接口所迷惑,接口隔离原则中的“接口”,更像是一种约定,因此在类的设计中我们同样应该遵循接口原则,因为类所提供的一些公共方法也是一种约定。

比如我们在监控、统计等系统中通常会用到各个指标的统计,比如均值、求和、最大值、中位数.......,这时我们设计了一个Indicator类,里面提供了sum avg p50 p95等属性,然后提供了一个compute方法来计算各个指标的值,最后返回一个Indicator对象。

public class Indicator {

    private Long sum;

    private Long min;

    private Long avg;

    private Long p50;

    private Long p95;

    private Long p99;

    public Indicator compute(List<Long> list){

        Indicator indicator=new Indicator();
		//...
        return indicator;
    }

   
}

我们想一想这样会不会有问题,如果我们所有的调用者都需要所有的指标,这样设计并没有什么问题,但如果有些调用者可能仅仅需要其中的某一个或者几个指标就会有问题了。因为,如果我只需要其中一个指标,但却计算了所有的指标值,浪费时间性能不说,一旦其中某一个指标计算过程中除了错误,就会导致我连其它几个指标都拿不到。这样客户端就依赖了自己所不需要的东西,违背了接口隔离的原则。

这时我们可以考虑把各个指标的计算分开来

   public Long min(List<Long> list){   }

   public Long max(List<Long> list){   }

   public Long avg(List<Long> list){   }

   ......

这样看起来接口隔离原则跟单一职责原则有些相似,但其实是有不同的,单一职责原则主要是针对模块、类、方法的设计,注重职责的单一,而接口隔离原则更注重站在调用者的角度上看约定是否存在自己所不需要的东西,它要求给每个使用者都按需提供接口,而不是建立一个庞大臃肿的接口以供所有调用者使用。

2.3 系统设计中的接口隔离原则

不仅仅是在OOP中的接口和类的设计要遵循接口隔离原则,在系统对外所提供的API的设计中,我们同样应该遵循接口隔离原则。

例如在用户系统的设计中,多数人都会提供一个用户API,然后这个API提供了一个大而全的接口列表。createmodifydeleteget .......

image-20210317085523677

但有些场景下并不合理的,因为这是站在服务提供者的角度上进行设计的。如果你的用户服务仅仅是提供给后台管理系统使用那么并没有问题,但是如果同时也提供给登录系统使用那么就会有问题了,因为登录系统可能只需要登录注册两个操作,那么对于登录系统的来说用户服务就提供了它所不需要的接口。这样以来我们对登录系统暴漏了删除和修改接口增加了系统风险。

此时,我们应该对登录系统单独的提供一组API接口。如下图所示

image-20210317085843149

3 总结

大而全的东西存在了太多的不确定性,在接口的设计中,我们应该遵循接口隔离原则,尽量提供小而美的接口。但同时我们也应该注意设计要适度,因为越小的东西就越灵活,但如果过于小又会增加系统的复杂性。

接口隔离原则强调了客户端被强迫不应该依赖它不需要的接口。它的应用不应局限于简单的OOP接口,小到类、方法的设计,大到系统之间的交互......接口隔离原则都可以指导我们进行更好的设计。

系列文章

设计原则:单一职责(SRP)

设计原则:开闭原则(OCP)

设计原则:里式替换原则(LSP)

设计原则:接口隔离原则(ISP)

设计原则:依赖倒置原则(DIP)

何谓高质量代码?

理解RESTful API

关注下方公众号,回复“代码的艺术”,可免费获取重构、设计模式、代码整洁之道等提升代码质量等相关学习资料

posted @ 2021-04-21 08:36  Mr于  阅读(615)  评论(0编辑  收藏  举报