《大话重构》

本文摘自《大话重构》一书。

第5章

第一步:从分解大函数开始

超级大函数―软件退化的重灾区

        解决超级大函数问题最有效的办法就是分解,按照功能一步一步分解,还原其应有的优化结构,在这个过程中我们常用的重构方法叫“抽取方法"。P49

        当我们在阅读一段大函数时,我们可能自觉不自觉地为这些代码分段,为一段功能相对独立的代码编写注释。有时我们可能还需要调整代码的先后顺序,将一些有更多关联的代码放在一起,如将变量的声明与变量真正使用的代码放在一起,或者将有明显前后关系的代码放在―起,这样的调整是一个好的开端.因为它让我们的代码开始变得有序,开始变得可读。

        随后我们就可以使用“抽取方法"了,将被我们分段、加上注释的代码从原函数中抽取出来,放在另外一个独立的函数中,为这个函数取个易懂的名称―这是一个非常重好习惯。

        重构是系列的等量变换,抽取方法是这些等量变换中最典型的例子。将―段代码从原函数中抽取出来,代码依然是那些代码,只是程序结构发生了变换。正因为如此,才能保证我们的重构过程安全可靠。

5.2 抽取方法的实践 P51
        你不必阅读和整理完所有的代码才开始抽取,许多遗留系统的大函数非常长,你可以整理部分,就开始着手重构,比如,把刚才分段的代码段抽取出来形成个独立的函数,在这里,代码段与注释,是我们决定是否需要抽取成函数的重要标志之一。
        在阅读过程中,许多长相相似的代码也是我们需要重视的重要代码,重复代码是许多遗留系统代码质量差的重要原因之,因此提高代码复用就成为了代码优化的个重要内容,整合大量重复的代码,将其提取到个统的函数中为其他各处所调用,是一个值得推荐的办法,因此,重复代码也是抽取函数的重要标志。P52
        除此之外,一些块操作的语句,如条件语句,循环语句,try语句,都可能成为抽取函数的标志,最典型的就是if语句,包含在if语句中间的常常是个相对独立的功能。

5.3 最常见的问题 P54
        将代码从原函数中抽取出来,放进新的函数时,将所有要交互的数据都写到函数的参数中,理论上是没有问题的,却不是―个好的设计, 在处理数据交互方面,应该选择值对象,但将一堆变量杂乱无章地塞进一个值对象中,不是不可以, 可是是随着重构的深入,值对象也在逐渐优化,最终每个值对象都应当对应现实业务中的一个事物,而它所包含的变量,就是这个事物应当有的属性。因此,最终函数传递的参数,应当是几个值对象,以及这些值对象以外的几个变量,建议一个函数的参数不超过六个,最好一至四个。

        另外一个比较麻烦的问题就是返回值,在java世界,函数的返回值只能是一个,如果我们的程序需要返回2个,甚至更多时,怎么办呢?返回一个值对象,是个好的办法。但在个函数中传递的参数是个值对象,返回值又是另个值对象,如果都这样设计就会出现大量的值对象,造成值对象的泛滥。P55

        将要返回的数据直接塞进参数值对象中,是我们可以采用的另一个好的方法。在Java语言中没有形参和实参的区别,如果传递进去的是一个对象,在函数中将该对象的某个属性进行了修改,即使该程序已经跳出了抽取函数而回到原函数,通过原函数对该对象的访问,就实质性地实现了抽取函数返回值的功能。我们的函数,就是对这些值对象中数据的处理过程,我们的处理过程被设计成由―系列函数依次对某个值对象进行连续处理,这样值对象就变成个载体,在原函数与抽取函数之间交互数据。P55

        但些参数,我们并不希望作为参数直接传递进来,这时候,将抽取函数中最前面或最后面的部分代码移出该函数,放回原函数,是一个比较直观的想法。

 第6章第二步:拆分大对象
6.1大对象的演化过程 P57
         职责驱动设计,就是要求我们设计的所有类和接口,都要有自己的职责定义,而每个类和接口内部的所有方法和属性都是围绕着该职责来进行的,它们都是高度相关的,每个类和接口决不去做跟自己职责无关的事情,所有与自己职责无关的事情,都应当交给其他拥有该职责的类来完成,而自己仅仅是去调用,这就是职责驱动设计的思想,而每个类其内部包含的功能所达到的高度相关的程度,我们称之为“内聚。P58

         我们判断功能之间是否相关的―个非常重要的原则,就是它们是否是引起软件变更的同一个原因。比如“检查购方是否存在”与"开票人是否有权限",不是软件变更的同一个原因:“检查购方是否存在”是与客户信息管理直接相关,而“开票人是否有权限”则是与用户权限定义密切相关,因此它们不能放在同―个类中.为什么呢?因为“检查购方是否存在”的业务逻辑变更时,不应当影响到“检查开票人是否有权限”的功能。最好的办法就是,将它们各自封装在各种相关的业务类中。59页

6.2 大对象的拆分过程—抽取类与职责驱动设计 P60
拆分大对象的过程其实非常简单,就是将原对象中的某些方法移动到其他对象中,这个重构手法被称为“抽取类” ,其移动的原则就是“职责驱动设计",即将这些行为或方法交给拥有该职责的业务类。 设计者应该始终遵循着―个原则/设计要与真实世界保持一致。

         尽管理论非常清晰,但要落到实际开发程中,面向身债的分析与设计依然是困难的。在实际工作中,分析一个方法应该给谁,判断一个类中是否有跟自己职责不相关的行为,标准依然是十分模糊的。这里我给大家三个判断方法:分析领域模型、分析业务变更的原因,以及寻找信息专家。 

        分析领域模型,是需要分析后期,设计开发初期我们应当完成的一项模型分析。分析领域模型,就是站在用户的角度,分析业务领域相关的事物、事物的属性、事物拥有的行为;以及事物与事物之间的关系。领域模型中有些什么事物,软件系统中就应当有什么对象;领域模型中的事物有些什么属性和行为,软件系统中这些对象就应当有什么属性和方法;领域模型中事物之间是怎样的关系,软件系统中相应的对象间就应当有对应的关系:我们对领域模型的分析,是确保我们软件系统的设计能够与真实世界相对应的一种保障。因此,在 件重构过程中,适时地绘制一些领域模型,按照领域模型的方法去创建对象、确认哪些方法应当分配给哪个对象,不失为一个好的方法。60页

         信息专家,就是软件系统中拥有某些有用数据的业务对象。在GRASP(中文名称为“通用职责分配软件模式”)设计模式中的信息专家模式告诉我们,应当将某个方法分配给那个拥有执行该方法所需数据的信息专家,检查客户状态所需的是客户信息,因此把这项检查分配给客户信息管理业务类;检查开票人权限所需的是用户权限信息,因此分配给用户权限业务类;检查发票库存所需的是发票库存信息,分配给发票库存管理业务类。采用信息专家模式可以大幅提高系统可维护性有效降低需求变更对系充的影响。比如,客户管理业务的变更不会影响开票业务代码,因为对客户状态的校验是在客户信息管理业务类中完成的,而开票业务类只是调用其方法并获得一个结果,仅此而已。P61

6.3单一职责原则(SRP)与对象拆分
          我们通常的理解,职责就是完成某一个方面的所有事情,而大师却给出了完全不同 的另—个解释:一个职责就是软件变化的—个原因,这也是软件设计原则SRP(中文译为单一职责原则)的精髓之所在。

        拿客户管理业务与客户状态校验来说吧,客户状态变更是客户管理业务的一个部分因此客户状态相关的业务变更,必然是包含在客户管理业务的变更中的,也就是说它们是软件变化的一个原因,在设计上放在一个类中理所当然。

         紧接着,业务需求再次变更,由原有单纯的开票业务,划分为正常开票与非正常开票两个业务。正常开票与非正常开票,在许多地方的业务是相同的,但也有许多业务不相同,正常开票业务变更,并不意味着非正常开票业务的变更;非正常开票的变更也不意味着正常票业务的变更。这为我们传递出了一个重要信息:正常开票与非正常开票是两个软件变化的原因,根据SRP原则,它们应当被拆分。

         但是另外一个问题接踵而至:正常开票与非正常开票的业务需求在许多地方是相同的,这就会导致,分别编写正常开票业务类与非正常开票业务类,会造成大量重复代码,怎么办? OOA/D中的继承正是解决该问题的最好办法。

6.4 合久必分,分久必合——类的归并
         随着重构工作的不断深入,开发人员开始逐渐对软件系统由不怎么懂转变为精通,随看对业务理解的逐渐精通,我们开始尝试绘制并逐渐完善领域模型,这时,系统中应当有哪些业务类,每个业务类都应当有哪些方法和行为,系统中的各个力法都应当分配到哪些业务对象中,对于这些问题我们开始逐渐熟悉而清楚起来,当我们逐步尝试在草图中绘制出领域模型时,进入系统再次审视这些力法类,我们开始明白它们真正应当怎样分配了,这时,重构开始进入“分久必合”阶段,起初分散在各个方法类中的方法,现在逐步合并到各个业务类中,从而实现了最终正确的设计。 

 第7章第三步:提高代码复用率
7.3.1当重复代码存在于同一对对象中时抽取方法

7.3.2当重复代码存在于不同对象中时—抽取类

7.3.3不同对象中复用代码的另一种方法—封装成实体类

7.3.4当代码所在类具有某种并列关系时一抽取父类 
         运用“抽取父类”(Extract superclass)的重构手法,从多个要重构的类中抽取出一个共同的父类。父类中包含的方法应当是经过比较后相同的部分,而将不同的部分保留在原有的类中。

 7.3.5 当出现继承泛滥时—将继承转换为组合
         除此之外,还有一种办法是将不同的部分用一个接口与其多个实现来解决,当实际的应用软件系统比较复杂时,使用继承比较容易出现“继承泛滥"的问题因此,一个可行的办法是将继承转换为组合。76页

7.3.6当重复代码被割裂成碎片时—继承结合模板模式 
         要重构的代码被相同的部分与不同的部分分割成了好多的碎片。遇到这种情况,采用继承结合模板模式的方法是最有效的了。如果你为一个算法定义了一系列步骤,并且允许子类来实现其中的一个或多个步骤,你就可以使用这个模式,该方法将把分离得支离破碎的程序过程,分解成数个方法定义在模板模式的父类中(即每个方法就是―个步骤)并且在父类中定义了这些方法的执行顺序。之后,将代码中相同的部分写在父类中,将不同的部分分别在子类中实现。78页

7.4代码重复的检查工具
         在我们的开发过程中有效地建立起持续集成境,常常是保证代码质量非常重要的手段之一。首先,在开发组中建立起版本控制服务器(如SVN)并要求所有开发人员频繁提交自己的代码,然后,在版本控制服务器的基础上建立起持续集成服务器(如Jenkins),在建立持续集成服务器的过程中,我们可以根据需要加入一些静态代码检查的工具(如PMD、Fingbugs、Checkstyle等等)这些静态代码检查工具,是用来检查我们编写的代码是否符合规范的,其中的一项检查就是是否有重复的代码。79页

第8章 第四步:发现程序可扩展点
8.1开放―封闭原则与可扩展点设计 
        在面向对象设计与开发领域有一个十分重要的原则,那就是OCP原则(Open-Close Principle,开放—封闭原则)。我们开发的软件系统,对于功能扩展是开放的。这就意味着,当系统需求发生变更时,我们可以对软件功能进行扩展,使其满足新的需求。同时,我们对软件代码的修改应当是封闭的。这意味着我们在修改软件的同时,不会影响到系统原有的功能。

        要遵守OCP原则,并不是意味着实现新需求时不能修改原有代码,而是应当遵从“两顶帽子”的设计原则,先重构原有的代码,使其具有可扩展功能,然后再添加新程序,使其满足OCP原则。这才是可扩展设计的关键之所在。 

        当新需求到来时,我们应当分为两个步骤进行设计:第一步是在不增加新功能的条件下实现可扩展功能。如上例所示,我们首先通过“抽取方法”将原程序的每个f语句的内容抽取到一个方法中,然后运用“抽取类”将这些方法抽取到多个类中。之后,我们运用抽取接口”去归纳各个不同的类,从而抽取出共同的方法放到接口中,最后实现接口的配置与调用。这里运用了一系列等价的重构方法,并以“小步快跑”的方式保证了每一步的正确性。

8.2 过程的扩展与放置钩子―运用模板模式增加可扩展点
        解决处理过程中相同或相似的代码,最好的办法就是使用模板模式。首先将那些相同的代码抽取出来形成函数,将这些函数抽象并升级为抽象类及接口,然后将各自不同的代码统函数名,放在各个实现类中各自去实现,这样,代码复用的问题就解决了。

        我设计了许多控件,如文本框,下拉框,复选框 。起初我为所有控件提供了draw()和beUsed()方法, 用于绘制控件和判断该条件在查询是是否被使用。随着控件品种的增加,一些控件需要在绘制前执行一个查询,为此我准备设计一个getItems()方法,只要这些控件定义了各自的查询语句,就可以通过该方法查询并返回结果。但问题是,前面已经设计好的控件不用这个方法,我不希望为这个功能的扩展影响了前面那些控件,这该怎么设计呢?

        每次面对这样的问题时,一种叫做“钩子”(hook)的设计就可以派上用场了。什么叫钩子”?它是一个空函数,调用它就如同什么都没有调用一般。但钩子如果被放在了抽象类中,作用就非常大了。如果抽象类的子类要使用它时,则重载这个函数,为其编写各自的代码,完成相应的操作;而其他的子类如果不使用它,则什么也不用做。当系统在调用各个子类时,被重载的子类就会去调用子类中的函数,而其他没有被重载的子类则会去调用抽象类中的“钩子”,就如同什么都没有做一样。

8.3面向切面的可扩展设计
        面向切面编程(Aspect,Oriented Programming,简称AOP)是另一项非常重要而又实用经常用于可扩展设计的技术,如同它的命名―样,AOP是将我们的程序像做手术一样,切出专横切面,这些横切面就是程序的可扩展点,但是,这些可扩展点上要放入哪些程序呢?我们不必急急忙忙做出决定,而是在程序开发的后期、上线以后,甚至之后的某次变更需求时,才将程序加入到扩展点中,这就是“面向切面编程”(AOP)。

        在所有这些AOP设计中,最典型的莫过于业务处理前的校验程序了, ,对于大多数网络应用系统来说,业务处理前的校验都是稀松平常的一件事,但问题是,需求人员告诉我,系统在实施的时候可能会对校验进行调整,比如,有些客户要求应付单只能填写者自己才能修改,而另一些客户却不那么要求,这个需求要求我们在设计时,单据修改人校验应当支持维护功能,可以加可以不加。

        这时候,我们的AOP终于要出场了,通过AOP为应付单业务类在业务处理前增加一个可扩展点,以上所有的问题都将不是问题,这时,所有的业务校验都不再写入到应付单业务类中,而是被解耦到个个业务校验类中,然后通过配置文件配置起来(这里举例用的Spring的框架的AOP配置功能,扩展程序称为拦截器,被扩展的业务称为目标对象,将业务检验与权限检验放在业务操作之前的切面叫向前拦截,将事物处理同时放在业务操作之前和之后两个切面叫环绕拦截,还有名称匹配自动代理)

8.4 其他可扩展设计
        除了以上提到的几个方法,还有一些设计思路我们可以参考,其中个就是继承。
一种比较常见的情况是,加入某个对象不能满足我们新的需求,需要做一点儿改动,但为了满足AOP原则又不能修改原程序时,比较好的办法就是对该对象继承―个子类,在子类中进行修改,以满足新的需求。93页

第9章第五步:降低程序依赖度
9.1接囗、实现与工厂模式
9.1.1彻底理解工厂模式和依赖反转原则

        在教科书中工厂模式被分为三种类型:即简单工厂、工厂方法与抽象工厂。在面向对编程的时候引入了接口的概念,引入接口的目的就是希望我们在编写程序的时候能保证足够内聚,即它的业务逻辑不要为更底层的具体实现所耦合。这就是依赖反转原则。
在面向对象设计中另外一个重要的原则,就是依赖反转原则。它是这样描述的:
        1、高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口;
        2、抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口。

        例子:数据库访问,不依赖于某个数据库,工厂创建不同的数据库,提供统一的操作接口。

        工厂模式的核心就是那个工厂类,它在系统中必须保证单例,即整个系统只有个实例:在大型Web系统中,工厂类通常都是在系统启动时加载它的所有产品·,即它可以提供给系统的那些对象。加载产品,实质就是创建对象,并将对象放入到工厂类中的―个集合对象中。为了提高系统搜索性能,该集合对象通常被设计成一个Map,即每个产品都有一个key可以搜索到它,这样,客户程序通过类似Spring的getBean(key)的方法从工厂中获取对象。
        我们将工厂需要加载什么对象,写入到xml配置文件或数据库中,当系统启动时,工厂就会去读取配置文件或数据库,根据里面的信息创建产品对象。 但这样的设计还有另外一个问题要解决,那就是性能的问题。在大型Web系统中读取文件效率都是比较低的,特别在大并发用户访问的条件下,因此读取配置文件通常都是在系统启动时一次性加载并放入系统内存中的,这样的问题是启动后需要维护配置文件(读者注:热启动/动态更新)。101页

9.2 外部接口与适配器模式—与外部系统解耦
        适配器模式就是通过转换,将两个不兼容的接口接在一起。
        适配器模式的核心,就是它的那个接口与实现类,当内部系统需要调用外部系统时,我们并不是直接调用外部系统相应的接口或方法,而是调用适配器的接口,对于内部系统而言,它们并不知道,也不关心外部系统长得什么样儿,它们只知道与外部系统接口的适配器的接口长得什么样儿,这就可以了。当外部系统的接口或方法发生变更的时候,唯一需要调整的就是适配器与外部系统接口的实现。106页

9.3 继承的泛滥与桥接模式
        桥接模式最核心的思想,就是将原有的因两个变化而形成的继承改写为组合,将原有的强关联变成了弱关联,进而实现两个变化的相互解耦解决“继承泛滥”的问题。
        拿这个例子来说吧,如果客户程序期望通过SQL和数组参数的方式执行查询,原来的程序是期望调用一个ArraySqlQuerySupport的子类,其数组参数传递的方式被写死在该类中;现在我们让客户程序调用―个SqlQuerySupport子类,在创建时动态装配ArrayQuery实现类,从而实现了和原有ArraySqlQuerySpport完全相同的功能,同时又让代码编写更加灵活,提高了代码可维护性与易变更性,桥接模式完美诠释了“优先使用组合而不是继承”的设计原则 。
        标准桥接模式中抽象类对接口的引用,采用的是属性变量的形式,而不是传递参数,这里为什么要采用传递参数的形式呢?因为它是运行在单例模式下,现在大量的系统都在尝试将所有的业务处理类以单例的形式驻:留于系统的内存中,采用这样的方式,对象不再需要频繁地创建与回收,系统可以得到较好的性能,但是,采用单例模式,某个类在整个系统中只有一个实例,因此其属性变量就实质性地变成了全局变量,如果这个属性变量恒定不变而使所有的线程都使用同样的值,则没有问题,但如果每个线程都需要像非单例模式那样对属性变量赋不同的值,则在大并发状态下系统会存在重大隐患。因此,在这样的情况下,许多设计模式都必须要改了,需要将原有的为属性变量赋值,改为向所调用的方法传递参数。111页

9.4 方法的解耦与策略模式
        策略模式就是为程序中的某个方法定义—系列执行策略,对每一个执行策略进行封装,而由运行中的对象自己去选择需要采用哪个,或者执行哪些策略。从标准策略模式可以引申出许多变体,首先,客户程序在使用策略的时候,一次可以使用一个,也可以使用多个甚至全部。因此,客户程序在引用策略接口时,可以是该接口变量,也可以是―个接口变量的集合,此外客户程序在使用策略的时候,可以事先加载属性变量,也可以在调用方法时,作为参数传递。客户程序使用策略的方式决定了策略模式设计的方式。如果客户程序的每个对象都可能使用不同的策略,则策略应当作为函数调用的参数传递;如果客户程序使用的策略相对固定,或者客户程序不是单例的,则策略将在客户对象创建时作为属性变量传递给客户程序。但不论采用哪种方式策略模式都不可能独立存在,其一定伴随着工厂模式,即所有策略都是在工厂中创建后传递给客户程序的。115页

9.5 过程的解耦与命令模式
        策略模式为软件系统中某个执行过程提供了多个备选的方案,如方案A、方案B、方案C,由运行中的程序自己去选择到底使用哪个方案,同时,随着软件的不断发展,我们还可以提供方案D、方案E,也就是说我们为系统提供了横向扩展的能力,但在实际工作中也有许多扩展不是像这样将整个方案A全部替换为方案B的整体扩展,而是将该执行过程中某些步骤(如步骤3)进行调整,或者添加、去除某个步骤。我们当然可以整体替换,但为某―两个变化而整体地新做一个方案似乎有些得不偿失,还会带来重复代码和复杂度增加的问题,因此,需要采用新的方法来扩展我们的代码,即对执行过程提供纵向扩展的能力。

        要对执行过程纵向扩展,可以运用模板模式将这个复杂的过程从逻辑或者业务角度划分为多个步骤,比如,创建―个工厂对象是个复杂的过程,但我们可以划分为创建对象、读取XML·解析XML创建产品类,装载产品类等。步骤划分好步骤以后,我们就可以通过模板模式进行纵向扩展了。

        将复杂过程抽取成多个方法,每个步骤一个方法,只是使复杂过程变得有序而易于阅读,但还不能达到灵活自由扩展的能力,这时我们的“命令模式”开始派上用场了。首先将每个步骤方法使用“抽取类”抽取出来.形成一个一个命令类,每个命令类都有一个统一方法,如do()然后,在原程序中串行地依次执行这些命令类,执行过程纵向扩展能力就可以实现了。 116页

9.6 透明的功能扩展与设计一组合模式与装饰者模式 
        我们现在拿到的需求是要更新一个功能,但这个功能并不是全新的功能,而是对原有功能的调整,这时候我们应当怎样设计呢?你可以去修改原有的代码,但这不符合OCP原则。假如原有功能依然还要使用而非为新功能所替代,则更不能轻易修改原有代码了。119页

        比如―个商场收银系统,原本提供了商品打折功能,可以按照商品折扣计算付款金额。随后,商场为了吸引更多的顾客,又提供了ⅥP功能,即ⅥP顾客在购买商品时可以打9折。系统顺利上线了,但问题很快就来了,如果一个VIP用户购买了打折商品应当怎样计算呢?非常遗憾,系统反馈说要么按打折商品计算,要么按VIP计算。

        新设计我们增加了一个新的实现类MixStrategy,在MixStrategy中,它有一个属性变量stategies,将PaymentStrategy接口下的其他所有实现都包含在里面。当客户程序要执行payOff()方法时,MXStrategy就依次执行它所包含的其他策略的payOff方法,最终计算出付款金额。120页

        打折商品支付实质上是在普通商品支付的基础上按照折扣打折,VIP商品支付实质上是在普通商品支付的基础上按照顾客ⅥP等级计算折扣,那么既打折又是VIP是什么意思呢?既打折又是ⅥP,实质就是在普通商品支付的基础上先按照折扣打折,然后再在打折的基础上按照顾客VIP等级打折,业务需求描述“在×××基础上做×××”给了一个明显的标示,告诉我们另外一个设计模式装饰者模式可以登场了。
装饰者模式(Decorator)还有个别名叫“包装”)我一直认为“包装"这个别名更能表达它确切的含义,即将原程序包起来,包装一番,使其改头换面重新登场。125页

        与Composite模式―样,装饰者模式也实现了“透明扩展”但与Composite模式的横向扩展不同,装饰者模式更强调一种纵向扩展,即将原程序包上好几层,实现对同一功能的多次扩展,因此装饰者模式被称为Wrapper更确切,装饰者本身也并不是一成不变的,它也可以通过继承形成多个装饰者。

        需要说明的是,装饰者模式长得就跟另个设计模式“代理模式"比较像了,长得像不等于就是一回事儿,装饰者模式是对原有功能的扩展,而代理模式却往往是对原对象实现的访问控制,其理念是不一样的,大家应当注意。127页

第10章第六步:我们开始分层了
10.1 什么才是我们需要的分层
        一般来说,一个大型Wb系统都应当分为这样几个层次:前端界面、MVC层、BUS层DAO层,以及更底层的基础平台层。

10.2 怎样才能拥抱需求的变化
        在一个理想的、易于变更的软件系统中,我们应该通过分层结构将业务层从各个技术层次分离出来:业务层次不再关注Web容器,因为它得到的已经是从容器中读取到数据的值对象;业务层也不再关注数据库,因为它通过注入依赖,已经将数据库的管理与访问交给了数据访问层。业务层只做一件事情,就是完成系统需求所要求的业务逻辑。

        通过以上分层的设计将业务层解耦出来,我们就为业务层应对用户需求的变更提供了所有必要的条件。那么,业务层怎样响应用户需求的变更呢?领域驱动设计的思想给我们指明了方向,那就是将业务层的设计与现实世界(说得专业—点儿叫业务领域)对应起来,用户的业务领域有什么事物,我们的软件世界就有什么对象:业务领域中的事物有哪些特性,软件世界中的对象就有哪些属性;业务领域中的事物执行着哪些行为,软件世界中的对象就拥有哪些方法。也就是说,我们在设计软件时,应当根据业务领域首先分析出领域模型,然后根据领域模型设业务层。132页

10.3 贫血模型与充血模型
        贫血模型个明显的特征是,它仅仅是看上去和领域模型一样,都拥有对象、属性、对象间的关联,但是当你观察模型所持有的业务逻辑时就会发现,贫血模型中除了一些getter和setter方法,几乎没有其他任何业务逻辑。事实上这种情况的出现是因为“贫血模型”的设计规定,不要把业务逻辑放到“领域模型”中,取而代之的是把他们都封装到众多的service中。这些service对象在“领域对象”之上形成一个service层,而仅仅把“领域对象”当做数据对象使用,这就是Martin Fowler对贫血模型的描述。136页

        面向对象设计是一种数据与行为的绑定 ,贫血模型退化了这种绑定,沦为了实质性的面向行为设计。

         什么是充血模型呢?它首先保留了领域模型中的设计,将领域模型直接转变为系统设计中的领域对象,领域对象储存了业务数据,同时又保留了领域模型的设计,通过继承将操作行为与数据绑定在—起从而实现OO的封装。但是,系统中的领域对象只具备了完成业务操作的能力,却不能从系统中接受用户请求,实现人机交互,即系统中必须要有一个对象去触发领域对象的相应业务操作,因此又设计了一堆Service。Service的职责就是接受用户请求,调用领域对象的相应方法。137页

 第17章 系统重构的评价
17.1 评价软件质量的指标 
         评价系统中超级大函数的数量,是我们的第一个评价指标。

         评价遗留系统中大对象的数据,是我们的第二个评价指标,指标名叫NPM。当系统中类的方法超过一定阈值时就可以评价为“大对象”,而我们通过重构降低了系统的大对象数量,则预示着系统的代码质量在提高。226页

         之后,我们检查的是系统代码的复用率。代码复用率高,意味着系统中相同功能被集中至同段代码而反复使用,进而达到系统可维护、易变更的要求。在重构之前,通过静态检查工具分析和发现重复的代码,然后一个一个地通过重构去解决它。随着系统重复代码的逐步减少,系统就开始在不知不觉中得到优化。226页

         再然后就进入代码依赖度的检查中,降低系统耦合度,是所有设计中最重要的一个环节,我们总是在说,要努力构建―个“低耦合、高内聚”的系统:这里的“高内聚”其评判是比较复杂的,需要我们―个―个地具体评判,很难用一些简单易行的指标进行评价(虽然:也有这样的指标)。然而“低耦合”却是可测量的,我们有许多指标可多角度、多方位地评价我们系统的耦合度,进而有针对性地制订重构与改造措施。
我们首先可以引入的一个指标被称之为“圈复杂度”。所谓圈复杂度,用最简单清晰的描述就是,你最少要用多少个测试用例,才能跑遍某个函数的所有程序分支,毫无疑问,我们采用的测试例越多,预示着该函数的业务逻辑越复杂。一个类中 226页

         某个方法的圈杂度,常常意味着该方法存在较高的耦合度,不是优秀的代码。因此评价系统中各个类、各个方法的圈复杂度,对代码质量的评价意义重大。 227页

         另一个评价标准是“传入耦合”,所谓传入耦合,就是―个类被其它多少个类import。与传入耦合相反,传出耦合就是一个类import了多少个其他类。在评价时,传出耦合比较简单,就是检查该类中的import语句条数,而传入耦合的检查比较难,但更有价值,传入耦合需要在整个系统中进行搜索,检查某个类被多少import进去。传入耦合大, 预示着该类的修改对整个系统影响会比较大,需要着重予个关注。
         此外,传出耦合度/(传出耦合度+传入耦合度)=不稳定性。这个公式说明,当―个类使用其他的类越多,被其他的类使用得越少,会更加容易地为其他类的变更所影响。因此,我们在设计时,不应当让个类对其他类有太多的依赖。如果依赖度越高,则意味着它越不容易为其他类所复用,自己却越容易为其他类所影响。 228页

17.2 怎样评价软件系统的质量
要分析软件质量的评价指标,一个比较全面专业的工具是ckjm,网址:http://www.spinellis.gr/sw/ckjm/
ckjm将系统中的每个类,按照8个指标进行评价,WMC指标代表了总圈复杂度。
还有一个重要指标是CBO,它实质就是传出耦合Ce,它代表的是该类import了多少个类。过多地引入其它类,意味着过于依赖其他类。这将使其依赖度过高,从而难于维护与变更,因而无法在其它场景中复用。总之如果该指标高的类,我们就需要考虑对它们的拆分与解耦了。
还有Ca,即传入耦合。传入耦合高意味着该类被其它程序大量使用,代码复用度高。传入耦合高从一个侧面反映了系统设计比较合理,易于维护与变更。传入耦合高的类进行改造,对系统的优化作用明显。
Ce与Ca的组合Ce/(Ce+Ca)代表了类的不稳定性,引用别人多,而别人使用自己少,预示着它在系统的维护过程中易于受到其它因素的影响。
另外,DIT与NOC高都预示着继承的泛滥,但它们不是绝对的,只是给我们提了个醒,到底是不是,还需要具体去分析。

另一个软件iPlasma可以让我们的指标与行业标准进行比较。它是一款开源,分析面向对象代码设计质量的评价软件。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值