原文:【《代码整洁之道》精读与演绎】之五 整洁类的书写准则-CSDN博客
这篇文章将与大家一起聊一聊,书写整洁类的一些法则。
一、系列文章前言
敲完上面这段文字的时候,心里在想,一个刚踏入编程生涯的新人,要经历多少的淬炼,才能领略到 Bob 大叔所谓的编程的真谛。
有人说,这个过程会很漫长,大概至少是在读完 N 本编程领域的经典著作,并经过大量的思考与实践之后。
而写这个系列的起因,正是因为最近闲暇时一直在阅读一些之前一直想看的经典著作,并有将阅读过程中一些思考和总结记录下来。为了不枉费这些阅读、思考与总结的过程,决定将这些零散的内容整理成文,沉淀下来。过些年后再回首,也许会觉得当时的一些思考,弥足珍贵。
这个系列的文章,不仅仅是读书笔记,而是对一本书核心内容的全新演绎,内容解刨,与重组。希望自己的这些文字,能对各位想进一步了解这些经典著作的读者们有所帮助。
三、整洁类的书写准则
合理地分布类中的代码
一般情况下,我们遵循变量列表在前,函数在后的原则。
类应该从一组变量列表开始。若有公有静态常量,应该最先出现,然后是私有静态变量,以及公有变量,私有变量。尽可能少的出现公有变量。
公共函数应该出现在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧跟在公共函数后面。
这样是符合自顶向下的原则,让程序读起来像一篇报纸文章。
尽可能保持类的封装
我们喜欢保持变量和工具函数的私有性,但不执著于此。有时,我们需要用到 protected
变量或者工具,比如让测试可以访问到。然而,我们会尽可能使函数或变量保持私有,不对外暴露太多细节。放松封装,总是下策。
3. 类应该短小
正如之前关于函数书写的论调。类的一条规则是短小,第二条规则还是要短小。
和函数一样,马上有个问题要出现,那就是,多小合适呢?
对于函数,我们通过计算代码行数来衡量大小,对于类,我们采用不同的衡量方法,那就是权责(responsibility)。
单一权责原则
单一权责(Single Responsibility Principle,SRP)认为,类或模块应有且只有一条加以修改的理由。
举个栗子,下面这个类足够短小了吗?
public class SuperDashboard extends JFrameimplements MetaDataUser
{
public Component getLastFocusedComponent ()
public void setLastFocused (Component lastFocused)
public int getMajorVersionNumber ()
public int getMinorVersionNumber ()
public int getBuildNumber ()
}
答案是否定的,这个类不够“短小”。5 个方法不算多,但是这个类虽方法少,但还是拥有太多权责。这个貌似很小的 SuperDashboard
类,却有两条关联度并不大的加以修改的理由:
第一,它跟踪会随着软件每次发布而更新的版本信息(含有 getMajorVersionNumber
等方法)。
第二,它还在管理组件(含有 getLastFocusedComponent
方法)。
其实,鉴别权责(修改的理由)常常帮助我们在代码中认识到并创建出更好的抽象。
我们可以轻易地将 SuperDashboard 拆解到名为 Version 的类中,而这个名为 Version 的类,极可能在其他应用程序中得到复用:
public class Version
{
public int getMajorVersionNumber ()
public int getMinorVersionNumber ()
public int getBuildNumber ()
}
这样,这个类就大致做到了单一权责。
4. 合理提高类的内聚性
我们希望类的内聚性保持在较高的水平。
类应该只有少量的实体变量,类中的每个方法都应该操作一个或者多个这种变量。通常而言,如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。一般来说,创建这种极大化的内聚类不可取,也不可能。
我们只希望内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。
举个高内聚的例子:
public class Stack
{
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
public int size ()
{
return topOfStack;
}
public void push (int element)
{
topOfStack++;
elements.add (element);
}
public int pop () throws PoppedWhenEmpty
{
if (topOfStack == 0)
throw new PoppedWhenEmpty ();
int element = elements.get (--topOfStack);
elements.remove (topOfStack);
return element;
}
}
这个类非常内聚,在三个方法中,仅有 size ()方法没有使用所有的两个变量。
注意,保持函数和参数短小的策略,有时候会导致为一组子集方法所用的实体变量增加。我们应该尝试将这些方法拆分到两个或者多个类中,让新的类更为内聚。
5. 有效地隔离修改
需求会改变,所以代码也会改变。在面向对象入门知识中我们学习到,具体类包含实现细节(代码),而抽象类则呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。
举个栗子,在一个设计场景下,我们与其设计直接依赖于 TokyoStockExchange
的 Protfolio
类,不如创建 StockExchange
接口,里面只声明一个方法:
public interface StockExchange
{
MoneycurrentPrice (String symbol);
}
接着设计 TokyoStockExchange
类来实现这个接口:
public class TokyoStockExchange extends StockExchange
{
//…
}
我们还要确保 Portfolio
的构造器接受作为参数 StickExchange
引用:
public Portfolio
{
private StockExchange exchange;
public Portfolio (StockExchange exchange)
{
this. exchange = exchange;
}
// ...
}
那么现在就可以为 StockExchange 接口创建可以测试的实现了,例如返回固定的股票现值。比如测试购买 5 股微软股票,我们下面的实现代码返回 100 美元的现值,然后再实现一个总投资价值为 500 美元的测试,那么大概代码则是:
public class PortfolioTest
{
privateFixedStockExchangeStub exchange;
privatePortfolio portfolio;
@Before
protected void setUp () throws Exception
{
exchange = new FixedStockExchangeStub ();
exchange.fix ("MSFT", 100);
portfolio = new Portfolio (exchange);
}
@Test
public void GivenFiveMSFTTotalShouldBe 500 () throws Exception
{
portfolio.add (5, "MSFT");
Assert.assertEquals (500,portfolio.value ());
}
}
如果系统解耦到足以这样测试的程度,也就更加灵活,更加可复用。部件之间的解耦代表着系统中的元素相互隔离得很好。隔离也让对系统每个元素的理解变得更加容易。
我们的 Portfolio 类不再是依赖于 TokyoStockExchange 类的实现细节,而是依赖于 StockExchange 接口这个抽象的概念,这样就隔离了特定的细节。而其实我们的类就遵循了另一条类的设计原则,依赖倒置原则(Dependency Inversion Principle , DIP),因为依赖倒置原则的本质,实际上就是认为类应该依赖于抽象,而不是依赖于具体细节。
四、一些思考与总结
让软件能够保持工作和让软件代码整洁,是两种截然不同的工作。我们中大多数人脑力有限,只能更多把更多精力放在让代码能够工作上,而不是放在保持代码有组织和整洁上。
问题是太多人在程序能够正常工作时就以为万事大吉了。我们没能把思维转向有关代码组织和整洁的部分,我们只是一直在做新的需求,而不是回头将臃肿的类切分为只有单一权责的去耦式单元。
与此同时,许多开发者害怕数量巨大的短小单一目的的类会导致难以一目了然抓住全局。他们认为,要搞清楚一件较大的工作如果完成,就得在类与类之间找来找去。其实,有大量短小的类的系统并不比有少量庞大类的系统更难掌控。问题是:你是想把工具归置于有许多抽屉、每个抽屉中装有定义和标记的良好组件的工具箱中呢,还是想要少数几个能随便把所有东西都扔进去的抽屉呢?大概我们都更趋向于选择前者。
每个达到一定规模的系统都包含大量逻辑和复杂性。管理这种复杂性的首要目标就是加以组织,以便开发者能知道在哪里找到需要的内容,专注于当下工作直接相关的具体模块。反之,拥有巨大、多目的类的系统,总是让我们在目前并不需要了解的一大堆东西中艰难跋涉。
最终再强调一下:系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
五、本文涉及知识点提炼整理
原则一:合理地分布类中的代码。类中代码的分布顺序大致是:
- 公有静态常量
- 私有静态变量
- 公有普通变量
- 私有普通变量
- 公共函数
- 私有函数
原则二:尽可能地保持类的封装。尽可能使函数或变量保持私有,不对外暴露太多细节。
原则三:类应该短小,尽量保持单一权责原则。类或模块应有且只有一条加以修改的理由。
原则四:合理提高类的内聚性。我们希望类的内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。
原则五:有效地隔离修改。类应该依赖于抽象,而不是依赖于具体细节。尽量对设计解耦,做好系统中的元素的相互隔离,做到更加灵活与可复用。
本文就此结束。
至此,《代码整洁之道》的精读与演绎系列文章,已经完结。
With Best Wishes.