大话重构连载19:大对象的演化过程
很好,我们终于迈出了重构的第一步,而这第一步我们瞄准了代码问题的重灾区——超级大函数。超级大函数之所以是代码问题的重灾区,就是因为它们往往难于阅读、难于维护。面对大函数我们采取的办法是拆分,以功能为核心将其拆分成一个一个独立的函数。拆分后的程序变得易于阅读了,因为要读懂程序你不再需要读完所有代码,选择性的读取那些顶级函数,只需了了数行代码,你就可以明白整个程序。
但是,当我们将数千行的大函数分解成数十个小函数时,另一个问题出现了。想象一下,数十个函数被杂乱无章地堆放在一个对象中,看看就让人头疼。实际上,我们是不会这样做的。当我们开始了对大函数的分解时,随之而来的就是对大对象的分解。大对象,就是指的那些包含数十个甚至上百个方法或者函数,功能无所不包的超级对象。在很多遗留系统中,总有那么几个超级对象,系统几乎所有的功能都在它的里面有对应方法。这样的对象,密密麻麻的方法让人困惑,更关键的是,各种各样的功能被耦合在一起,稍有修改就会影响到许多功能,甚至让那些毫不相干的功能产生BUG。因此,我们应当合理地拆分我们的大对象。
与大函数一样,很多时候遗留系统中的大对象,也都是伴随系统业务复杂度的逐渐增长而出现的,我们来看看它的演进过程吧。我们说软件实际上是对现实世界的模拟,通过这种模拟,实现信息化的管理,来提高我们的生产效率。但是,现实世界是复杂的,各种事物之间存在着各种各样纷繁复杂的联系,因此我们不可能完全模拟现实世界的所有,只可能是现实世界的一部分,客户急需要模拟的那一部分。
人的大脑认识事物总是一个由简单到复杂的过程,这是我们的客观规律。因此,我们的软件模拟真实世界也是一个由简单到复杂的过程。最开初我们的想法总是非常简单而单纯的,就是让软件做一件非常简单而明确的事情。由于这时候业务非常简单,我们不需要太多的类和方法就可以实现业务操作。比如开票业务,就是将已开具的发票信息读取出来,保存。这样一个简单操作,设计成一个简单的开票业务类合情合理。
但是,随着软件模拟真实世界的不断发展,业务变得越来越复杂。比如这个开票业务,我们随后的业务开始变更,要检查购方是否存在、开票人是否有权限、库存中是否还有发票,等等。起初只有一种开票方式,但随着非正常开票业务的增加,许多相关的业务也随之变化……随着业务的不断增加,软件代码的规模也在发生着质的变化(如图6.1所示)。
图6.1 开票业务的演化过程
过去开票业务类只有百来行代码,现在被膨胀到数千行代码。各种条件语句层层嵌套,各种临时变量穿插跑位,程序变得难于理解。由于读不懂代码,修改代码的程序员开始在走钢丝,一不小心改动了某个关键程序就可能引入重大BUG。为了避免重大BUG的出现,测试人员耗费巨大精力进行严格的测试。毫无疑问,软件开始进入一种恶性循环,软件退化开始一步步加深。
面对这种软件规模增大而带来的恶性循环,我们必须做出改变。面对问题我们不能病急乱投医,而是应当对症下药。这个正确的药方就是以职责驱动设计思想为核心,调整我们的程序结构,构建高内聚、低耦合的软件系统。职责驱动设计,就是要求我们设计的所有类和接口,都要有自己的职责定义。而每个类和接口内部的所有方法和属性都是围绕着该职责来进行的,它们都是高度相关的。每个类和接口决不去做跟自己职责无关的事情,所有与自己职责无关的事情,都应当交给其它拥有该职责的类来完成,而自己仅仅是去调用。这就是职责驱动设计的思想,而每个类其内部包含的功能所达到的高度相关的程度,我们称之为“内聚”。
概念似乎有一些抽象,我们来举例说明吧。对于开票业务,我们设计了开票业务类来处理它,因此开票业务类的职责就是完成开票操作,这似乎毫无问题。但是,我们仔细审视开票操作,就会发现它包含了好几个部分:首先,我们读取客户、开票人、发票库存等信息进行相关的校验,然后保存这些发票到数据库中,最后统计当月的票量及金额。通过这样的分析,它们似乎不再那么功能相关了,读取和校验客户、开票人、发票库存等信息是客户、开票人、发票库存实体类的职责,读取和保存发票似乎是发票类的职责,而统计当月票量与金额似乎是财会统计类的职责。分与不分,完全取决于软件代码的复杂程度。如果总共也就几十行代码,我们写成一个类中的几个方法就可以了;但随着功能复杂度的加深,那么我们必须得拆分,分配到不同的类中配合完成我们的功能。
随着软件业务的不断变化,我们的软件在发生着质的变化。发票保存前我们必须要进行一系列的校验工作:检查购方是否存在、开票人是否有权限、是否还有发票库存,等等。不同的校验,读取的是不同的数据,它们的顺序可能变化,校验的个数也可能在调整。随着需求的变化,一些校验被增加进来而另一些则被剔除。我们判断功能是否相关的一个非常重要的原则,就是是否是软件变更的同一个原因。比如,“检查购方是否存在”与“开票人是否有权限”,不是软件变更的同一个原因:“检查购方是否存在”是与客户信息管理直接相关,而“开票人是否有权限”则是与用户权限定义密切相关,因此它们不能放在同一个类中。为什么呢?因为“检查购方是否存在”的业务逻辑变更时,不应当影响到“检查开票人是否有权限”的功能。最好的办法就是,将它们各自封装在各种相关的业务类中(如图6.2所示)。
图6.2 开票业务的拆分
经过以上分析我们发现,开票操作随着业务逻辑的不断发展,应当在原有的程序结构上,将开票业务类拆分成几个部分:各种校验类、发票业务类与财会统计类。这样的拆分使得开票业务类最终由一个什么都干的多面手,变成了一个管理者。它不再参与那些具体的工作,而是将工作分配给不同的人,成为一个组织协调者。经过这样的调整,我们的程序将变得更加易于阅读、维护、变更。
大话重构连载首页:http://www.cnblogs.com/mooodo/p/talkAboutRefactoringHome.html
特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!