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

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

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

设计——接口抽象设计

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

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

该项目是模拟出租车运行,地图为 80	imes 80 的正方形网格图,每个点的四个邻接点不一定都连通,但保证整个图是连通的,共有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) ==> 
esult = -1;
   * (response.size > 0) ==> (( esult = 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: 
esult = 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: ( esult == true) ==> trace.contain(src,now,dst) && trace.runDirection obey traffic.state;
* ( esult == 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四个环节的关系,当然一般为了简化仅使用前两个,但是我们考虑问题就应该多想一点,要做到正确条件一定导致正确结果,不正确条件一定导致不正确结果,要使整个规约定义是完备的,这样才能使设计毫无漏洞。

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

原文地址:https://www.cnblogs.com/swainz/p/9091255.html