refactoring Patterns:第三部分 | ![]() | ![]() | ||
![]() | ||||
![]() |
![]() |
石一楹 (shiyiying@hotmail.com) 本文紧接 第二部分,继续讲述应用 refactoring 应该考虑的问题。
他把Refactoring的情景和面向对象出现使得情景相比较: 但是Martin Fowler和其他人确实观察到了Refactoring可能引发的某些问题,我们可以来看一下: 数据库 O/R mapping可以用来解决这个问题。使用专业的O/R mapping工具能够实现关系数据库的迁移。但是,就算这样,迁移也需要付出额外的代价。 如果你使用的并非关系数据库,而是直接采用OO数据库,这一点的影响可能会变得更小。 所以,我建议每一个使用数据库的应用程序都应该采用O/R mapping或者OO数据库。目前出现的各种企业级应用解决方案如J2EE本身就提供这样的构架。 如果你的代码没有这样一个隔离层,那么你必须手工或编写专用的代码来实现这些迁移功能。 接口改变和Published Interface 为了保证系统的可观察行为不变,你必须保证这些接口的改变不会影响到你无法取得 的代码。如果你拥有了所有使用该接口的类的源代码,你只要把这些地方同时也改变即可。 但是,如果你没有办法得到所有这些使用的代码,那么你就不得不采取额外的途径。事实上,如果你的代码是一个代码库(如Sun JDK的集合框架)或者是一个Framework,那么这一点几乎是不可避免的。 要使得这些依赖于你老接口的代码能够继续工作,你必须保留老接口。现在你有两套接口,一套是老的,一套是经过Refactoring的新接口。你必须把对老接口的调用分派到新接口。千万不要拷贝整个函数体,因为这会产生大量的重复代码。 这种方法虽然能够解决问题,但是却非常麻烦。由于Refactoring通常会涉及到状态、行为在不同类之间的转移,如果一个方法从一个类移动到另一个类,那么使用这种分派的方法可能需要一些不必要的中间状态或者参数。这会使你的代码显得难以理解和维护,在一定程度上削减了Refactoring所应起到的作用。 因此,这种方法只应该用于过渡时期。给用户一定的时间,允许用户代码能够逐渐转移到新接口,在超过一定的期限后,删除老方法,不再支持老接口。这也是Java Deprecated API的意义所在。 像这样保护接口虽然可能,却非常困难。你至少需要在一段时间内维护两套接口,以保证原来使用你老接口的客户代码还能继续使用你的新代码,Martin Fowler把这些接口称之为Published Interface。虽然你不可能避免公布你的一部分接口,不然谁也不能使用你的代码,但是过早公布不必要的接口会造成不必要的麻烦,就像Martin Fowler给我们的提示: 用Refactoring思想武装自己的设计 Refactoring包含两个方面的想法:它告诉你可以从简单的设计做起,因为即使代码已经实现,你还是可以用它来改进你的设计。然而,另一方面,它绝不是告诉你可以信手涂鸦。我给你的忠告是: 如果你一开始就设计了愚蠢的接口,甚至是错误的接口。在程序演变的过程中,这一部分可能变成系统的核心。对之进行Refactoring可能需要花费大量的精力,而改变接口和类的操作可能会是这些Refactoring主要内容。对核心类接口的变化可能会迅速波及到系统的各个层面,如果你的总体结构是好的,那么这种涟漪可能会在某一个层次消失。(譬如环状和层次性的体系结构。)如果你没有这样的抽象机制和保护体系,那么对核心类的修改将会直接导致整个系统的变更,这是不能接受的。 所以,在设计一个类的时候,你需要问自己几个问题,如果事情发生了这种变化,我会如何修改来适应?如果发生了那种变化,我会怎样来适应?如果你能够想到可能的Refactoring方法,那么证明你的设计是可行的。这并不意味着你要去实现这样的设计,而是保证自己的设计不会把自己逼入到死角。如果你发现自己的代码几乎没有办法Refactoring来适应新的需求,那么你要仔细考虑考虑别的思路。 每次公司的程序员问我一个设计是否合理,我总是反问几个问题:你如何适应这种变化,适应那种可能的变化。我同时指出现在没有必要去实现这些变化。我很少直接回答他好坏或者给他一个答案,但在思考了我反问他们的问题以后,程序员总能对自己的设计做出好的评判,从而找到很好的解决方案。所以,使用Refactoring的思想考虑你的设计。 编程语言 Refactoring最初的研究是从Smalltalk开始的.随着Refactoring在Smalltalk上的极端成功,更多的面向对象社团开始把Refactoring扩展到其他语言环境.但是不同语言的不同特点有时会对应用Refactoring提供便利,有时却会制造障碍. .静态类型检查和存取保护 和Smalltalk这样的动态类型语言不同,对静态类型进行检查的语言(C++,Java,Delphi等等)通常具有类继承和相关的存取保护(private,protected,public),这些特点使得寻找对某一个函数的引用变得相对简单.如果重命名的函数原先声明为private,那么对该函数的引用只能是在他所在的类或者该类的友类(C++)等等.如果声明为protected,那么只有本类,子类和友员类(同包类)才能引用到该成员函数.如果声明为public,那么还只需要在本类、子类、友类和明确引入该类的其他类即可(include,import)。 我想提起大家注意的另外一个问题。在软件的最初开发和整个开发流程中尽可能早地应用好的设计原则是一个软件项目成功的重要因素。不管是从封装的角度还是从Refactoring的角度来看,定义成员变量和成员函数应当从最高的保护级别开始。除了非常明显的例子之外,你最好首先把成员变量和函数定义为private。随着软件开发的进一步深入,当其他类对该类提出"额外"的请求,你慢慢地放宽保护。原则是:如果能够放在private,就不要放在protected,能够放在protected,就不要放在public。 预处理指令 依赖对象尺寸和实现格式的代码 使用C++的指针、cast操作和sizeof(Object)这些依赖对象尺寸和实现格式的代码很难refactor。指针和cast介入别名的概念,这使得你要查找所有对此Object有引用的代码变得非常困难。这些特征的一个共同特点就是它们暴露了对象的内部表达格式,从而违反了抽象的基本原则。 举个例子,C++使用V-table机制来表达可执行程序中的成员变量。继承得来的成员变量在前,本类定义的在后。一个我们经常使用,并且认为安全的refactoring是push up fields,也就是把子类中的一个成员变量移到父类。因为现在变量从父类继承而非本类定义,经过refactoring后的可执行程序之中变量的实际位置已经发生了变化。 如果程序中所有的变量引用都是通过类接口来存取的,那么这样的变化不会有问题。但是,如果变量是通过指针运算(譬如,一个程序员有一个指向对象的指针,知道变量在类的第9个字节,然后使用指针运算给第9个字节赋值),上面的refacoting过程就会改变程序的行为。类似情况,如果程序员使用if (sizeof(object)==15)这样的条件判断,refactoring的结果很可能会对该对象的大小产生影响,从而变得不再安全。 语言复杂度 解析引用的方式 反射、Meta级程序分析和变更 Java虽然还没有像CLOS这样强大的meta级功能,但是JDK的发展已经显示了Java在这方面非常强劲的实力。象上面的例子,我们也可以在Java上做到。 一个小结 从实践者的角度来看,目前最流行的refactoring文献基本上都采用Java语言作为范例,其中包括Martin的《Refactoring》。目前市场上有数种支持Java和Smalltalk的Refactoring工具,而C++的工具却几乎没有。这里面,语言本身的复杂性有很大的影响。 当然,这并不意味着C++程序员就不应该使用refactoring技术,只不过需要更多的努力。Refactoring技术已经证明自己是OO系统演化的最佳方法之一,不要放弃。
|