面向接口编程

其实我大二的时候开始,我就已经在不断地听说面向对象编程,这个词语不断地出现在我的眼前,并且在过去的几年里我也学习了 c# 这门同是面向对象的编程语言,可惜那个时候我还并没有将所有的心思放在对编程的研究中(惭愧)。这就导致我虽然身处在面向对象的世界中却不自知。知道升到本科,真正意识到自己要做一名软件开发人员,我才开始逐渐去了解面向对象编程这个思想。

在以主动的姿态踏入面向对象编程的世界中以后,我开始对这个世界有了极大的兴趣,它似乎是现实世界在在程序代码中的映射,所有现实世界的一些规则,基本都可以在面向对象的思想中找到同样的设计思想,这简直就太厉害了。过去为什么会觉得写代码是一件非常枯燥的事情,就是因为看不懂。可为什么看不懂呢,我觉得就是不熟悉是一方面,可最重要的是它(面向过程的语言)的语言结构让我们觉得非常困惑。绝大多数人的大脑思维都是基于已知的,被人们广泛知晓并且被认证过得事实来进行思考。而面向过程的语言虽然看起来样子很像是某些真实事物的反映,可仔细观察,它的(语言)行为模式和现实世界是大相径庭的,也就是只是形似而不神似。所以我们虽然看着都认识,却完全不理解它想要做什么,或者表达了什么。

到了面向对象编程的语言中,我们很容易就能看得懂这段代码的意图,甚至不怎么了解编程的人也能根据关键字和字面意思猜个七七八八,这不仅仅因为我们知道这些关键字的意思,同时可以体会到语言的意图,或者可以说,这些代码自己在说话,不仅仅是语法上跟接近真是语言,语义上也更加简洁易懂,语句之间的衔接关系也要更加紧密,真是如此,我们在编写面向对象的代码的时候,丝毫没有之前的迟滞感,因为写代码跟自己平常说话已经没有了太大区别,这个时候你还会觉得写代码很难么。

扯了这么多,我今天主要想说的还是面向接口编程这一块。这个词对于我来说还算是比较陌生的,也是以前隐约听别人提起过,但并不是太过在意。知道我这两天在学习一个课程的时候,听到了老师略微提了几次,并且有了一个相关的案例,然后我就被震撼到了。感觉自己好像发现了新大陆,从来都不知道之前被自己随随便便写的接口还有这种使用方式,不,这更是一种设计思想,真正的面向对象的设计思想。

没有依据的嘶吼是很苍白的,我简单说明一下这个案例:

这是一个功能模块,需要实现数据分页的功能,目前可供参考的方法有三种:使用 List 对象的 sublist 方法来来实现、使用数据库查询语句来实现,以及使用 hibernate 框架的分页功能来实现。按照以前的我,我会怎么做呢。

我会首先定义出数据库数据对应的实体类,也就是 MVC 架构的 M 层,然后对 M 层设计对应的 dao 接口,接着针对每个 M 来编写实现了 dao 接口的 Impl 类,然后在控制层调用 Impl 类来查询出每个分页需要的数据,最终就可以将数据显示到 View 层。

你觉得我这样做怎么样呢,当时的我觉得这就是最标准的实现思路了,甚至我以为这个老师也会用跟我类似的想法来实现,直到我看过了这个课程。如果你一开始的想法跟我一样,那么你也跟我一样,对面向对象编程的认识还太浅显,接着往下看吧。

在这个课程里,这位老师首先也是在 model 包中定义出了数据库要查询的数据实体类,例如要查询学生信息,那么就定义出学生的一个实例,用来映射数据的数据,这一点跟我之前的思路是一致的,但是接下来老师又定义了一个 Pager 类,用来存放查询出的分页数据,这一点我就没有想到。在我只是稍微有点惭愧的情况下,老师又开始打脸了。在定义了 Pager 类的基础上,老师又给这个类加上了泛型!天哪原谅我还没接触过泛型,对这个举动直接就是一脸懵逼的。好在也是基于面向对象的思想,还是比较好懂的,老师随便一讲解就明白其中的用法。在这个 Pager 类中,包含一个 List,totalPage,pageSize,currentPage,totalRecord,就已经将所有分页可能用到的信息定义了出来。这个时候我就已经开始佩服这位老师,不管后面是如何实现的,这里泛型的运用就不是我能比的。紧接着老师又在这个包内定义了 StudentDao 接口,其中就只有查询学生分页信息的这么一个方法:

1
public Pager<Student> findStudent(Student searchModel, int pageNum, int pageSize);

我想大概是因为这里已经是涉及到具体的查询学生信息的方法的,所以 Pager 的泛型已经具体到了 Student 这个实体类。有了这个接口,就可以用不同的方式实现分页信息的查询。其实排除这里泛型的运用,以及查询的具体实现,我们思路也是差不多的嘛(勿喷。。)。然而接下来老师的行为就让我彻底不解了。老师在这里又在 Service 包中定义了一个接口 StudentService ,并且在其中定义了一个与之前 StudentDao 接口完全相同的方法 findStudent 。不是已经定义了一个查询学生信息的接口么,接下来只要再写一个实现 StudentDao 接口的类来实现具体的查询逻辑不就行了么。然后老师的行为有恢复了我认知上的正常,定义了使用 sublist 方法并且实现了 StudentDao 接口的类 SublistStudentDaoImpl ,在这个类中实现了用 List 的 sublist 方法进行分页信息的查询;具体的代码实现我就不提了,并不是要研究的重点,不过有一点我想要说明一下。老师给 Student 这个实体类又添加了一个构造方法

1
2
3
4
5
6
7
public Student(Map<String, Object> map) {
this.id =(Integer) map.get("id");
this.stuName =(String) map.get("stu_name");
this.age =(Integer) map.get("age");
this.gender =(Integer) map.get("gender");
this.address =(String) map.get("address");
}

通过传入一个 map 来构造一个 Student 实例,map 中存放的是从数据库里查询出来的字段名与字段值,之所以这么设计是因为数据库层返回的就是 List<Map<K,V>> 类型。这样就能极大程度的简化 Student 构造的过程,并且代码看起来也会很清晰。

在这之后,老师又在 Service 包中新建了一个实现了 StudentService 接口的类 SublistStudentServiceImpl ,在这个类所做的事情就让我稍微明白了为何要再多加一个 StudentService 接口的原因。这个类里定义了一个 StudentDao 接口,又在这个类的构造方法中实例化了一个 SublistStudentDaoImpl 类,是的,实例化了一个 SublistStudentDaoImpl 类,因为这个类实现了 StudentDao 接口,所以完全是可以以 StudentDao 接口来实例化的。这种方式也是我之前不曾使用过的,果然对于面向对象思想我还有很长的路要走。由于这个类实现了 StudentService 接口,就可以在 findStudent 方法中,调用被实例化 的 StudentDao 接口的 findStudent 方法,实际上就是 SublistStudentDaoImpl 类中的 findStudent 方法。

在往下就是在控制层拿到数据再设置到视图层了。经过上面这么一折腾,我很自然地猜到,应该会在视图层中定义一个 StudentService 接口,然后借此实例化一个 SublistStudentServiceImpl 类,通过这个类就能切实的拿到查询出来的数据集。果然老师也是这么做的,从方法定义来看,最终拿到的是一个 Pager 对象,这里的控制层就是 Servlet ,所以将这个 Pager 对象放入到 request 中即可。如此第一种方式实现分页查询就完全搞定了。

到这里我还是不能完全理解老师的想法,因为两个接口的功能确实是重复了的,直接在控制层通过 StudentDao 接口实例化 SublistStudentDaoImpl 类的结果完全是一致的,还省去了中间的数据流动。

其实这里就涉及到更深的分层思想和 solid 面向对象五大原则了。因为我这里的业务场景非常简单,就算是 findStudent 这个方法,也只是根据 student 的 name 和 gender 两个属性来查询,查询的也只是学生的实体信息,并没有涉及到复杂查询,比如多表联合查询等。如果没有 Service 这一层作为中间的过渡,那么当你想要增加其他查询功能的时候,就会非常麻烦。因为我这里的 Service 中的逻辑非常简单,就只是返回查询结果这一行代码,所以看起来 Service 的存在并没有什么意义,好像在什么地方调用 studentDao 都是可以的。然而在正式的生产场景中是不太可能会一行代码就搞定的,那么针对未来的功能模块的添加,需要预留足够良好的层次设计。

假如我现在有很多查询方法,每个方法的底层实现都不相同,那么就可以让新的功能类区实现 studentDao 接口,再新建合适的类去实现 StudentService 接口,在其中通过 studentDao 接口去引用前面的实现类。这样的好处就是可以实现模块分离,或者说可以 解耦。如果从宏观来看,不设置 Service 层,没有 Service 接口来做中专,那么控制层就会直接接触到数据库层,但是按照 solid 中的 d(DIP,依赖注入或倒置) 原则,高层模块不应依赖于低层模块,二者都应该依赖于抽象 ,回想一下,dao 包和 service 包里都定义了接口,而其对应的实现类也都在这个包内,包内的接口就是这个包中实现类的抽象,而其上层也正是通过 这两个层的接口来调用具体的逻辑,这就是高层逻辑与底层逻辑之间没有直接的耦合关系,而是通过抽象的接口来调用,并且这两个实现类的核心逻辑都是根据接口的定义来完成的,也就是实现依赖于抽象,最后抽象和接口就使模块之间的依赖分离。solid原则可以参考![1]。

不知不觉就写了这么多,却感觉还是没有完全能说清楚,除了 solid 五原则,面向对象还有其他的原则,但这些原则不是一朝一夕就可以理解透彻的,随着编码经验的增加和知识的积累,慢慢地领悟这些原则,并逐渐将其运用到实际的代码中,你的代码质量也就会越来越高。

参考solid 原则