Prefactoring——Guidelines
Prefactoring——Introduction
虽然Prefactoring这个概念并无多少新意(个人看法),但Ken Pugh在书中通过一个项目的开发过程来讲述了很多Guidelines, 对于那些有2年以上开发经验的人来说,可以把自己的开发经验与其做一个印证,也能拿出一些开发中感到困惑的问题来到书中寻找答案。
我花了两天时间看这本书,列出这本书的精华——Guidelines,这些指导原则主要包括basic design principles、Extreme Abstraction、Extreme Separation和Extreme Readability。我把guidelines和自己的理解记录下来与各位一起探讨。有些Guidelines是采用小故事的形式来说明的,为了直观,我直接将其隐喻含义列出来。
大多数指导原则围绕的是Abstraction、Separation of Concerns和Readability三个基本概念。
抽象是面向对象系统中的关键原则,关注的是“What”——需要做什么,而不是“How”——实现的细节。抽象过程中使用的利器就是(接口)Interface。
SoC,分离关注点——AOP中常常会提到的概念(其实在AOP提出前就有SoC了),在不同的classes、methods和variables中分离职责,而只担负单一的职责。应用到实践就是每个类只代表一个简单的事物,每个方法只做一件事情,每个变量只代表唯一的意义。
代码不仅是要在计算机上执行的,还要面对其读者,所以代码应该具有良好的可读性,任何开发者甚至客户都能读懂代码。
各个指导原则贯穿于开发流程中的各个阶段,每个原则并非绝对的,在运用中要看具体的环境——开发中充满了折中和妥协的艺术,特别是做设计的时候。对于Principle和Guidelines我使用“【Guideline】”来表示,使用“【IMO】”来表达我的看法。
【Guideline 1】对系统做的任何决定都应符合Big Picture。Big Picture是开发中系统的整体视图(broad perspective),包括系统的整体架构和商业目标。
【IMO】商业目标决定系统的范围,而架构则决定系统的实现。只要这两者确定下来,那后续的工作都不能背离它们。Big Picture就像旅行者的地图,让他清楚地知道自己在什么位置,明天该走到哪里。Big Picture的表达方式不拘泥于某种形式(Block Diagram、UML……),关键在于其表达的内容。当然,如果架构存在缺陷,甚至商业目标根本是错误的,那后续的工作可能都是毫无意义的。 这里有个Big Picture的例子。
【Guideline 2】针对良好定义的接口做设计,并且遵照接口的契约。
【IMO】面向接口而不是面向实现,这是任何良好设计的基础。
【Guideline 3】对于每个接口,验证输入是否符合契约。
【IMO】在我们把输入传入系统内部处理之前,我们应该验证输入数据是否合乎规范。其实这也是编写安全代码的基本要求。
【Guideline 4】代码应能明确传达其目标和意图。
【IMO】就是说应该具有良好的可读性。增强代码的可读性主要是代码规范和风格的问题。衡量的标准就是其他人是否能读懂代码,如果普通客户都能读懂的话,那代码的可读性肯定是没有问题了。
【Guideline 5】保持清晰,减少二义性。
【IMO】我们也不可能保证任何东西都非常的清晰易懂,对于那些领域相关代码,只有具备相应知识的人才能看懂。但我们应该尽量做到EXPLICITNESS。
【Guideline 6】一致性就是一种简单。具有一致性的风格和解决方案会使得代码维护变得简单。
【IMO】非常正确。所以我们致力于使用代码规范来统一代码风格,使用界面规范来统一界面风格,对于同类的事情使用相同的方法处理。而用户也习惯于使用风格一致的操作界面,对于已经使用很久的系统如果修改后用户接口大变,那用户一定会抱怨。
【Guideline 7】适应预构的态度。在重复的东西出现前,去除它。
【IMO】这似乎有一点矛盾。我在写代码的时候如何知道这段代码将会是重复的呢?这大概需要根据经验而定了。所以说“预构”是属于老手的东西,而非新手。比如老手会知道使用模板(template)来复用某个算法。
【Guideline 8】Don't Repeat Yourself。不要重复自己。任何知识在系统中都必须有个唯一、明确和权威的表述。
【IMO】信息只有一个权威的来源(source),如果需要多种形式的话,可以将这个唯一source转换为其它形式,如果需要改变该信息,那只能在一个地方去做改变。具体的表现就是数据一致性。一份数据来源于唯一的地方,修改它也应该只有一个地方(这个地方可能是某个方法)。
【Guideline 9】将你的假定和决定记入文档。有了这样的文档或日志,我们就可以进行项目的回顾。
【IMO】在面临多种选择而需要做决定的时候,我们应该把做决定的理由记录下来,特别是那些重要的因素。而后,我们就可以分析当初的决定以及做出决定的过程。很多项目组在做出某个决定的时候(比如系统设计,采用的技术方案,具体的代码实现技术等)没有做到这一点,以至于后来者根本不知道系统为何这样实现,小组成员也不能很好地总结经验得失。
【Guideline 10】确定什么是系统中的Deviations(就我的理解:基本等同于Exception,可恢复,比如用户输入了一个不存在的id号)和Errors(不可恢复到正常状态的异常,比如网络不通),并决定如何处理它们。
【IMO】这就是系统异常处理的策略了。可以把异常进行类别划分,以区别不同的异常;且划分严重等级,然后分别处理它们。异常处理是个比较复杂的工作,涉及到异常的捕获、传播、处理和错误恢复等。通常用户期望看到的是友好的提示信息,而开发者能从日志中了解到底在哪里发生了什么样的异常。
【Guideline 11】别急着飞奔,直到你知道你要去哪里。在你使系统快速运行前保证系统正确。
【IMO】先让系统正确运行,然后再考虑效率的问题。这和系统的抽象程度有比较大的关系。抽象程度越高,完成一件工作往往经过的环节就越多,具体的表现就是方法调用的次数大大增加,这势必降低性能。对于那些有严格资源限制(memory,disk,CPU time)的系统我认为从一开始就要考虑避免做过高的抽象化,注意性能问题。
【Guideline 12】工具就是工具,聪明地使用它们。
【IMO】工具能提升我们的工作效率,也会深刻影响我们的工作方式。但工具毕竟是工具,使用工具而不是被工具所左右,在合适的场合使用合适的工具,工具始终是服务于我们的。
在一个示例系统的开发过程中讲述这些Guidelines。
第一个版本只做MFS(最小特性集合)。分析这些需求,进行初步的设计,在coding时再细化它们,可能添加新的class。
【Guideline 13】防止分析瘫痪(Analysis Paralysis)和设计瘫痪(Design Paralysis)。
【IMO】分析瘫痪是指停滞于分析阶段,总觉得有些需求还没有分析清楚,裹足不前,迟迟无法进入设计阶段。而设计瘫痪是指设计者想把每个class的每个method的细节都设计得非常清楚,迟迟无法进入编码阶段。做过系统分析的人应该有体会,想把需求的每个细节都考虑得非常清楚,让他写一份需求规格迟迟不能完成,他在做分析的时候甚至想到了设计实现的问题,结果白白延误了时程。而设计其实也不可能是一下子能做得面面俱到和详细完备的,即使你使用sequence diagram来表达use-case realization,也不能确定每个细节,而只能确定最主要的部分——只要能确定这部分就足够了,在编码阶段再逐步细化设计是实际的做法。
【Guideline 14】需求总会变化,但你并不知道它会如何变化。所以最好的做法就是先在目前的条件下工作,准备好在需求变化时变更计划。
【IMO】没有不变的需求,能做的就是对需求进行有效地管理。这又是一个很大的主题了。一种做法是先列出最小Minimum Feature Set(MFS),快速做出一个prototype system和客户进行沟通,获得反馈以调整系统。不过也会一些系统需求很少变化的,甚至几乎不变,这样的系统使用传统的瀑布模型大概就可以搞定;而那些需求有小范围的变化的系统,使用RUP这样的流程大概是比较合适的;而需求变化剧烈的系统,使用XP会比较合适。
【Guideline 15】报表能定义系统。
【IMO】通过报表,我们能了解到用户期望得到哪些信息。对于那些有大量报表的信息系统,我们可以从这些报表了解到系统需要收集和处理什么数据。我曾经参与了一个大型的信息系统的开发,用户需要大量的报表(上百张),可是等系统快要交付的时候才发现系统中根本没有某些报表中需要的数据,这是一个实实在在的教训。
【Guideline 17】Plan Globally, Develop Locally。全盘规划,局部开发。增加的实现应该顺应整体计划。
【IMO】这里体现了Big Picture和计划的作用,无论项目进行到了哪里,我们都应该清楚下一步该如何进行。做手边的事情,展望明天的计划。
【Guideline 18】对于功能性需求,如果它不能被测试,那我们就不需要它。
【IMO】不知道Ken Pugh为何提出这点,我想这是显而易见的。不能测试的功能就是无法验证的功能,那这个功能性需求根本就不能称为需求了。而非功能性测试则不在此列,诸如“易用性”和“易于维护”这样的非功能性需求根本就是模糊的概念,因而无法精确验证。
【Guideline 19】CLEAR-CUT TEST。做合理和明确的测试。
【IMO】Clear-cut的测试不是指说明了“2秒内完成远程数据查询任务”的test-case。还要看具体的环境因素。如果一个测试根本无法完成,那可能是需求定义就有问题,比如在一个速度极慢的网络上完成大数据量的查询任务,需求中要求“2秒内完成”,其实这是不合理的。测试不仅能发现软件的问题,也能发现需求的问题,做过test-case测试的人就有体会。
【Guideline 20】计划好测试。预先做好测试策略能带来好的设计。
【IMO】做测试的时候,总需要在某个context中完成它(通常会使用Mock Object),所以对某个单独的方法进行测试常常是无多大意义的。在这个过程中,我们能发现设计上的问题。
【Guideline 21】如果你忘记了安全问题,那说明你是靠不住的。安全不应该只是作为补救措施,而应该在开发的各个阶段都考虑到。
【IMO】微软的“威胁模型”可以帮助我们很好地体会这点。参考。
【Guideline 22】DON'T OVERCLASSIFY。不要过度分类。将概念分离到各个类中是基于行为,而非数据。
【IMO】也就是说,我们应该根据类中方法的行为逻辑来划分类,而不是数值。比如,某个事物能分为几类,我们可以有两者设计:每个类别的事物都用一个class来表示;或者只使用一个class,含有一个“类别”属性,这个属性是一个枚举。到底哪种设计合理要根据上面的原则来判断。
【Guideline 23】DECLARATION 胜过 EXECUTION。Declarative-style的编码可以提供更好的弹性。
【IMO】Declarative-style是指那种可以config的代码,而不是把一些东西hardcoding到代码里面,常用的做法就是使用table-driven code和配置文件。比如数据库的连接串等这样应该作为配置项的内容不能直接写死到代码里面。
【Guideline 24】AVOID PREMATURE INHERITANCE。避免过早的继承。
【IMO】先使用接口而不是继承。以前的设计者喜欢使用继承,而现在则喜欢使用接口、代理和组合等等手段。
【Guideline 25】在不同的程序之间使用文本(text)通信,而非程序内部。
【IMO】显而易见。不同平台的程序使用文本通信可以不用去考虑不同平台的差异。比如我们广泛使用的XML。而程序内部使用某个string text来决定逻辑则不是好的做法,可以使用枚举代替。
【Guideline 26】如果有集合(Collections)操作,那就让它成为一个集合。集合可以将对象的使用和对象的存储隔开,并且可以隐藏聚合操作(aggregate operations)的实现。
【IMO】.Net Framework中有大量的例子:XXXCollection。
【Guideline 27】每个class应只代表一个抽象,可以使用简单的描述来解释其目的和含义。
【IMO】也就是说要避免模糊的概念,只负有单一的职责。
【Guideline 28】对象法则:一个对象应该只做其方法声明做的事情;一个对象应是无害的;当一个对象无法完成被请求的操作时,它应该通知其使用者。
【IMO】面向对象设计的单一职责原则(SRP)。熟悉c/c++的人写方法的时候通常喜欢给方法加一个返回值(比如int类型的)而不是使用void。从返回值就能知道方法是否执行成功了。.Net中则使用抛出Exception的办法来告诉调用者出现了什么状况。
【Guideline 29】NEVER BE SILENT。如果一个方法遇到了错误(error),应报告(report)出来而不是不作任何回应。
【IMO】属于错误处理的范畴。我想遇到了错误(或者异常)并不是简单地“Report”了。
【Guideline 30】将method置于class中是基于它需要什么。如果方法不需要instance data,那它不是类的成员;否则,它是类的成员。
【IMO】也就是说,如果方法中不会处理类的实例数据的话,那它应该是个静态方法(在c/c++中则可作为函数单独存在);否则是非静态方法。.Net中工具类常常都是静态方法,因为它们不处理所在类的instance data,这些方法只属于class,而不属于object。
【Guideline 31】用接口而非继承。在类之间的关系中,接口能提供更好的流动性(fluidity)。
【IMO】interface是很好的东西。但c++中没有interface,而只能使用继承,效果也一样。
【Guideline 32】将某个特定的工作做好,能提高复用度。
【IMO】类和方法只做做单一的事情,越单一就越容易复用。有人喜欢little method,每个方法只包含几行代码,做很少的事情。不过方法调用太多会影响性能,要写更多的代码,也使得做一件工作需要跳转多个地方,可能让人头晕。我看还要注意“度”的问题。
【Guideline 33】将策略和实现分离。将“what”和“how”分离可以使得“what”更易读和维护。
【IMO】众所周知的啦。不过很多人在编码时却犯错。比如一个方法内部要根据某个策略来做某件事情,根据“将策略和实现分离”的原则,策略应该写在另一个方法中,而本方法只做其应该做的事情。更具体的例子:Save方法中我需要保存一条记录,但需要判断是否已经有相同的记录(相同不一定就是指相同的id号)存在了,然后再决定如何操作,根据“将策略和实现分离”的原则,那我应该写一个方法IsExist来判断是否存在相同记录,而不是把IsExist的逻辑写在Save方法中。
【Guideline 34】使用Extreme Naming。
【IMO】命名要能完整和清晰低表达method所做的事情。这样可以使得可读性更好,使用者也更加易于理解该方法的用途。虽然这可能使得method name比较长,但这是值得的。C/C++世界里面的人们好像都喜欢使用很短的名字,而且热衷于使用大量缩写,这也是其代码常常比较难懂的原因。
【Guideline 35】重载函数会变成过载。少使用重载,而使用唯一的命名,这样函数可提高自我描述性(self-describing)。
【IMO】重载是很多语言都提供的特性,但它会影响代码的可读性。
【Guideline 36】分离关注点到更小的关注点。分离类和方法的职责以简化每个类和方法。
【IMO】没有什么可说的,SRP的具体体现。
【Guideline 37】FIGURE OUT HOW TO MIGRATE BEFORE YOU MIGRATE。
【IMO】似乎是废话,如果没有想清楚如何去做一件事情的话,当然不会盲目去做。
【Guideline 38】确定对象的唯一性标准。
【IMO】如果某对象是唯一的,我们需要确定如何判断它是唯一的。比如ID什么的。
系统的第一个版本出来了(MFS的版本,只实现了最基本的功能),我们有必要进行“回顾”。使用这个版本可以从客户那里得到反馈,从而可以进行下个版本。
【Guideline 39】每个Release之后都进行回顾(retrospective)。检查你的设计以及你如何设计的会给下个release带来帮助。
【IMO】这的确是很好的做法,也是很多project没有做到的。善于总结的人和team必定是进度最快的人和team。
【Guideline 40】Place all test-only methods in a test interface。Class会有一些仅仅用于测试的方法,把这些方法放到test interface中,发布的产品中不应该含有这些方法。
【IMO】事实上我们设计的class中不应该含有这样的test method——为了能够测试该类而给该类添加了额外的方法,这说明设计存在问题。但是,刚开始设计的时候可能会出现的问题,不用担心,随着设计的完善,这样的test method会越来越少。Class是否易于测试是衡量设计好坏的重要标准之一。
【Guideline 41】BUILD FLEXIBILITY FOR TESTING。Plan for flexibility in your design to allow for ease of testing。如果系统的测试版实现很容易被正式版实现代替代话,那说明该设计具有弹性,是易于测试的。
【IMO】Ref:Agile Modeling。
【Guideline 42】CONSIDER THE USER。
【IMO】不为用户考虑的软件不是好软件,也将是失败的软件。我们需要考虑不同用户的需要。
【Guideline 43】NOTHING IS PERFECT。通常都会有一个更好的解决方案,但我们做到够好就可以了。
【IMO】资源是有限的:时间(time)和预算(budget)。如果这两者都不受限制的话,我们肯定能做出完美的系统(什么样算完美呢?),可惜我们常常受困于紧缩的时程,非常有限的资金投入,我们只要做的足够好就行(什么是足够好呢?Ref1,Ref2)。我想如果系统满足了用户的需要,达到了其商业目标,那就是足够好的。
【Guideline 44】精确的定义可以帮助我们避免不必要的曲解和争论。
【IMO】系统分析师的职责就是要写出不具有二义性的需求规格来。如果某些概念界定得模糊不清,那最后难免会发生争论。
用户添加了新的需求,我们的系统需要做出相应的反应。
【Guideline 45】DECOUPLE WITH ASSOCIATIONS。使用聚合(Association)来解耦class。
【IMO】类A和类B相耦合,类B又和类C相耦合,对于这样的情况我们可以使用Association来解耦这三个类,另类B拥有指向类A和类C的引用(作为类成员),而让类A和类C都不直接和类B发生联系。
【Guideline 46】Use state-based analysis to examine object behavior。使用状态图来分析对象的行为。
【IMO】UML中的State Diagram可以在我们做分析类的时候派上用场,合适的话,可以使用State模式来解决问题。
【Guideline 47】TEST THE INTERFACE, NOT THE IMPLEMENTATION。
【IMO】显而易见的。我们测试的是其外部表现行为,而非具体的实现。以前看到 “unit test中是否要测试private方法” 的讨论,我想可以应该本原则来回答这个问题。既然是“针对接口编程,而不是针对实现”,那我为何要去测试private方法?
【Guideline 48】Split a single interface into multiple interfaces if multiple clients use different portions of the interface。
【IMO】尽量避免Fat Interface。Ref:《Large-Scale C++ Software Design》。
【Guideline 49】Create something basic before adding refinements。
【IMO】敏捷开发的思想。
【Guideline 50】Use templates or scripts for classes and methods to create consistent logic。
【IMO】Ref:Reusable。C++中有模板,现在C#2.0中也有泛型了,合适地使用模板能提高代码的复用,借用Effective C++中的话:当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类;当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。
【Guideline 51】Create the interface you desire and adapt the implementation to it。
【IMO】似乎是废话,接口变了,实现当然要变。
【Guideline 52】Add proxies to interfaces to add functionality。
【IMO】在合适的时候使用Proxy Pattern。
【Guideline 53】THE EASIEST CODE TO DEBUG IS THAT WHICH IS NOT WRITTEN 。Never write functionality that already exists in usable form。
【IMO】也就是说要尽量利用已有的东西,没有必要任何都自己去写。现在这么多优秀的Open Source的project真是可以为我们省掉不少事情。
【Guideline 54】MORE IS SOMETIMES LESS。Use a prewritten module with more features than you currently need and adapt it to your current needs。过犹不及。
【IMO】不用的东西就把它去掉。
【Guideline 55】Indirection, using either methods or data, adds flexibility。
【IMO】实际的例子就是Factory——不直接使用new来得到实例,而是通过Factory。另外,还有CSS。
【Guideline 56】LOGS ARE USEFUL。
【IMO】日志的作用不用多说了。
【Guideline 57】PLAN YOUR LOGGING STRATEGY。Determine where and how you are going to log。
【IMO】确定合理的日志策略。记录大量无谓的log等于没有log。一般我们需要记录用户的安全验证信息和程序异常信息(主要是那些严重的Error,以及能帮助我们调试程序的信息)。
客户改变了先前的需求,现在的某个Term发生了概念上的改变,我们该如何这样的问题处理呢?
【Guideline 58】DON'T CHANGE WHAT IT IS。Create new terms rather than trying to apply new meanings to current terms。
【IMO】Ken Pugh给出了很好的答案——不要改变原先的概念(特别是系统的一些核心概念),因为那会带来很多问题,可能引起混乱和混淆。给一个类添加心的方法是很容易的,但是要改变既定方法的外部行为的话,那将会带来很大的代价。添加新的term好过给已有的term添加新的语义。
【Guideline 59】BE READY TO IMPORT AND EXPORT。Data should be available for use outside the system via a well-defined data interface。
【IMO】Import和Export是系统中很实用的功能。Import可以方便地初始化系统数据,而Export可以方便数据迁移。
【Guideline 60】CONSIDER FAILURE AN EXPECTATION, NOT AN EXCEPTION。Plan how operations should respond to failures。
【IMO】健壮的系统需要Error Detection And Recovery的能力。这是个很大的议题,Ref:A Program Structure For Error Detection And Recovery。
现在又添加了一些预期要加入的功能,这些功能起初是排除在MFS之外的。
【Guideline 61】USE THE CLIENT'S LANGUAGE。Use the client's language in your code to make it easier to compare the logic in the code to the logic of the client。
【IMO】Ref:Domain-Driven Design: Tackling Complexity in the Heart of Software。
【Guideline 62】BUSINESS RULES ARE A BUSINESS UNTO THEMSELVES。Keep business rules separate from other logic。
【IMO】业务规则常常变化,所以把它和其它逻辑分离开来是较好的做法。现在也有一些开源的Rule Engine了。
【Guideline 63】CONSIDER PRIVACY。Systems need to be designed with privacy in mind。
【IMO】安全也是一个庞大的话题,也是在系统的任何阶段都需要考虑的问题。
现在又有了新需求了,原因是客户的业务扩大了,现有的系统根本不能满足其需要了。系统势必要调整其架构才能满足新业务的要求,而且系统将面向的是Web,安全性方面的要求更高了。
【Guideline 64】With interfaces exposed to the outside world, ensure that input validation and logging occurs。
【IMO】验证用户的输入的合法性,这是关系到系统安全和业务逻辑正确执行的问题。
【Guideline 65】AVOID PREMATURE GENERALIZATION。Solve the specific problem before making the solution general。
【IMO】有不少人喜欢搞GENERALIZATION(ref)——其实是过犹不及。
Refactoring这个词本身并不值得引起多大的关注,重要的是上面的Guidelines,给我们提供了很有价值的指导。
Prefactoring: Extreme Abstraction, Extreme Separation, Extreme Readability