重构--改善既有代码的设计读书笔记 第三章(3)

重构--改善既有代码的设计读书笔记 第三章(3)

其实在看Martin Fowler的重构这本书的很长一段时间里,我一直把它错当成了另外一本书的中文版,那本书的名字就是:John.Wiley.and.Sons.Refactoring.in.Large.Software.Projects.Performing.Complex.Restructurings.Successfully.Jun.2006
以前曾经看过这本书,目前还没有找到中文版。相对于《重构--改善既有代码的设计》的侧重实践来说,上面这本书理论性更强一些。看讲实践的书有个缺点,就是你看了不上手做一做的话,等于白看,而理论呢,你可以记住。所以下面穿插一些《Refactoring.in.Large.Software.Projects.Performing.Complex.Restructurings.Successfully》的理论,有很多通过英文字面意思很难理解,这样我会在网上找些资料,然后抄记在下面:
设计原理
        正如代码会“有味”一样,结构也会“有味”,他们通常会由对于设计原理的违反而引起。因此设计原理会为我们提供一些有价值的东西进而帮助我们为设计去去狐臭。在我们从设计中找出违反这些设计原理的地方的同时,我们也有了自己的想法来改善我们的设计。。下表列出了当前流行的设计原理:

1、DRY(Don't Repeat Yourself):不要写重复的代码。也可以叫做“一次且仅一次(Once and Only Once)”原理。
2、SCP(Speaking Code Principle):代码应该清楚地表明它自己的意图。代码中出现注释,说明代码不能清楚地表现自己的意图。
3、OCP(Open Closed Principle):一个设计单元应该向修改开放。这种修改不应该导致调用方无效。继承是一种能够让你达到此目的的方法。子类可以进行一些调整,而超类保持不变。
4、LSP(Liskov Substitution Principle):个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。反过来代换是不成立的
5、DIP(Dependency Inversion Principle):等级高的概念不应该依赖于等级低的概念(或实现)。对于从属关系来说刚好相反。因为等级高的概念比起等级低的概念来说变动的可能性要小。
6、ISP(Interface Segregation Principle):接口隔离原理。接口应该尽量的小。他们应该仅包含很少的方法,但是这些被放置在同一个接口中的方法应该是紧密相关的。
7、REP(Reuse/Release Equivalency Principle):可重用的元素必然是已经被发布的元素。

8、 CRP(Common Closure Principle): 包中的所有类对于同一类性质的变化应该是共同封闭的。 一个变化若对一个包产生影响,则将对该报的所有类产生影响。而对其他的包不构成任何影响。
9、 ADP(Acyclic(无环的)Dependencies Principle):包之间的依赖关系应当是无环的。
10、 SDP(Stable Dependencies Principle):一个包应该依赖于至少像它自己那样稳定的包。
11、 SAP(Stable Abstractions Principle):越稳定的包,其抽象程度越高。不稳定的包通常是具体化的包。
12、 TDA(Tell,Don't Ask):不要向一个对象去申请另外一个对象,而应该告诉这个对象应该如何去做。
13、 SOC(Separation Of Concerns):不要在一个类中包含多个关注点。这通常也被叫做“责任单一原理”。
类关系图中的“狐臭”
两个或多个类之间的关系通常包含“使用”和“继承”两种。如果我们想在一个系统中找出“使用”关系,我们应该看一下“静态”关系图。在系统运行时呈现出来的“使用”关系构成了对象之间的动态关系图。
一、无用类
重构--改善既有代码的设计读书笔记 第三章(3)_第1张图片
出自Refactoring.in.Large.Software.Projects.Performing.Complex.Restructurings.Successfully
那些不再使用的类会因为具有一些显而易见的无用功能而增加系统的负担。这些负担包括:程序开发者需要花费大量的时间来分辨出一个正确的类,构建系统的时间变得更长,系统变得更加难以理解。
相对于完全无用的类,更多的情况下你会遇到包含部分无用代码的类。这是因为一个类中包含了太多的功能,而在真正使用的时候,那些功能又被抛弃掉了。我们可以在重构的时候依据这些功能把类进行拆分,通过拆分我们会首先得到无用类。然后进一步对它进行处理。
不仅仅单单一个类会成为无用类,有的时候,处在同一个关系图中的类都回成为无用类(s)。
无用类通常因为两种原因而出现:
1、技术上提供覆盖面尽可能广的支持:一个开发者推测一个类可能最终会被用到,即使没有迹象表明现实中有与之对应的需求。
2、重构:通过对系统的修改,以前需要用到的类,现在变得陈旧过期了。
二、树型依赖关系
重构--改善既有代码的设计读书笔记 第三章(3)_第2张图片
出自Refactoring.in.Large.Software.Projects.Performing.Complex.Restructurings.Successfully
树型依赖关系图体现了对于系统的一种分解。树中的每一个类都确定的被另外一个类使用。
鉴于在面向对象的应用程序中,功能分解本身通常会被看作是一种“狐臭”,一种树型依赖结构像重复代码一样也应该被看作是一种“狐臭”。在图3-6中,类Protocol除了被类Data Storage使用外,不再被别的地方使用。重用没有发生。
三、静态环状依赖关系
重构--改善既有代码的设计读书笔记 第三章(3)_第3张图片
出自Refactoring.in.Large.Software.Projects.Performing.Complex.Restructurings.Successfully
两个类彼此使用对方是最简单的环状依赖关系。环状依赖关系中也可以包含多个类。
环状依赖关系会使环本身变得更加臃肿。环所具有的消极影响包括:
a、不可理解性:这些类不能够被一个又一个的单独理解,因为它们彼此依靠对方来实现功能。又或者是,一个类需要从多个类中选择一个作为理解他们本身的前提。
b、可维护性:环状依赖可能包含严重的并且不可预测的递推关系,进而导致修改包含它的系统变得非常困难。
c、可计划性:对环的修改到底会造成什么影响是很难预期的。估算到底需要多少的工作来完成这个修改会是一件很困难的事。
d、设计的清洁性:因为处在环中的那些类会直接或间接的访问到处在环中的任意一个类,因此,从理论上讲,类中的这种关系太随意了。如果在这个环中又恰好把一个方法放到错误的类中,会导致理解这种设计变得更加困难。
e、重用性:这些类必须同时重用。如果在一个给定的语境中,只有其中的一个类所提供的功能被关注,因为处在环中,这个类不能被简单的剥离并使用。
f、可测试性:这些类必须一起被测试。这增加了测试的要求以及堪错的难度。如果想要独立的测试其中的一个类,就必须使用“伪对象”。
g、异常处理:通常异常会在环中被“堆积”。如果环中的一个方法抛出了异常,他会潜在得影响环中的其他方法。
h、依赖引入问题:每一个处于环中的类同时又依赖于另一个处于环中的类处在环外的依赖关系。
显然,环中的最大长度越大,代码的“狐臭”味就越浓。然而,在有些地方,只包含两个类的环却是一种被推崇的设计方案,被应用于很多设计模式中(如迭代器模式)。
四、显式依赖
面向对象支持封装原理以及信息隐藏原理:内部的实现被隐藏于接口之中。客户端的代码无须知道任何有关于API的实现。另外,接口与实现类之间存在一些“裂痕”。很多开发者相信封装和信息隐藏只会在成员变量被定义为“私有”的时候才会有效。这当然不是事实:在很多系统中都是支持“属性”的,并且属性可以被继承。然而,有这样一个事实,在一个系统中,显式依赖关系却是无法隐藏的。一个具有公共依赖关系的系统在被修改的时候,总会产生这样那样的问题,反之,私有的局部的依赖关系只会有小范围的影响。
TDA(Tell,don't ask)原理指出了一条正确的途径:理想情况下,客户端告诉对象自己打算做什么。既不要通过已有对象获取一个新的对象,更不要用那个新的对象进行一些操作。
下面看一个例子。在这个例子中,“订单”有很多的状态。我们可以简单的以打开的订单和关闭的订单来简单的区分一下。打开的订单就是客户未回款的订单。现在我们要写一些代码,来计算所有打开的订单的总额。
如果我们使用一个方法calculateValueOpenOrders(注意,这个方法是写在调用方的,我们的调用方的类名定为Foo),而方法体如下。可以看出这个方法违反了TDA原理:
public   float  calculateValueOpenOrders(ListOfOrders orders) 
{
    
float  totalValue  =   0.0f ;
    
for  ( int  i = 0 ; i < orders.getNumber(); i ++
    {
        Order a 
=  orders.getOrder(i);
        
if  (a.isOpen) 
        {
            totalValue 
+=  a.getValue();
        }
    }
    
return  totalValue;
}
原因就是客户端的Foo直接使用Orders来获取其中的一些信息,而不是告诉Orders我要做什么。
重构--改善既有代码的设计读书笔记 第三章(3)_第4张图片
出自Refactoring.in.Large.Software.Projects.Performing.Complex.Restructurings.Successfully
现在我们做一下修改,把打开的订单的金额计算移动到订单内部进行。如果订单是打开的,返回本订单的金额,反之,返回0。
public   class  ListOfOrders 
{
    
public   float  calculateValueOpenOrders() 
    {
        
float  totalValue  =   0.0f ;
        
for  ( int  i = 0 ; i < getNumber(); i ++
        {
            Order a 
=  getOrder(i);
            totalValue 
+=  a.getOpenValue();
        }
        
return  totalValue;
    }
}
     
public   class  Order 
{
    
public   float  getOpenValue() 
    {
        
if  (isOpen()) 
        {
            
return  getValue();
        }
        
else  
        {
            
return   0 ;
        }
    }
}
我们可能会因为所有Order都要返回打开的订单的金额而感觉不爽。如果你打算多次使用TDA原理,你可能通过增加一个方法“addOpenValue”来增加类order的柔性。但是,同时这也意味着类Order会知道另外的一些Order的存在。在这种情况下,我们可能会违反SOC(即单一职责原理)。我们不应该忽略这样一点:在使用一些设计原理的时候,还要兼顾系统的平衡性。在这个例子中,这个方法是否应该加入取决于它是否真的与这个类的“领域模型”所匹配。
新的实现方式不仅仅使代码更加简短,而且还包括如下的优点:
功能有了正确的归属。在大多数情况下,上面第一段代码中的方法calculateValueOpenOrders通常是被放到UI类中或者是一些Helper类中,而这些类通常有一些奇异的名字(比如OpenOrders Calculator)。这对于这个方法来说,不是一个正确的归属。
TDA原理确保类的使用应该局部化,而不要散步到系统的各个角落。因此可以简化一些优化方案的实现。

你可能感兴趣的:(重构--改善既有代码的设计读书笔记 第三章(3))