但假如被合并的代码所在的类具有某种并列关系,甚至是同一个父类下的多个子类,或者同一接口的多个实现类,则我们可以采用继承的方式解决代码复用的问题。
具体做法是这样的,第一步还是整理原有的代码,通过比较,将需要重构的多份原代码中相同的与不同的代码整理出来。在整理过程中,可以将不同的代码,保留在各自的原程序中,而将相同的代码抽取出来成为独立的函数。这些函数就是我们后面需要抽象、合并、复用的代码。
下一步呢,就是运用“抽取父类(Extract Superclass)”的重构手法,从多个要复用的类中抽取出一个共同的父类。父类中包含的方法应当是经过比较后相同的部分,而将不同的部分保留在原有的类中。
举一个例子吧:在一个开票业务中,开票被分为正常开票与非正常开票。不论是哪种开票,它们都需要保存,因此它们都有save()这个方法。但是,正常开票与非正常开票在保存时,有相同的操作,却也存在着差异。在我们重构前,“保存”操作在正常开票与非正常开票业务类中各自实现了一遍。显然,这个方法在两个类中存在着大量重复的代码。
随后我们开始整理与分析。我们发现在整个保存的过程中,保存前的校验是存在着差异的,但都需要校验;保存前的数据处理有相同的部分,也是不同的部分;最后执行保存,以及保存后的返回处理是相同的。经过这样的分析,我们分别将原有的正常开票业务类与非正常开票业务类中的保存操作分为了三段:valid(), prepare()与save()。
最后,我们运用“抽取父类”重构方法,抽取出“开票业务类”这个父类,将valid()设为抽象方法,将prepare()*同的代码放在父类中,将save()在父类中实现,将valid()和prepare()中不同的部分,分别在正常开票与非正常开票业务类中各自实现(如图):
这里“开票业务类”是个抽象类,不能被实例化。它的valid()方法是个抽象方法,什么都不写,让各个子类各自实现去。方法prepare()在父类中实现了公共的部分,但各个子类在实现其不同的部分时,应当调用父类,就像这样:
1 /*
2 * @param vo the value of Object
3 */
4 public void prepare(Fp vo) throws IOException {
5 super(vo); //调用父类中的公用代码
6 xxxxxxxx; //编写子类中各种不同的部分
7 }
最后,save()在两种开票业务中的代码完全相同,因此仅仅在父类中编写,子类不再各自实现。
这样设计带来的巨大好处是大大提高了程序的可维护性:如果代码共同的部分变更了,则去修改父类;如果代码各自的部分变更了,则去修改子类。如果子类中某些代码因需求的变更变为了共同的操作,则提升到父类中;相反,父类中某些共同的操作,因需求的变更而不相同时,则降级到子类中分别实现去。
除此之外,还有一种办法是将不同的部分用一个接口与其多个实现来解决。当实际的应用软件系统比较复杂时,使用继承比较容易出现“继承泛滥”的问题。因此,一个可行的办法是将继承转换为组合,具体方法是这样做的:
当我们完成了对代码的比较以后,将代码不同的部分封装到一个统一接口下的多个实现类中。然后将代码相同的部分合并成一个业务类,为各个客户程序所使用。举个例子吧,在ERP软件中有一个功能就是将各种不同的单据生成财务凭证。不同的单据生成的财务凭证是不一样的,如应付单应当将其对方科目作为凭证的借方科目,将应付科目作为贷方科目;付款单应当将其付款科目作为借方科目,将其结算方式对应的结算方式科目作为贷方科目……但不论那种单据,尽管科目与规则不同,但都是由一到多个借方分录和一到多个贷方分录组成。原程序是分别为各种不同的单据制作凭证生成类,如“应付单凭证生成类”、“付款单凭证生成类”等等,因此出现了大量重复代码。随后我们开始重构。通过分析发现,各种不同单据在整个过程中主要是生成分录的规则不同,同时合并分录的策略也不相同,但分录生成与合并分录之间是一种排列组合关系,即任何一种单据都可能有三种合并方式。不同属性的排列组合关系最容易造成“继承的泛滥”,如一个“应付单凭证生成类”,要分解成三个不同合并方式的类,“付款单凭证生成类”同样要分解为三个。除此之外,读取单据、保存凭证的过程则是相同的。因此我们进行了如下设计:
这样的设计,当来了一个业务,要求用多张应付单,按照凭证类型合并的方式生成凭证,则使用“应付单实现类”与“按凭证类型合并”;当另一个用付款单不合并生成凭证时,则使用“付款单实现类”与“不合并凭证”,功能得到实现。
采用该方法重构代码时,有效地解决了复杂环境下造成“继承泛滥”的问题,同时提高了系统可维护性。试想,如果要增加一种新的单据,我们则仅仅写一个它的“分录生成”实现类就可以了,其它什么都不用管,多么简便呀。原来的一大堆诸如“应付单凭证生成类”、“付款单凭证生成类”等等,现在一个简单的“凭证生成业务类”统统都搞定了,程序多么清晰呀。
最后,在复用代码的过程中,还有一种情况经常出现并且比较讨厌,那就是要重构的代码,被相同的部分与不同的部分分割成了好多的碎片。遇到这种情况,采用继承结合模板模式的方法,是最有效的了。模板模式(Template Method)是GoF设计模式中的一个,如果你为一个算法定义了一系列步骤,并且允许子类来实现其中的一个或多个步骤,你就可以使用这个模式。该方法将把分离得支离破碎的程序过程,分解成数个方法定义在模板模式的父类中(即每个方法就是一个步骤),并且在父类中定义了这些方法的执行顺序。之后,将代码中相同的部分写在父类中,将不同的部分分别在子类中实现。
比如,我经常要创建工厂类,虽然每个工厂类都各有各自的不同,但所有工厂类的初始化总是一样的:采用各种方式(相对路径、绝对路径、jar包或zip包中的路径,等等)寻找XML配置文件、读取XML文件、解析XML文件中的内容、根据内容创建产品、将产品放入到工厂中、为客户程序搜索工厂中的产品。这其中第1、2、5步对于任何工厂都是相同的,而第3、4、6步各个工厂却不尽相同。这时,创建一个AbstractFactoryTemplate的模板类,让各个工厂去继承它,则每个工厂类只需实现各自的第3、4、6步,工厂类就快速实现了(如图)。
提高代码复用率的方法林林总总、不胜枚举,并且每种方法都有各自的适用场景。因此,对开发人员提出了很高的技术要求。我们只有在实际工作中多思考、勤练习、仔细体会,才能熟练掌握各项技能,切实提高程序的代码质量。(续)
相关文档:
遗留系统:IT攻城狮永远的痛
需求变更是罪恶之源吗?
系统重构是个什么玩意儿
我们应当改变我们的设计习惯
小步快跑是这样玩的(上)
小步快跑是这样玩的(下)
代码复用应该这样做(1)
代码复用应该这样做(2)
代码复用应该这样做(3)
特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!