关闭

【《代码整洁之道》精读与演绎】之五 整洁类的书写准则

101人阅读 评论(0) 收藏 举报
分类:

文章链接: http://blog.csdn.net/poem_qianmo/article/details/52344732
作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442

这篇文章将与大家一起聊一聊,书写整洁类的一些法则。

 

一、引言



以下引言的内容,有必要伴随这个系列的每一次更新,这次也不例外。

 

《代码整洁之道》这本书提出了一个观点:代码质量与其整洁度成正比,干净的代码,既在质量上可靠,也为后期维护、升级奠定了良好基础。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

 

但我们知道,很多时候,理想很丰满,现实很骨感,也知道人在江湖,身不由己。因为项目的紧迫性,需求的多样性,我们无法时时刻刻都写出整洁的代码,保持自己输出的都是高质量、优雅的代码。

 

但若我们理解了代码整洁之道的精髓,我们会知道怎样让自己的代码更加优雅、整洁、易读、易扩展,知道真正整洁的代码应该是怎么样的,也许就会渐渐养成持续输出整洁代码的习惯。

 

而且或许你会发现,若你一直保持输出整洁代码的习惯,长期来看,会让你的整体效率和代码质量大大提升。

 



 

二、本文涉及知识点思维导图

 

 

国际惯例,先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。




 


 

三、整洁类的书写准则

 

 


合理地分布类中的代码


一般情况下,我们遵循变量列表在前,函数在后的原则。

类应该从一组变量列表开始。若有公有静态常量,应该最先出现,然后是私有静态变量,以及公有变量,私有变量。尽可能少的出现公有变量。

公共函数应该出现在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧跟在公共函数后面。

这样是符合自定向下的原则,让程序读起来像一篇报纸文章。


尽可能保持类的封装

 

我们喜欢保持变量和工具函数的私有性,但不执著于此。有时,我们需要用到protected变量或者工具,比如让测试可以访问到。然而,我们会尽可能使函数或变量保持私有,不对外暴露太多细节。放松封装,总是下策。

 

 

类应该短小

 

正如之前关于函数书写的论调。类的一条规则是短小,第二条规则还是要短小。

和函数一样,马上有个问题要出现,那就是,多小合适呢?

对于函数,我们通过计算代码行数来衡量大小,对于类,我们采用不同的衡量方法,那就是权责(responsibility)。


3.1 单一权责原则


单一权责(Single Responsibility Principle,SRP)认为,类或模块应有且只有一条加以修改的理由。

举个栗子,下面这个类足够短小了吗?

  1. public class SuperDashboard extends JFrameimplements MetaDataUser  
  2. {  
  3.     public Component getLastFocusedComponent()  
  4.     public void setLastFocused(Component lastFocused)  
  5.     public int getMajorVersionNumber()  
  6.     public int getMinorVersionNumber()  
  7.     public int getBuildNumber()  
  8. }  

答案是否定的,这个类不够“短小”。5个方法不算多,但是这个类虽方法少,但还是拥有太多权责。这个貌似很小的SuperDashboard类,却有两条关联度并不大的加以修改的理由:

第一, 它跟踪会随着软件每次发布而更新的版本信息(含有getMajorVersionNumber等方法)。

第二,它还在管理组件(含有getLastFocusedComponent方法)。

其实,鉴别权责(修改的理由)常常帮助我们在代码中认识到并创建出更好的抽象。

我们可以轻易地将SuperDashboard拆解成名为Version的类中,而这个名为Version的类,极可能在其他应用程序中得到复用:

  1. public class Version  
  2. {  
  3.     public int getMajorVersionNumber()  
  4.     public int getMinorVersionNumber()  
  5.     public int getBuildNumber()  
  6. }  

这样,这个类就大致做到了单一权责。

 

 

合理提高类的内聚性


我们希望类的内聚性保持在较高的水平。

何为类的内聚性?类的内聚性就是类中变量与方法之间的依赖关系。类中方法操作的变量越多,就越黏聚到类上,就代表类的内聚性高。

类应该只有少量的实体变量,类中的每个方法都应该操作一个或者多个这种变量。通常而言,如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。一般来说,创建这种极大化的内聚类不可取,也不可能。

我们只希望内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。

举个高内聚的例子:

  1. public class Stack   
  2. {  
  3.     private int topOfStack = 0;  
  4.     List<Integer> elements = new LinkedList<Integer>();  
  5.   
  6.     public int size()   
  7.     {  
  8.         return topOfStack;  
  9.     }  
  10.   
  11.     public void push(int element)  
  12.     {  
  13.         topOfStack++;  
  14.         elements.add(element);  
  15.     }  
  16.   
  17.     public int pop() throws PoppedWhenEmpty   
  18.     {  
  19.         if (topOfStack == 0)  
  20.             throw new PoppedWhenEmpty();  
  21.         int element = elements.get(--topOfStack);  
  22.         elements.remove(topOfStack);  
  23.             return element;  
  24.     }  
  25. }  

这个类非常内聚,在三个方法中,仅有size()方法没有使用所有的两个变量。

注意,保持函数和参数短小的策略,有时候会导致为一组子集方法所用的实体变量增加。我们应该尝试将这些方法拆分到两个或者多个类中,让新的类更为内聚。

 



有效地隔离修改


需求会改变,所以代码也会改变。在面向对象入门知识中我们学习到,具体类包含实现细节(代码),而抽象类则呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。

 

举个栗子,在一个设计场景下,我们以其设计直接依赖于TokyoStockExchange的Protfolio类,不如创建StockExchange接口,里面只声明一个方法:

  1. public interface StockExchange  
  2. {  
  3.     MoneycurrentPrice(String symbol);  
  4. }  

接着设计TokyoStockExchange类来实现这个接口:

  1. public class TokyoStockExchange extends StockExchange  
  2. {  
  3.        //…  
  4. }  

我们还要确保Portfolio的构造器接受作为参数StickExchange引用:

  1. public Portfolio  
  2. {  
  3.     private StockExchange exchange;  
  4.     public Portfolio(StockExchange exchange)  
  5.     {  
  6.         this.exchange = exchange;  
  7.     }  
  8.     // ...  
  9. }  

那么现在就可以为StockExchange接口创建可以测试的实现了,例如返回固定的股票现值。比如测试购买5股微软股票,我们下面的实现代码返回100美元的现值,然后再实现一个总投资价值为500美元的测试,那么大概代码则是:

  1. public class PortfolioTest  
  2. {  
  3.     privateFixedStockExchangeStub exchange;  
  4.     privatePortfolio portfolio;  
  5.    
  6.     @Before  
  7.     protected void setUp() throws Exception  
  8.     {  
  9.         exchange = new FixedStockExchangeStub();  
  10.         exchange.fix("MSFT", 100);  
  11.         portfolio = new Portfolio(exchange);  
  12.     }  
  13.    
  14.     @Test  
  15.     public void GivenFiveMSFTTotalShouldBe500() throws Exception  
  16.     {  
  17.         portfolio.add(5, "MSFT");  
  18.         Assert.assertEquals(500,portfolio.value());  
  19.     }  
  20. }  

如果系统解耦到足以这样测试的程度,也就更加灵活,更加可复用。部件之间的解耦代表着系统中的元素相互隔离得很好。隔离也让对系统每个元素的理解变得更加容易。


我们的Portfolio类不再是依赖于TokyoStockExchange类的实现细节,而是依赖于StockExchange接口这个抽象的概念,这样就隔离了特定的细节。而其实我们的类就遵循了另一条类的设计原则,依赖倒置原则(Dependency Inversion Principle , DIP),因为依赖倒置原则的本质,实际上就是认为类应该依赖于抽象,而不是依赖于具体细节。

 


 

 

四、一些思考与总结

 


让软件能够保持工作和让软件代码整洁,是两种截然不同的工作。我们中大多数人脑力有限,只能更多把更多精力放在让代码能够工作上,而不是放在保持代码有组织和整洁上。

问题是太多人在程序能够正常工作时就以为万事大吉了。我们没能把思维转向有关代码组织和整洁的部分,我们只是一直在做新的需求,而不是回头将臃肿的类切分为只有单一权责的去耦式单元。

 

与此同时,许多开发者害怕数量巨大的短小单一目的的类会导致难以一目了然抓住全局。他们认为,要搞清楚一件较大的工作如果完成,就得在类与类之间找来找去。其实,有大量短小的类的系统并不比有少量庞大类的系统更难掌控。问题是:你是想把工具归置于有许多抽屉、每个抽屉中装有定义和标记的良好组件的工具箱中呢,还是想要少数几个能随便把所有东西都扔进去的抽屉呢?大概我们都更趋向于选择前者。

 

每个达到一定规模的系统都包含大量逻辑和复杂性。管理这种复杂性的首要目标就是加以组织,以便开发者能知道在哪里找到需要的内容,专注于当下工作直接相关的具体模块。反之,拥有巨大、多目的类的系统,总是让我们在目前并不需要了解的一大堆东西中艰难跋涉。

 

最终再强调一下:系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

 





五、本文涉及知识点提炼整理

 

 

原则一:合理地分布类中的代码。 类中代码的分布顺序大致是:

1. 公有静态常量

2. 私有静态变量

3. 公有普通变量

4. 私有普通变量

5. 公共函数

6. 私有函数


原则二:尽可能地保持类的封装。尽可能使函数或变量保持私有,不对外暴露太多细节。

原则三:类应该短小,尽量保持单一权责原则。类或模块应有且只有一条加以修改的理由。

原则四:合理提高类的内聚性。我们希望类的内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。

原则五:有效地隔离修改。类应该依赖于抽象,而不是依赖于具体细节。尽量对设计解耦,做好系统中的元素的相互隔离,做到更加灵活与可复用。



本文就此结束。

下篇文章,我们将继续《代码整洁之道》的精读与演绎,探讨更多的内容。

Best Wish~

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:5575次
    • 积分:249
    • 等级:
    • 排名:千里之外
    • 原创:16篇
    • 转载:25篇
    • 译文:0篇
    • 评论:0条
    文章分类