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

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

设计——接口抽象设计

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

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

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

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

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

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

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

版本一:

// 普通出租车
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四个环节的关系,当然一般为了简化仅使用前两个,但是我们考虑问题就应该多想一点,要做到正确条件一定导致正确结果,不正确条件一定导致不正确结果,要使整个规约定义是完备的,这样才能使设计毫无漏洞。

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

设计与实现分离——面向接口编程(OO博客第三弹)的更多相关文章

  1. 接着继续(OO博客第四弹)

    .测试与JSF正确性论证 测试和JSF正确性论证是对一个程序进行检验的两种方式.测试是来的最直接的,输入合法的输入给出正确的提示,输入非法的输入给出错误信息反馈,直接就能很容易的了解程序的运行情况.但 ...

  2. OO博客总结——OO落下帷幕

    OO博客总结--OO落下帷幕 凡此过往,皆为序章. 不知不觉OO课程即将落下帷幕,一路坎坎坷坷磕磕绊绊,可算是要结束了,心里终于松了一口气,也有小小的不甘和遗憾.凡此过往,皆为序章.特殊的线上OO课程 ...

  3. 手把手教从零开始在GitHub上使用Hexo搭建博客教程(三)-使用Travis自动部署Hexo(1)

    前言 前面两篇文章介绍了在github上使用hexo搭建博客的基本环境和hexo相关参数设置等. 基于目前,博客基本上是可以完美运行了. 但是,有一点是不太好,就是源码同步问题,如果在不同的电脑上写文 ...

  4. Django搭建博客网站(三)

    Django搭建博客网站(三) 第三篇主要记录view层的逻辑和template. Django搭建博客网站(一) Django搭建博客网站(二) 结构 网站结构决定我要实现什么view. 我主要要用 ...

  5. Django 系列博客(三)

    Django 系列博客(三) 前言 本篇博客介绍 django 的前后端交互及如何处理 get 请求和 post 请求. get 请求 get请求是单纯的请求一个页面资源,一般不建议进行账号信息的传输 ...

  6. JavaScript 系列博客(三)

    JavaScript 系列博客(三) 前言 本篇介绍 JavaScript 中的函数知识. 函数的三种声明方法 function 命令 可以类比为 python 中的 def 关键词. functio ...

  7. thinkphp5项目--个人博客(三)

    thinkphp5项目--个人博客(三) 项目地址 fry404006308/personalBlog: personalBloghttps://github.com/fry404006308/per ...

  8. hexo搭建博客系列(三)美化主题

    文章目录 其他搭建 1. 添加博客图标 2. 鼠标点击特效(二选一) 2.1 红心特效 2.2 爆炸烟花 3. 设置头像 4. 侧边栏社交小图标设置 5. 文章末尾的标签图标修改 6. 访问量统计 7 ...

  9. OO博客作业-《JML之卷》

    OO第三单元小结 一.JML语言理论基础以及应用工具链情况梳理 一句话来说,JML就是用于对JAVA程序设计逻辑的预先约定的一种语言,以便正确严格高效地完成程序以及展开测试,这在不能容忍细微错误的工程 ...

随机推荐

  1. USB协议规范文档简介

    USB协议规范文档简介       USB驱动开发必须对USB相关的协议规范有一定程度的了解,理解得越深,遇到的问题就会越少,解决问题的速度也就越快. 工欲善其行,必先利其器.USB协议规范就是USB ...

  2. 五种典型开发周期模型(瀑布、V、原型化、螺旋、迭代)

    五种典型开发周期模型(瀑布.V.原型化.螺旋.迭代) 总结一下经常可以见到的系统开发周期模型.    在过去的几年里,可以很奇葩的碰到类似于“创业项目库”这种需求非常明确,工作量十分可控,对质量要求比 ...

  3. SpringMVC的应用与工作流程解析

    一:SpringMVC是什么 SpringMVC只是Spring的一个子框架,作用学过Struts2的应该很好理解,他们都是MVC的框架.学他就是用来代替Struts2的,那么为什么不用Struts2 ...

  4. css学习之LInk & import

    一.用link加载外部样式表 1.放置位置:放在head元素中 2.样式表中只能包含样式规则,不能包含其他标记语言.如出现了标记,会导致其中一部分或全部被忽略. 3.type = 'text/css' ...

  5. Tarjan算法初探 (1):Tarjan如何求有向图的强连通分量

    在此大概讲一下初学Tarjan算法的领悟( QwQ) Tarjan算法 是图论的非常经典的算法 可以用来寻找有向图中的强连通分量 与此同时也可以通过寻找图中的强连通分量来进行缩点 首先给出强连通分量的 ...

  6. vue实现菜单切换

    vue实现菜单切换,点击菜单导航切换不同的内容以及为当前点击的选项添加样式,或者组件. method里: css: html代码: <nav> <ul> <li> ...

  7. PHP操作xml学习笔记之增删改查(1)—增加

    xml文件 <?xml version="1.0" encoding="utf-8"?><班级>    <学生>       ...

  8. 数据结构与算法之排序(4)希尔排序 ——in dart

    研究了网上大部分的希尔排序代码,发现大部分都是互相抄的——因为网上甚至某些书上的实现大部分都是错的.希尔排序是插入排序的升级版,通过引入间隔,然后分组进行插入排序.再逐步缩小间隔,直至间隔为1时,做全 ...

  9. 【ruby题目】以|为分割点,将arr转换为二维数组

    #以|为分割点,将arr转换为二维数组 arr = ['] tmp = [] tmp2 = [] for x in arr tmp << x if x != '|' tmp2.push A ...

  10. MongoDB操作-备份和恢复,导入和导出

    mongodb数据备份和恢复主要分为二种:一种是针对库的mongodump和mongorestore,一种是针对库中表的mongoexport和mongoimport 1. 常用命令格式 mongod ...