[5+1]接口隔离原则(二)

前言java

面向对象的SOLID设计原则,外加一个迪米特法则,就是咱们常说的5+1设计原则。图片↑ 五个,再加一个,就是5+1个。哈哈哈。
这六个设计原则的位置有点不上不下。论原则性和理论指导意义,它们不如封装继承抽象或者高内聚低耦合,因此在写代码或者code review的时候,它们很难成为“应该这样作”或者“不该该这样作”的一个有说服力的理由。论灵活性和实践操做指南,它们又不如设计模式或者架构模式,因此即便你能说出来某段代码违反了某项原则,经常也很难明确指出错在哪儿、要怎么改。
因此,这里来讨论讨论这六条设计原则的“为何”和“怎么作”。顺带,做为面向对象设计思想的一环,这里也想聊聊它们与抽象、高内聚低耦合、封装继承多态之间的关系。
数据库

[5+1] 接口隔离原则(一)设计模式

上一部分在这里
架构




[5+1] 接口隔离原则(二)app


接口隔离与面向对象
我记得,项目管理中有一项“干系人管理”。在干系人管理中,咱们须要识别出与项目存在利益关系的各方,而后肯定各自的关注点,最后根据不一样的关注点作不一样的沟通协做、资源协调、指望管理、结果与过程汇报等。图片项目干系人管理
在干系人管理中,咱们须要注意一点:不一样关系人的关注点大多不同。用户关注能不能知足需求;客户关注能不能赚到钱;boss大佬关注结果,项目经理关注过程;产品经理关注功能,技术经理关注质量;对接系统的开发关注接口文档,系统内部开发关注流程、类和库表设计……
在实践中,咱们经常会从一套基础数据中提取不一样内容,以知足不一样干系人的不一样关注点。例如,一份详细设计文档就能够知足产品经理、技术经理、对接开发和内部开发的关注点;一份分工排期表既可让大佬知道何时有结果,也可让项目经理知道过程当中须要注意哪些人、把控哪些点。
尽管有不少不一样数据都来自同一个源头,但咱们通常不会把基础数据直接分发给不一样的干系人。项目经理把进度日报发给boss,boss也许眉头一皱嫌他太啰嗦而后把他开掉了。游戏策划把发给客户的抽卡/氪金分析数据捅给用户,用户也许眉头一皱游戏太垃圾而后就退游保肝了。图片说难听点就是“见人说人话见鬼说鬼话”
面向对象中也有相似的设计思路。有时候,尽管底层使用的是同一个类,可是,面向不一样调用方时,咱们会提供不一样的接口。典型的例子就是LinkedList:分布式

public class LinkedList<E>    extends AbstractSequentialList<E>    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{    // 略}public interface Deque<E> extends Queue<E> {    // 略}

LinkedList类实现了List<E>(从而实现了Collection<E>)、Deque<E>(从而实现了Queue<E>)等接口。于是,当须要使用有序集合、而且随机写入数据时,咱们就能够经过List<E>接口来操做它。若是只须要从表头写入、从表尾读取时,咱们也能够只用Queue<E>接口来操做它:
List<String> list = new LinkedList<>();list.add("1");list.add("2");list.add(1,"0");
Queue<String> queue = new LinkedList<>();queue.add("1");String tail = queue.poll();

相似的还有new ConcurrentHashmap().keySet()——明明是ConcurrentHashMap,生被用成了ConcurrentHashSet。

咱们的业务系统中也有这种类,最典型的就是数据库操做类。通常来讲,数据库操做的增删改查都会放在同一个Dao或者Mapper类中。其中的读操做还好说,写操做必须严密封锁起来,以确保只能在业务操做、业务事务中以一致性方式写入数据。不然的话,“你也说聊斋,我也说聊斋”,你们乱涂乱画起来,岂不要把婴宁嫁给宁采臣了? 图片 聂小倩第一个不答应
封锁写操做的最简单方式,就是接口隔离:读操做和写操做定义成不一样的接口。读接口能够任意使用;写接口只容许业务操做使用,其它操做想要写入数据库,必须调用业务操做接口。这样,就能够避免完整业务数据被部分写入、进而违反业务一致性的问题了。
从不一样的方面描述同一件事情,不管在管理上仍是在面向对象设计上,都是一种很高效并且很必要的工做方式。在管理上,咱们把这种工做方式叫作“见人说人话,见鬼说鬼话”;在面向对象设计上,咱们把它叫作“接口隔离原则”。


接口隔离与抽象
不少时候,咱们一提到抽象,就会直接把它与接口划上等号。因此很天然的,谈到接口隔离与抽象,咱们也会直接地想到把“接口隔离”与“更小的抽象”划上等号。
这个观点倒也没有什么大问题。尤为是当接口隔离原则被简化为“把庞大而臃肿的接口拆分红更小、更具体的接口”时,它与抽象之间的关系天然就只能是“把庞大而臃肿的抽象拆分为更小、更具体的抽象”了。
例如,有时咱们会在Dao层之上,增长一个DbService层,将其用做数据库操做的更高层抽象:ide

public interface  DbService<T,Q>{    /**查询一条数据*/    Optional<T> query(Q query);    /**新增一条数据*/    T save(T data);    /**更新数据*/    Optional<T> edit(T data, Q query);    /**删除数据*/    int remove(Q query);}

这个数据库操做抽象看起来不错,并且蛮通用的。不过,在业务中,咱们能够容许任一功能模块都来读数据,但只能容许在特定的业务流程中写数据。所以,读操做和写操做应当区别对待。

然而,DbService所定义的抽象却把读、写两个操做同时暴露了出来:只要能够读数据,就能够写数据。例如:
public class QueryDataService{    private DbService<Data, Query> dbService;
   public Data queryData(Long id){        Query q = new Query(id);        Data data = dbService.query(q)        return data;    }}

实际上这个类中只须要查询数据。可是注入DbService接口以后,这个类也具有了写数据的能力。也就是说,“写操做”被泄露到了限定的业务流程以外。虽然大多数状况下,泄露出去的“写操做”都是可控的;然而对“我不想卖、你不能卖”的抽象设计来讲,这就是一个设计上的问题。

要改正这个问题,其实也很简单:把读写操做拆分到两个接口中就能够了:
public interface DbReader<T,Q>{    /**查询一条数据*/    Optional<T> query(Q query);}public interface DbWriter<T,Q>{    /**新增一条数据*/    T save(T data);    /**更新数据*/    Optional<T> edit(T data, Q query);    /**删除数据*/    int remove(Q query);}/**保留DbService,以便:* 1. 兼容老代码 * 2. 为某些特定业务提供读写双操做的接口,*/public interface  DbService<T,Q> extends DbReader<T,Q>, DbWriter<T,Q>{}

这种接口拆分,不正是接口隔离原则所要求的吗?

接口隔离原则与抽象之间的关系,不只仅是这样的接口拆分。若是说设计抽象的目的是“我不想卖、你不能买”,那么接口隔离原则的要求就是“我不想买、你不能卖”——对,合起来就是“不能强买强卖”。严格的说,同时符合了这两个要求的抽象,才是合格的设计。 图片 旅游和购物也应该“隔离”开
就像前面列举的一些例子同样:调用者并不关心doSth()方法的步骤,接口就不该该提供诸如step1()/step2()这样的方法;调用者只须要approve()方法,接口就不该该提供queryUser()方法。虽然从抽象设计的角度来看,它们的确是服务方自愿提供的方法。但从接口隔离原则的角度来看,这些方法可不是调用方想要的东西。
这就像去理发店理发时,Tony老师推销给你一张五折会员卡并说服你预存一千块钱同样——看起来是他让顾客获得了实惠,其实是他“绑架”了顾客下一次的消费行为。对于消费者来讲,换一家理发店的成本也许不高,况且原先的卡还能够挂咸鱼卖掉;可是对系统来讲,一次重构调整的成本可就很差说了。若是能把重构范围约束在抽象内部,那大概就花个工本费;若是重构范围包括了接口的全部调用方——尤为是分布式环境下的接口调用方——那简直就是地狱难度了。


接口隔离与高内聚低耦合
其实前面已经把接口隔离与高内聚低耦合之间的关系表述得很清楚了:适当地遵循接口隔离原则,有助于建立高内聚低耦合的抽象和模块。
例如,把SomeService接口中的step1()/step2()等方法删掉,只保留doSth()方法,不只遵循了接口隔离原则,也下降了服务调用者与提供者之间的耦合度。结合《细说几种耦合》来看,这个改造至少能够避免双方产生内容耦合。
而把FlowService中关于用户的功能拆分到UserService中,则能够有效地提升对应模块的内聚性:用户相关功能和流程相关功能都放到各自的模块中,内聚性至少能够从偶然内聚提升到过程内聚甚至顺序内聚(参考《细说几种内聚》)。内聚性提升了,天然地,用户模块和流程模块之间的耦合性也下降了。
虽然遵循接口隔离原则有助于提升内聚性、下降耦合性,可是“过犹不及”,过于强调接口隔离,有时反而会下降内聚、增长耦合。
例如,Java中的Iterator接口中,就有这样两个方法:ui

public interface Iterator<E> {
   boolean hasNext();
   E next();
   // 其它方法,略}

大多数状况下,hasNext()方法和next()都是配套使用的。能够说,这两个方法在一块儿,才能构成一个完整的迭代器抽象。若是咱们机械地套用接口隔离原则,把它俩硬生生地拆分到两个不一样的接口中,反而下降了这个抽象设计的内聚性。

实际上,前面所讨论的把DbService接口拆分红DbReader和DbWriter的例子,也可能产生相似的问题。若是DbService不是供业务逻辑使用、而是仅仅提供资源服务,那么增删改查操做就没有必要拆开了。 图片 好好的家伙事儿拆得稀碎也不行



接口隔离与封装继承多态
接口隔离原则与封装的关系很是容易理解;相比之下,它与继承、多态之间的关系就不那么清晰了。
在面向对象中,接口是实现封装特性的最有力也最多见的手段。与接口密切相关的接口隔离原则,天然也与封装特性有着密切的关系。
相信咱们不少人都被“过分包装”恶心过:实际的商品重不到三两、大不过拳头,非要左一层“精美包装”、右一层“豪华包装”。结果呢?买的人花一笔冤枉钱买了个不痛快,用的人拆一大堆空盒子用得不痛快。哪怕是用来收礼,若是知道这“礼物”的90%是包装盒,送礼的人恐怕也会以为脸面无光吧!spa

图片

过分包装设计


冗长的接口和过分包装的问题同样,都是自觉得是地把一大堆用户不须要的、深恶痛绝的东西强加给用户。这种“强加于人”,在市场营销中叫“捆绑销售”,在面向对象中就叫“不当封装”:该“封”起来的没有作好密封,不应“装”进来的一股脑地装到了一块儿。
可见,恰当的接口隔离能够保证咱们的类拥有更好的封装性。一样的,作好接口隔离,也能在类的继承方面给咱们提供便利。
在Java中,因为接口方法都只有方法签名、没有方法体,所以,实现类只有两个选择:将自身声明为抽象类,或者实现接口中的全部方法。虽然Java8容许接口方法定义方法体、以提供一个默认实现,但这个默认实现的功能很是弱,基本只能用来向下兼容,真要实现业务功能,还得靠实现类来重写方法。总之,咱们仍能够认为:接口中声明的方法,最终都要被实现类重写。由此能够推断:一个只有三个方法的接口,和一个包含了十三个方法的接口相比,显然是前者对实现类更友好。
固然,咱们也能够采用接口-基类-实现类的层次结构,来减小实现大接口时的开发量。例以下面这样:

public interface Service{    void method1();    void method2();    // 中间若干方法,略    void methodN();    }public class BaseService implements Service{    public void method1(){        // 默认实现    }    public void method2(){        // 默认实现    }    // 中间若干方法的默认实现    public void methodN(){        // 默认实现    }}
public class SubService extends BaseService{    public void method5(){        // 实现本身所需方法    }}

这样作,确实能够解决每次实现接口都须要重写全部方法的问题。不过,此时咱们又要面对另外一个可能更严重的问题:若是咱们的SubService只须要提供method5()这一个方法,而不须要提供BaseService中的其它功能,也就是说前者与后者并不知足继承所要求的“is-a”关系,此时咱们让SubService继承BaseService,真的不是捡起芝麻丢了西瓜吗?

诚然,并非全部的大接口都会有这样的问题;但几乎全部的小接口都没有这种问题。反过来讲,使用小接口时,咱们几乎不用担忧出现过分继承问题;而使用大接口时,咱们至少应该认真思考一下这个接口及其继承层次是否合理。这也就是此前提到的接口隔离原则的定位:它并非绝对不可打破的禁忌,而是潜在问题、系统风险的一种指示剂。
相比封装与继承,接口隔离原则与多态之间的关系更加直观些。若是不使用多态,那么咱们必定会违反接口隔离原则:当一个接口下只有一个实现类时,增长新的逻辑是都不免要增长接口方法,长此以往,这个接口就会变成一个巨无霸,接口隔离原则天然就无从谈起了。反过来讲,使用多态特性,咱们就应该遵照接口隔离原则、应该定义和使用“小而美”的接口。不然的话——设想一个声明了十多个方法的接口,每次借助多态特性来增长新的实现类时,咱们都不得不把全部方法都重写一遍,那得多么费劲!
固然,咱们仍然能够借助接口-基类-实现类的层次结构避免这个问题。但此时,咱们又回到了前面提到的那个问题上:“前者与后者并不知足继承所要求的‘is-a’关系,此时咱们让SubService继承BaseService,真的不是捡起芝麻丢了西瓜吗?”


接口隔离与其它设计原则
接口隔离与单一职责
接口隔离原则与单一职责原则之间的关系是显而易见的:违反接口隔离原则,就必定会违反单一职责原则。
不管咱们把接口隔离原则定义为“客户端只须要依赖他们须要的接口”、仍是定义为“把大接口拆分红小接口”,只要违反了这一原则,接口内就势必会出现不该出现的方法声明。例如前面示例中反复提到的接口实现步骤、其它模块功能等。而接口方法通常都是抽象方法,必须由实现类重写。在二者的叠加影响下,实现类中必定会出现本来不该出现的方法实现。即便咱们使用了接口-基类-实现类的层次结构,或者为接口方法提供了默认方法体,也没法解决这一问题:基类中已实现的方法,以及接口中的默认方法,都会被实现类继承下来,成为它本身的功能。这样一来,实现类想要保持单一职责,就只能是个奢望了。
接口隔离原则与单一职责原则之间的这种关系,归根结底的说,是接口与实现类之间的关系决定的:接口对外声明了“我能作什么”,实现类则为接口提供了“怎么作”的功能支撑。这就有点像产品和开发同样:产品提需求,定义“这个产品能作什么”;开发出设计、写代码,解决“怎么作”的问题。
质量低下的产品需求是开发的一大痛苦之源;相似的,质量低下的接口定义也会给开发带来无尽的痛苦。应付糟糕的产品需求已经让人心力交瘁了,开发又何苦为难本身呢?仍是认认真真遵照接口隔离原则、定义简单清晰的接口吧!
接口隔离与开闭
在面向对象思想中,开闭原则的核心在于合理、高效地利用继承和多态特性来“增长”新的实现类、而不是“修改”原有的实现类。所以,接口隔离原则与开闭原则之间的关系,须要继承和多态来理解:明白了接口隔离原则与继承、多态之间的关系,也就很容易理解它与开闭原则的关系了。
接口隔离与里氏替换
接口隔离原则主要讨论接口的设计,而里氏替换原则则“下沉”到了继承层次中,主要讨论子类继承父类时的问题。所以,两者的关系与接口隔离和开闭之间的关系同样,也须要绕道继承和多态。



往期索引

《面向对象是什么》

从具体的语言和实现中抽离出来,面向对象思想到底是什么? 公众号:景昕的花园面向对象是什么


抽象

抽象这个东西,提及来很抽象,其实很简单。

花园的景昕,公众号:景昕的花园抽象


高内聚与低耦合

细说几种内聚

细说几种耦合

"高内聚"与"低耦合"是软件设计和开发中常常出现的一对概念。它们既是作好设计的途径,也是评价设计好坏的标准。

花园的景昕,公众号:景昕的花园高内聚与低耦合


封装

继承

多态》

——“面向对象的三大特性是什么?”——“封装、继承、多态。”


《[5+1]单一职责原则》

单一职责原则很是好理解:一个类应当只承担一种职责。由于只承担一种职责,因此,一个类应该只有一个发生变化的缘由。 花园的景昕,公众号:景昕的花园[5+1]单一职责原则


《[5+1]开闭原则(一)

《[5+1]开闭原则(二)

什么是扩展?就Java而言,实现接口(implements SomeInterface)、继承父类(extends SuperClass),甚至重载方法(Overload),均可以称做是“扩展”。什么是修改?在Java中,严格来讲,凡是会致使一个类从新编译、生成不一样的class文件的操做,都是对这个类作的修改。实践中咱们会放宽一点,只有改变了业务逻辑的修改,才会纳入开闭原则所说的“修改”之中。 花园的景昕,公众号:景昕的花园[5+1]开闭原则(一)


《[5+1]里氏替换原则(一)

《[5+1]里氏替换原则(二)

里氏替换原则(Liskov Substitution principle)是一条针对对象继承关系提出的设计原则。它以芭芭拉·利斯科夫(Barbara Liskov)的名字命名。1987年,芭芭拉在一次名为“数据的抽象与层次”的演讲中首次提出这条原则;1994年,芭芭拉与另外一位女性计算机科学家周以真(Jeannette Marie Wing)合做发表论文,正式提出了这条面向对象设计原则

花园的景昕,公众号:景昕的花园[5+1]里氏替换原则(一)


[5+1]接口隔离原则(一)

通常咱们会说,接口隔离原则是指:把庞大而臃肿的接口拆分红更小、更具体的接口。不过,这并非接口隔离原则的定义。 实际上,接口隔离原则的定义实际上是这样的…… 客户端不该被迫依赖它们压根用不上的接口; 或者反过来讲,客户端应该只依赖它们要用的接口。
花园的景昕,公众号:景昕的花园[5+1]接口隔离原则(一)


景昕的花园.png

相关文章
相关标签/搜索