前言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> { // 略}
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();
接口隔离与抽象
不少时候,咱们一提到抽象,就会直接把它与接口划上等号。因此很天然的,谈到接口隔离与抽象,咱们也会直接地想到把“接口隔离”与“更小的抽象”划上等号。
这个观点倒也没有什么大问题。尤为是当接口隔离原则被简化为“把庞大而臃肿的接口拆分红更小、更具体的接口”时,它与抽象之间的关系天然就只能是“把庞大而臃肿的抽象拆分为更小、更具体的抽象”了。
例如,有时咱们会在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);}
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; }}
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>{}
接口隔离与高内聚低耦合
其实前面已经把接口隔离与高内聚低耦合之间的关系表述得很清楚了:适当地遵循接口隔离原则,有助于建立高内聚低耦合的抽象和模块。
例如,把SomeService接口中的step1()/step2()等方法删掉,只保留doSth()方法,不只遵循了接口隔离原则,也下降了服务调用者与提供者之间的耦合度。结合《细说几种耦合》来看,这个改造至少能够避免双方产生内容耦合。
而把FlowService中关于用户的功能拆分到UserService中,则能够有效地提升对应模块的内聚性:用户相关功能和流程相关功能都放到各自的模块中,内聚性至少能够从偶然内聚提升到过程内聚甚至顺序内聚(参考《细说几种内聚》)。内聚性提升了,天然地,用户模块和流程模块之间的耦合性也下降了。
虽然遵循接口隔离原则有助于提升内聚性、下降耦合性,可是“过犹不及”,过于强调接口隔离,有时反而会下降内聚、增长耦合。
例如,Java中的Iterator接口中,就有这样两个方法:ui
public interface Iterator<E> {
boolean hasNext();
E next();
// 其它方法,略}
接口隔离与封装继承多态
接口隔离原则与封装的关系很是容易理解;相比之下,它与继承、多态之间的关系就不那么清晰了。
在面向对象中,接口是实现封装特性的最有力也最多见的手段。与接口密切相关的接口隔离原则,天然也与封装特性有着密切的关系。
相信咱们不少人都被“过分包装”恶心过:实际的商品重不到三两、大不过拳头,非要左一层“精美包装”、右一层“豪华包装”。结果呢?买的人花一笔冤枉钱买了个不痛快,用的人拆一大堆空盒子用得不痛快。哪怕是用来收礼,若是知道这“礼物”的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(){ // 实现本身所需方法 }}
接口隔离与其它设计原则
接口隔离与单一职责
接口隔离原则与单一职责原则之间的关系是显而易见的:违反接口隔离原则,就必定会违反单一职责原则。
不管咱们把接口隔离原则定义为“客户端只须要依赖他们须要的接口”、仍是定义为“把大接口拆分红小接口”,只要违反了这一原则,接口内就势必会出现不该出现的方法声明。例如前面示例中反复提到的接口实现步骤、其它模块功能等。而接口方法通常都是抽象方法,必须由实现类重写。在二者的叠加影响下,实现类中必定会出现本来不该出现的方法实现。即便咱们使用了接口-基类-实现类的层次结构,或者为接口方法提供了默认方法体,也没法解决这一问题:基类中已实现的方法,以及接口中的默认方法,都会被实现类继承下来,成为它本身的功能。这样一来,实现类想要保持单一职责,就只能是个奢望了。
接口隔离原则与单一职责原则之间的这种关系,归根结底的说,是接口与实现类之间的关系决定的:接口对外声明了“我能作什么”,实现类则为接口提供了“怎么作”的功能支撑。这就有点像产品和开发同样:产品提需求,定义“这个产品能作什么”;开发出设计、写代码,解决“怎么作”的问题。
质量低下的产品需求是开发的一大痛苦之源;相似的,质量低下的接口定义也会给开发带来无尽的痛苦。应付糟糕的产品需求已经让人心力交瘁了,开发又何苦为难本身呢?仍是认认真真遵照接口隔离原则、定义简单清晰的接口吧!
接口隔离与开闭
在面向对象思想中,开闭原则的核心在于合理、高效地利用继承和多态特性来“增长”新的实现类、而不是“修改”原有的实现类。所以,接口隔离原则与开闭原则之间的关系,须要继承和多态来理解:明白了接口隔离原则与继承、多态之间的关系,也就很容易理解它与开闭原则的关系了。
接口隔离与里氏替换
接口隔离原则主要讨论接口的设计,而里氏替换原则则“下沉”到了继承层次中,主要讨论子类继承父类时的问题。所以,两者的关系与接口隔离和开闭之间的关系同样,也须要绕道继承和多态。
往期索引
从具体的语言和实现中抽离出来,面向对象思想到底是什么? 公众号:景昕的花园面向对象是什么
《抽象》
抽象这个东西,提及来很抽象,其实很简单。
花园的景昕,公众号:景昕的花园抽象
《高内聚与低耦合》
《细说几种内聚》
《细说几种耦合》
"高内聚"与"低耦合"是软件设计和开发中常常出现的一对概念。它们既是作好设计的途径,也是评价设计好坏的标准。
花园的景昕,公众号:景昕的花园高内聚与低耦合
《封装》
《继承》
《多态》
——“面向对象的三大特性是什么?”——“封装、继承、多态。”
单一职责原则很是好理解:一个类应当只承担一种职责。由于只承担一种职责,因此,一个类应该只有一个发生变化的缘由。 花园的景昕,公众号:景昕的花园[5+1]单一职责原则
什么是扩展?就Java而言,实现接口(implements SomeInterface)、继承父类(extends SuperClass),甚至重载方法(Overload),均可以称做是“扩展”。什么是修改?在Java中,严格来讲,凡是会致使一个类从新编译、生成不一样的class文件的操做,都是对这个类作的修改。实践中咱们会放宽一点,只有改变了业务逻辑的修改,才会纳入开闭原则所说的“修改”之中。 花园的景昕,公众号:景昕的花园[5+1]开闭原则(一)
里氏替换原则(Liskov Substitution principle)是一条针对对象继承关系提出的设计原则。它以芭芭拉·利斯科夫(Barbara Liskov)的名字命名。1987年,芭芭拉在一次名为“数据的抽象与层次”的演讲中首次提出这条原则;1994年,芭芭拉与另外一位女性计算机科学家周以真(Jeannette Marie Wing)合做发表论文,正式提出了这条面向对象设计原则
花园的景昕,公众号:景昕的花园[5+1]里氏替换原则(一)
通常咱们会说,接口隔离原则是指:把庞大而臃肿的接口拆分红更小、更具体的接口。不过,这并非接口隔离原则的定义。 实际上,接口隔离原则的定义实际上是这样的…… 客户端不该被迫依赖它们压根用不上的接口; 或者反过来讲,客户端应该只依赖它们要用的接口。
花园的景昕,公众号:景昕的花园[5+1]接口隔离原则(一)