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

(本文所使用的面向对象语言为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. 进程调度之FCFS算法(先来先运行算法)

    #include<stdio.h> #define PNUMBER 5//进程个数 #define SNUMBER 3//资源种类个数 //资源的种类,三种 char stype[SNUM ...

  2. Target Audiences在弹出的people picker中不显示Alias列有空的项目

    [客户需求] 当编辑webpart时,Target Audiences在弹出的people picker搜索时候,Alias列有空的项目,客户要求不显示Alias列有空的项目. [分析] 首先这个“D ...

  3. Oracle ora-12514监听程序当前无法识别连接描述中请求的错误

    昨天刚安装好oracle数据库,还可以登录,到今天,登录时就发出了这样的错误 到网上找了半天,上面都是说监听器服务的问题,但是试过后依旧不行.最后重启了一次,就解决了异常 原来是oracle中一个服务 ...

  4. 面向对象之this关键字

    方法之间调用,可以通过方法名进行调用.但构造方法,无法通过构造方法名来相互调用. 构造方法之间的调用,可以通过this关键字来完成. l  构造方法调用格式: this(参数列表); l  构造方法的 ...

  5. Scala_类和对象

    类是对象的抽象,而对象是类的具体实例.类是抽象的,不占用内存,而对象是具体的,占用存储空间. import scala.beans.BeanProperty class ChecksumAccumul ...

  6. 数据结构与算法之二叉树 ——in dart

    用dart语言实现的二叉树,实现了插入.查找.删除,中序遍历.前序.后序遍历等功能. class BinaryTree<E extends Comparable> { Node<E& ...

  7. bzoj1043 [HAOI2008]下落的圆盘

    Description 有n个圆盘从天而降,后面落下的可以盖住前面的.求最后形成的封闭区域的周长.看下面这副图, 所有的红色线条的总长度即为所求. Input 第一行为1个整数n,N<=1000 ...

  8. 【转载】ATL问题集

    原文:http://blog.csdn.net/fengrx/article/details/4171629 这些问题是以前在csdn当版主是一些朋友整理的,今天找到了,贴到这里来! #1 如何使用控 ...

  9. Bash shell 常用快捷键

    1,ctrl+c  :强制终止当前命令 2,ctrl+l   :清屏 3,ctrl+a  :光标移动到行首 4,ctrl+e   :光标移动到行尾 5,ctrl+u   :从光标所在位置删除到行首 6 ...

  10. Field 'email' doesn't have a default value

    MySQL在出现这个Field xxx doesn't have a default value错误的原因是:我们设置了该字段为非空,但是我们没有设置默认值造成的. 或着 缺少字段.