设计与实现分离——面向接口编程(OO博客第三弹)
如果说继承是面向对象程序设计中承前启后的特质,那么接口就是海纳百川的体现了。它们都是对数据和行为的抽象,都是对性质和关系的概括。只不过前者是纵向角度,而后者是横向角度罢了。今天呢,我想从设计+语法角度说一说我感受到的面向接口编程,从而初探设计与实现分离的模式。
(本文所使用的面向对象语言为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博客第三弹)的更多相关文章
- 接着继续(OO博客第四弹)
.测试与JSF正确性论证 测试和JSF正确性论证是对一个程序进行检验的两种方式.测试是来的最直接的,输入合法的输入给出正确的提示,输入非法的输入给出错误信息反馈,直接就能很容易的了解程序的运行情况.但 ...
- OO博客总结——OO落下帷幕
OO博客总结--OO落下帷幕 凡此过往,皆为序章. 不知不觉OO课程即将落下帷幕,一路坎坎坷坷磕磕绊绊,可算是要结束了,心里终于松了一口气,也有小小的不甘和遗憾.凡此过往,皆为序章.特殊的线上OO课程 ...
- 手把手教从零开始在GitHub上使用Hexo搭建博客教程(三)-使用Travis自动部署Hexo(1)
前言 前面两篇文章介绍了在github上使用hexo搭建博客的基本环境和hexo相关参数设置等. 基于目前,博客基本上是可以完美运行了. 但是,有一点是不太好,就是源码同步问题,如果在不同的电脑上写文 ...
- Django搭建博客网站(三)
Django搭建博客网站(三) 第三篇主要记录view层的逻辑和template. Django搭建博客网站(一) Django搭建博客网站(二) 结构 网站结构决定我要实现什么view. 我主要要用 ...
- Django 系列博客(三)
Django 系列博客(三) 前言 本篇博客介绍 django 的前后端交互及如何处理 get 请求和 post 请求. get 请求 get请求是单纯的请求一个页面资源,一般不建议进行账号信息的传输 ...
- JavaScript 系列博客(三)
JavaScript 系列博客(三) 前言 本篇介绍 JavaScript 中的函数知识. 函数的三种声明方法 function 命令 可以类比为 python 中的 def 关键词. functio ...
- thinkphp5项目--个人博客(三)
thinkphp5项目--个人博客(三) 项目地址 fry404006308/personalBlog: personalBloghttps://github.com/fry404006308/per ...
- hexo搭建博客系列(三)美化主题
文章目录 其他搭建 1. 添加博客图标 2. 鼠标点击特效(二选一) 2.1 红心特效 2.2 爆炸烟花 3. 设置头像 4. 侧边栏社交小图标设置 5. 文章末尾的标签图标修改 6. 访问量统计 7 ...
- OO博客作业-《JML之卷》
OO第三单元小结 一.JML语言理论基础以及应用工具链情况梳理 一句话来说,JML就是用于对JAVA程序设计逻辑的预先约定的一种语言,以便正确严格高效地完成程序以及展开测试,这在不能容忍细微错误的工程 ...
随机推荐
- 进程调度之FCFS算法(先来先运行算法)
#include<stdio.h> #define PNUMBER 5//进程个数 #define SNUMBER 3//资源种类个数 //资源的种类,三种 char stype[SNUM ...
- Target Audiences在弹出的people picker中不显示Alias列有空的项目
[客户需求] 当编辑webpart时,Target Audiences在弹出的people picker搜索时候,Alias列有空的项目,客户要求不显示Alias列有空的项目. [分析] 首先这个“D ...
- Oracle ora-12514监听程序当前无法识别连接描述中请求的错误
昨天刚安装好oracle数据库,还可以登录,到今天,登录时就发出了这样的错误 到网上找了半天,上面都是说监听器服务的问题,但是试过后依旧不行.最后重启了一次,就解决了异常 原来是oracle中一个服务 ...
- 面向对象之this关键字
方法之间调用,可以通过方法名进行调用.但构造方法,无法通过构造方法名来相互调用. 构造方法之间的调用,可以通过this关键字来完成. l 构造方法调用格式: this(参数列表); l 构造方法的 ...
- Scala_类和对象
类是对象的抽象,而对象是类的具体实例.类是抽象的,不占用内存,而对象是具体的,占用存储空间. import scala.beans.BeanProperty class ChecksumAccumul ...
- 数据结构与算法之二叉树 ——in dart
用dart语言实现的二叉树,实现了插入.查找.删除,中序遍历.前序.后序遍历等功能. class BinaryTree<E extends Comparable> { Node<E& ...
- bzoj1043 [HAOI2008]下落的圆盘
Description 有n个圆盘从天而降,后面落下的可以盖住前面的.求最后形成的封闭区域的周长.看下面这副图, 所有的红色线条的总长度即为所求. Input 第一行为1个整数n,N<=1000 ...
- 【转载】ATL问题集
原文:http://blog.csdn.net/fengrx/article/details/4171629 这些问题是以前在csdn当版主是一些朋友整理的,今天找到了,贴到这里来! #1 如何使用控 ...
- Bash shell 常用快捷键
1,ctrl+c :强制终止当前命令 2,ctrl+l :清屏 3,ctrl+a :光标移动到行首 4,ctrl+e :光标移动到行尾 5,ctrl+u :从光标所在位置删除到行首 6 ...
- Field 'email' doesn't have a default value
MySQL在出现这个Field xxx doesn't have a default value错误的原因是:我们设置了该字段为非空,但是我们没有设置默认值造成的. 或着 缺少字段.