1.0整洁类的书写准则
1.1 合理地分布类中的代码
一般情况下,我们遵循变量列表在前,函数在后的原则。
类应该从一组变量列表开始。若有公有静态常量,应该最先出现,然后是私有静态变量,以及公有变量,私有变量。尽可能少的出现公有变量。
公共函数应该出现在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧跟在公共函数后面。
这样是符合自定向下的原则,让程序读起来像一篇报纸文章。
1.2 尽可能保持类的封装
我们喜欢保持变量和工具函数的私有性,但不执著于此。有时,我们需要用到protected变量或者工具,比如让测试可以访问到。然而,我们会尽可能使函数或变量保持私有,不对外暴露太多细节。放松封装,总是下策。
1.3 类应该短小
正如之前关于函数书写的论调。类的一条规则是短小,第二条规则还是要短小。
和函数一样,马上有个问题要出现,那就是,多小合适呢?
对于函数,我们通过计算代码行数来衡量大小,对于类,我们采用不同的衡量方法,那就是权责(responsibility)。
1.3.1 单一权责原则
单一权责(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()
}
这样,这个类就大致做到了单一权责。
1.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()方法没有使用所有的两个变量。
注意,保持函数和参数短小的策略,有时候会导致为一组子集方法所用的实体变量增加。我们应该尝试将这些方法拆分到两个或者多个类中,让新的类更为内聚。
1.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 GivenFiveMSFTTotalShouldBe500() throws Exception
{
portfolio.add(5, "MSFT");
Assert.assertEquals(500,portfolio.value());
}
}
如果系统解耦到足以这样测试的程度,也就更加灵活,更加可复用。部件之间的解耦代表着系统中的元素相互隔离得很好。隔离也让对系统每个元素的理解变得更加容易。
我们的Portfolio类不再是依赖于TokyoStockExchange类的实现细节,而是依赖于StockExchange接口这个抽象的概念,这样就隔离了特定的细节。而其实我们的类就遵循了另一条类的设计原则,依赖倒置原则(Dependency Inversion Principle , DIP),因为依赖倒置原则的本质,实际上就是认为类应该依赖于抽象,而不是依赖于具体细节。
2.0总结
- 合理地分布类中的代码: 类中代码的分布顺序大致是:
<1> 公有静态常量
<2> 私有静态变量
<3> 公有普通变量
<4> 私有普通变量
<5> 公共函数
<6> 私有函数 - 尽可能地保持类的封装: 尽可能使函数或变量保持私有,不对外暴露太多细节。
- 类应该短小,尽量保持单一权责原则: 类或模块应有且只有一条加以修改的理由。
- 合理提高类的内聚性: 我们希望类的内聚性保持在较高的水平。内聚性高,表示类中方法和变量相互依赖,相互结合成一个逻辑整体。
- 有效地隔离修改: 类应该依赖于抽象,而不是依赖于具体细节。尽量对设计解耦,做好系统中的元素的相互隔离,做到更加灵活与可复用。