设计与实现分离——面向接口编程(OO博客第三弹)

       若是说继承是面向对象程序设计中承前启后的特质,那么接口就是海纳百川的体现了。它们都是对数据和行为的抽象,都是对性质和关系的归纳。只不过前者是纵向角度,然后者是横向角度罢了。今天呢,我想从设计+语法角度说一说我感觉到的面向接口编程,从而初探设计与实现分离的模式。java

(本文所使用的面向对象语言为java,相关代码都是java代码)算法

设计——接口抽象设计

      继承的思想很容易理解,提取几类相近数据中的公共部分为基类,各个独立部分在基类的基础上作本身专属的延伸。接口是抽象归纳输入和输出,而具体的实现交由具体实现接口的类来完成,从而达到同样的接口不同的实现方式,使得管理统一化,实现多样化。编程

      概念扯了那么多,仍是先上个例子吧,以课程中的出租车调度项目为例。数组

该项目是模拟出租车运行,地图为 80\times 80 的正方形网格图,每一个点的四个邻接点不必定都连通,但保证整个图是连通的,共有100辆出租车运行。
任意两个结点之间有道路或者无道路。
出租车未接单时为随机游走,即随机向可行方向之一运动一步。接单以后选择最短路径运行。

       看到这个版本一的需求,我当时的第一想法是什么呢?出租车的行为可归纳成两种模式,随机游走和最短距离寻路,这两种行为都是要基于图数据的,那么就开个邻接矩阵存储图,连通为1不连通为0,而后去作相应的实现便可。这样听起来彷佛没什么问题,彻底是基本操做嘛。可是,看到我说版本一,相信聪明的人必定猜到还有后续的版本。是的,变化的需求是程序设计者最大的敌人。版本二的需求改动以下:安全

新增道路打开关闭功能,连通的路能够被关闭,关闭以后也能够选择再次打开,道路的状态变成了三种,普通的出租车没法经过关闭后的道路。新增VIP出租车,VIP出租车能够经过被关闭的道路。

       关闭道路?嗯…面对这样的需求改动,以大一时的蠢习惯,那就开个flag数组,对于全部的连通边初始化为1,关闭道路就把对应的flag置为0,每次访问图的同时访问flag数组,想法是很美好的,但若是需求又变了呢,道路的状态再次增长了呢,总不可能继续开更多的flag吧。因此,应该先定义好各类状态对应的值,经过一个邻接矩阵来存储对应的状态值,使用一种数据结构来管理。为简化说明咱们就设置关闭道路代号为2。网络

       数据存储解决以后,就要作相应的逻辑处理了,两种出租车,对于图中的道路有不一样的访问权限,那是否是应该每一个出租车写一个最短路径搜索呢?又或者是给最短路搜索方法新传入一个出租车类型参数,根据类型参数的不一样选择不一样的分支去执行。这个时候,就轮到接口出场了。咱们来细细梳理逻辑,两种出租车都是要搜索最短路径,所使用的算法是相同的,惟一的不一样点在于两种出租车对于“连通”的判断逻辑不一样,其余的代码部分应该都是可复用的。被C语言腐蚀的我第一时间想到了什么——函数指针,若是是使用C语言的话,咱们须要为两种出租车定义各自的连通性判断函数,而后经过一个函数指针传入最短路径搜索函数(相似stdlib.h中的qsort函数同样)。那么在java中有殊途同归之妙的就是使用接口来实现了,这正好符合面向接口编程的目的——实现不一样,接口内容相同。因此咱们应该对于每种类型的出租车实现专属的连通性判断接口,在任何须要访问图的时候传入该接口便可。下面附上代码:数据结构

版本一:ide

// 普通出租车 if(inRange(u)&&graph[v][u]==1){ do something } // VIP出租车 if(inRange(u)&&graph[v][u]==1||graph[v][u]==2){ do something } 

版本二:函数

if(inRange(u)&&inter.isConnected(v,u)){ do something } 

       试想你的代码中有多处须要判断连通性,你是选择一处一处写“graph[v][u]==XXX”,仍是选择使用接口来管理呢?全部须要使用的地方使用同样的模式,代码可读性高,复用性好。需求改变修改代码时仅需修改或新增接口实现便可,不用在文件中各处修补,维护起来也方便。一样将具体的实现逻辑做为保存在类中,外部只能调用没法修改,提升了安全性。学习

语法——动态接口

       听到这里确定有人会想:明白了明白了赶忙代码走起。不过先别急,在最基本的接口实现语法以外,还有一种更加高级的写法——动态接口。

  基本的接口实现是在类中实现重写接口的具体实现,而后将其做为该类的实例化对象的方法使用,说到这里聪明的你必定发现了:这样的作法传参数的时候仍是必须将对象传进去,咱们的目的是仅仅使用这一个方法,可是却不得不将整个对象传进去,这又扩大了对象的共享范围,难道就不能像C语言同样只是传个方法进去吗?答案是确定的,那就是动态接口。具体的代码以下:

// 接口定义 public interface TaxiInterface { boolean isConnected(int x,int y); } // 接口在类中的实现 public TaxiInterface setTaxiInterface(){ return new TaxiInterface() { @Override public boolean isConnected(int x, int y) { int temp; temp=map.getGraphInfo(x,y); return temp==MapHelper.getOpen()||temp==MapHelper.getSamePoint(); } }; } 

  什么?在方法里重写方法。是的你没有看错,随时随处重写,哪里有需求,哪里就有接口的实现,很是的灵活。语法提炼一下,就是在新建接口对象的时候重写其实现内容。对于咱们的问题,咱们对于每一个出租车类定义一个接口类型成员变量,而后经过set方法定义具体内容。在传递的时候使用相应的get方法,只是将此接口变量传递出去。外部的方法只能使用接口中定义的内容,关于该类的其余全部内容都无权访问。这种写法既方便快捷,又保证了数据的隐私性和安全性。不过提醒一点,在没有熟练掌握前不要乱用哦。  

语法——default和static接口方法

  如今咱们跳跃到下一个问题。假如说如今你有成吨的类,都要实现某一个接口,而其中不少类对于接口中某个方法的实现是相同的,仅有少数不一样。可是要修改的类太多了,按照传统的路子,你得实现一个,而后不停的人肉ctrl+c,这种事光是想一下就以为痛苦,程序猿明明是最擅长偷懒的人啊!不要担忧,在Java 8 以后,接口拥有了default和static方法,拯救了这个问题。

  咱们都知道接口中定义的抽象方法都是自带public abstract属性的,可是在方法声明最前面加上default关键字,就能够在接口中完成此方法的缺省实现,其余实现该接口的类均可以通用该方法,有特殊需求类的单独重写就能够,调用时直接经过方法名调用便可。举个例子,Iterable.java源码中的forEach遍历方法就是这样实现的,提供了一个通用的迭代方法。

default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } 

  P.S. 有时间能够多读读相关类库源码。我读了部分TensorFlow源码和java类库源码发现本身相关能力都有很大提升。

  话说回来,那static又能干什么呢,这个就很相似类中的static修饰的方法,即不须要实现接口(implement XXX),使用接口名.方法名便可调用。

  注意:一个接口中能够有多个default和static修饰的方法,可是一旦使用这两个关键字该方法就必须实现。

设计——传入对象 or 传入接口

  在初学OOP的时候,很使人苦恼的一点就是对象的传递,每一个类负责本身的数据,各个类实例化的对象之间又要共享数据传递信息,可是将整个对象传来传去的话又会形成数据隐私的暴露,说不定还会产生奇奇怪怪的错误,很难追溯缘由。那么借由以前使用接口传递连通性判断方法的思路,咱们能不能变传入对象为传入接口呢?

  传入对象,就可使用对象全部public的数据和方法(一个package的话固然default也能够,不过一个package这么反工程的事情可干不得)。既然有可使用的可能性那么就有了各类错误和安全问题的可能性,设计的初衷是交给它几个方法的使用权,实际上却搞成了一键root?可能有人会想开发时保证不乱调用方法便可,可是潜在的危险始终存在,咱们最好仍是将全部问题扼杀在摇篮里。

  若是咱们对于每一个类想传递的方法(信息交流内容)定义专门的接口,将接口做为参数传递进去,则就是另外一番景象。因为接口对象只能使用接口中定义的方法,至关于咱们已经定义好了条条框框,接收者只能使用规定的内容,配合每一个方法中的规约定义和异常检测,这样就将危险的可能性降到了零。同时,将一个接口做为类之间的交流通道,信息传递必须按照接口定义的规则来,这是否是一瞬间感受有点像操做系统中的系统调用syscall或是网络中的通讯协议?这一点很好的符合了“封闭-开放原则”,即对修改封闭,对扩展开放。任何类没法修改传递信息的方式,而每一个类自身能够任意的进行扩展,只要不影响传递信息的相关方法想怎么扩展怎么扩展,两边互不关心对方的发展,只要知足传递信息接口的要求便可。

  面向接口编程说究竟是将设计和实现分离,这是其核心。同时,这里的“接口”并非单单指java中的interface或是其余语言的相似语法,这是一种思想,先规约设计,再具体实现。

设计规约(JSF)

  以前的三次做业我并无出现JSF问题,多是因为主要是使用天然语言书写表意比较完整,那么对于一样的内容,如何使用逻辑语言达到完备的表达效果同时又十分简洁呢,我以为一个办法是经过阅读好的写法来学习,下面上几个例子:

1.

    private synchronized int selectTaxi(){ /** * @REQUIRES: None * @MODIFIES: None * @EFFECTS: \exist taxi in response;taxi has the highest credit;select taxi; * if taxi.num>1;select the shortest current distance to passenger one; * if not \exist taxi in response, return -1; * @THREAD_EFFECTS: \locked() */ }

  该方法是从response队列中选择出信用最高的出租车,若是有多辆车信用相同选择到乘客距离最近的一辆,返回其对应的索引值,若是队列为空返回-1.(其实应该抛出异常更好,这是出租车代码中最古老的部分了还没来得及重构)。能够看到我以前的写法主要使用了天然语言辅以部分逻辑语言,那么改进版以下:

    private synchronized int selectTaxi(){ /** * @REQUIRES: None * @MODIFIES: None * @EFFECTS: (response.size == 0) ==> \result = -1;
   * (response.size > 0) ==> ((\result = index) ==>
    *       (selected_taxi.index == index) && (\all taxi response.contain(taxi);taxi.credit <= selected_taxi.credit;) &&
    *       (\all taxi taxi.credit == selected_taxi.credit; taxi.distance >= selected_taxi.distance;))
* @THREAD_EFFECTS: \locked()
*/ }

2.

   public boolean runPermission(Point src, Point now, Point dst){ /** * @REQUIRES: src.inRange && now.inRange && dst.inRange && src is neighbour of now && now is neighbour of dst; * @MODIFIES: None; * @EFFECTS: \result = whether the current light state permits taxi passing through; */ }

  该方法的做用是在路口判断是否能够直接通行或是等待红绿灯,初始版是标准的“白话文”,那么改进版以下:

   public boolean runPermission(Point src, Point now, Point dst){ /** * @REQUIRES: traffic.state in {0,1,2} && graph.contain(src) && graph.contain(now) && graph.contain(dst) && traffic.locate == now
* \exist edge in edges;edge.begin == src && edge.end == now &&
* \exist edge in edges;edge.begin == now && edge.end == dst; * @MODIFIES: None; * @EFFECTS: (\result == true) ==> trace.contain(src,now,dst) && trace.runDirection obey traffic.state;
* (\result == false) ==> trace.contain(src,now,dst) && trace.runDirection disobey traffic.state;
*/ }

  首先,对于逻辑语言JSF的书写,不要从主观角度去描述行为,谁作了什么谁拥有什么,而是要从客观出发,描述客观对象的性质和状态,相似于数学定义的方法,状态A就能对应到反馈A1,状态B就能对应到反馈B1。在书写格式角度正确以后,则应该着重注意逻辑的严密性,单单的A==>B是很弱的,这仅仅描述了事物的一部分。完整来看,应该是A==>B,B==>A,!A==>!B,!B==>!A四个环节的关系,固然通常为了简化仅使用前两个,可是咱们考虑问题就应该多想一点,要作到正确条件必定致使正确结果,不正确条件必定致使不正确结果,要使整个规约定义是完备的,这样才能使设计毫无漏洞。

  规约定义配合以前说的面向接口思想,将设计和实现分离开来,用接口来设计功能,用规约定义来规范每一个接口和方法的内容,保证每次运行使用给定的正确的方法,每一个方法的执行符合规格定义的内容,对于符合前置条件的输入进行对应的后置条件处理,对不符合的作相应的异常检查和处理。当作完这些设计工做,完成了规约层的事,这时候再开始实现层的工做就会事半功倍!这样,才叫程序设计。

相关文章
相关标签/搜索