这篇文字是在重构一个系统的过程中对于所发现的代码设计上的问题的小结,以及从这些问题中引申出的个人对编码规范和代码设计的感悟。
一切要从下面这些接口说起。










以上只列举了部分接口,这些接口负责不同类型单据的审批。可以看到,这些接口中都有撤回,批准,拒绝,退回,添加审批人和转交他人几个方法,个别的接口因为业务差异定义了自己特有的方法,并且以上接口的实现类中的代码也是大同小异。
很明显,上图中的那些接口其实只需定义一个“审批”接口便可被全部代替,然后为各具体类型的单据创建实现“审批”接口的类,各实现类中的相同代码还可提取到接口中。
面向接口编程的理念是要基于协议或约定来编程,这里的“接口”指的是一组协议或约定,从代码的层面出发,接口就是能力的抽象,其目的是增强程序的灵活性。但这个本意很好的编程理念也不知是被哪个大明白理解成了现在的样子,并在开发规范中规定:每个类必须对应一个接口,殊不知自己这个大明白其实是一直被禁锢在一种特定的开发语言之中,而根本不去了解Java interface背后的本质意义。
不过话说回来,上图中那些多余的接口也不能全怪后来者,因为从工作那天起,我们就被要求————凡是业务类就必须要将其方法抽到一个与其同名的接口中,美其名曰编码规范。因此,上述代码其实都是在这种思维定势下写出的。当一个接口只对应一个类的方式成为了一种习惯,那么在遇到上面这种,一个接口确实需要多个实现类的情况时,反而不会写代码了,忘记了封装,忘记了抽象,忘记了多态,只记得死板的教条和僵化的执行以及Ctrl + C / V,这难道不是编码规范对于Java interface滥用的悲哀吗!
如果说上面的代码是一种悲哀,那么下面的设计则是另一种极端,设计者倒是思考了,但却没选对方向。看下图中的接口。

从接口名可以看出,这是给上文提到的那些接口中的每一个方法都建立了一个接口。怎么说呢,我倒是能理解这么设计的初衷,他一定是想起了初学Java时那些小动物的例子。孙敬修爷爷娓娓道来。

小盆友,鸟啊就是一个对象,那鸟会干什么呢?
飞!
真是太聪明啦,对,飞。那么鸟这个对象中就会有一个名为fly的方法。 那再想想还有什么会飞呢?
塑料袋!(啪一个大嘴巴)
蝙蝠!
好宝贝儿,真聪明。对,蝙蝠也会飞。那这时候我们就可以定义一个名为Flyable的接口,然后在接口中定义一个名为fly的方法,我们让鸟和蝙蝠都实现这个Flyable接口,那么它们就都可以飞啦!
鸟和蝙蝠不是一类动物,它们为什么可以实现相同的接口呢?
傻孩子,接口代表着一种能力,如果实现了一个接口,也就说明这个类具备了相应的能力。
显然,上图中接口的设计者是将前文提到的那些审批流程所用到的方法都视为了一种能力。我只想捂脸哭笑的说一句:好容易有点勤劳和智慧还用错了地方。但凡这个工程中有第二处用到了那堆接口都可以说这是一个成功的设计。但,这个真没有。
在从这个工程中暂时找出上面这两个与本文题目有关,且看着十分不爽的代码设计吐槽一番后,接下来就得想个可以优雅的解决上述问题的办法,毕竟吐槽并不是目的。
首先,重新定义一个接口——BillOperationService,可将create(),save(),commit(),以及approve(),reject(),sendBack(),addApprover(),transferTo()这几个与单据审批有关的方法全部放入该接口中,因为这些方法是所有类型单据所共有的操作。除以上方法外,还须再定义一个getBillTypeCode(),用于返回各类型单据的类型编码。剩下那些各类型单据所特有的方法则可视情况而定了,比如下图所示的download(),这些download()内部的代码几乎完全相同,显然这应该是一种能力,所以可以为这个能力创建一个Downloadable接口,并提供默认的实现代码,如此download()的调用者就无需依赖不必要的接口,从而也符合了ISP。

其次,让本文开始所列举的那些接口的实现类实现BillOperationService接口,各实现类通过getBillTypeCode()返回其对应的单据类型编码。
最后,通过Spring将BillOperationService的各实现类注入到Map中(Map<String, BillOperationService>),可以用BillOperationServiceFactory来持有这个map,以如下方式调用。
提供getInstance()的重载方法
^
|
BillOperationServiceFactory.getInstance(Long billId / Bill bill).approve()/reject()/...;
常说代码设计,到底设计了代码的什么?
举例来说,请实现返回一个目录下所有文件的功能。对于这个看似简单的功能,有多少人设计,就会有多少种实现方法,你用new File().listFiles(),我用Files.walk(),这算是设计吗?就这?仅就这个简单的功能而言,设计是体现在出现异常情况时该如何告知调用者,是抛异常,还是返回错误码;如果目录下的文件有几万甚至更多,该怎么返回给调用者,是一次性返回,还是加个分页的参数;如果要求返回结果有序,是选择快排还是归并,等等。或者,将上文中介绍的审批流程进行合理抽象后重用。这才算是代码设计和OO应有的样子。
从开始学Java那天起就知道面向对象。那为什么要面向对象,最大的好处当然是重用,可并不是说采用OOP后系统中就不存在过程形式代码,反而过程形式的代码才是一切的基础。因为,即使将一个方法封装进一个对象,该方法内的代码也是过程形式的,也就是先干什么,后干什么。多年养成的开发习惯已经让很多Javaer一出手就是过程形式的代码,而很少考虑将一个过程形式的代码封装到一个命名合适的对象中以便重用(这绝不仅仅是写个util那么简单的问题)。
本文中的反例,一个是思维被编码规范所桎梏且怠于思考,一个是思考后在错误的方向上瞎使劲。如果当初有老鸟review这些代码,很可能就没有我今天的这篇文字。
把最基本的设计先做好,定下一个好的基调,后面的事才会水到渠成。
本文算是《该如何命名代码包以及划分代码包的结构》的姊妹篇,这两篇文字中的代码均来自于同一个系统。今后肯定还会遇到糟糕的设计,估计能写出一个代码设计吐槽系列来。


被折叠的 条评论
为什么被折叠?



