《代码整洁之道》第 10 章 类

第 10 章 类

10.1 类的组织

遵循标准的 Java 约定,类应该从一组变量列表开始。如果有公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量。

公共函数应跟在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧随在该公共函数后面。这符合了自顶向下原则,让程序读起来就像一篇报纸文章。

封装

我们喜欢保持变量和工具函数的私有性,但并不执着于此。有时,我们也需要用到受护 (protected) 变量或工具函数,好让测试可以访问到。对我们来说,测试说了算。若同一程序包内的某个测试需要调用一个函数或变量,我们就会将该函数或变量置为受护或在整个程序包内可访问。然而,我们首先会想办法使之保有隐私。放松封装总是下策。

10.2 类应该短小

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

类的名称应当描述其权责。实际上,命名正是帮助判断类的长度的第一个手段。如果无法为某个类命以精确的名称,这个类大概就太长了。类名越含混,该类越有可能拥有过多权责。例如,如果类名中包括含义模糊的词,如 Processor 或 Manager 或 Super,这种现象往往说明有不恰当的权责聚集情况存在。

10.2.1 单一权责原则

单一权责原则(SRP)认为,类或模块应有且只有一条加以修改的理由。该原则既给出了权责的定义,又是关于类的长度的指导方针。类只应有一个权责——只有 一条修改的理由。

鉴别权责(修改的理由)常常帮助我们在代码中认识到并创建出更好的抽象。可以轻易地将全部三个处理版本信息的SuperDashboard方法拆解到名为Version的类中(如代码清单 10-3 所示)。Version 类是个极有可能在其他应用程序中得到复用的构造

// 代码清单10-3单一权责类
public class Version {
	public int getMajorVersionNumber();
	public int getMinorVersionNumber();
	public int getBuildNumber();
}

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

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

每个达到一定规模的系统都会包括大量逻辑和复杂性。管理这种复杂性的首要目标就是加以组织,以便开发者知道到哪儿能找到东西,并且在某个特定时间只需要理解直接有关的复杂性。反之,拥有巨大、多目的类的系统,总是让我们在目前并不需要了解的一大堆东西中艰难跋涉。

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

10.2.2 内聚

类应该只有少量实体变量。类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性

一般来说,创建这种极大化内聚类是既不可取也不可能的;另一方面,我们希望内聚性保持在较高位置。内聚性高,意味着类中的方法和变量互相依赖、互相结合成一个逻辑整体。

看看代码清单 10-4 中一个 Stack 类的实现方式。这个类非常内聚。在三个方法中,只有 size( ) 方法没有使用所有两个变量

// 代码清单 10-4 Stack.java (一个内聚类)
public class Stack {
  private int top0fStack = 0;
  List<Integer> elements = new LinkedList<Integer>() ;
  
  pub1ic 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(--top0fStack); 
    elements.remove(topOfStack);
    return element;
 	}                                   
}

10.2.3 保持内聚性就会的到许多短小的类

……

10.3 为了修改而组织

对于多数系统,修改将一直持续。 每处修改都让我们冒着系统其他部分不能如期望般工作的风险。在整洁的系统中,我们对类加以组织,以降低修改的风险。.

代码清单 10-9 中的 Sql 类用来生成提供恰当元数据的 SQL 格式化字符串。这个类还没写完,所以暂时不支持 update 语句等 SQL 功能。当需要 Sql 类支持 update 语句时,我们就得“打开”这个类进行修改。打开类带来的问题是风险随之而来。对类的任何修改都有可能破坏类中的其他代码。必须全面重新测试。

// 代码清单10-9
// 一个必须打开修改的类
public class Sql {
	public Sql(String table, Column[] columns)
	public String create()
  public String insert(0bject[] fields)
  public String selectAll()
  public String findByKey(String keyColumn, String keyValue)
  public String select(Column column, String pattern)
  public String select(Criteria criteria)
  public String preparedInsert()
  private String columnList(Column[] columns)
  private String valuesList(Object[] fields, final Column[] columns)
  private String selectWithCriteria(String criteria)
  private String placeholderList (Column[] colunns)
}

当增加一种新语句类型时,就要修改 Sql 类。改动单个语句类型时,也要进行修改,比如打算让 select 功能支持子查询。存在两个修改的理由,说明 Sql 违反了SRP 原则。

可以从一条简单的组织性观点发现对 SRP 的违反。Sql 的方法大纲显示,存在类似 selectWithCriteria 等只与 select 语句有关的私有方法。

……

代码清单 10-10 中的解决方式如何呢? 代码清单 10-9 中 Sql 类的每个接口方法都重构到从 Sql 类派生出来的类中了注意那些私有方法,如 valuesList,直接移到了需要用它们的地方。公共私有行为被划分到独立的两个工具类 Where 和 ColumnList 中。

// 代码清单 10-10 一组封闭类
abstract public class Sql {
	public Sql(String table, Column[] columns)
	abstract public String generate()
}

public class CreateSql extends Sql {
	public CreateSql(String table, Column[] columns)
	@Override public String generate()
}

public class SelectSql extends Sql {
	public SelectSql(String table, Co1umn[] columns)
	@Override public String generate()
}

public class InsertSql extends Sql {
	public InsertSql(String table, Column[] columns, Object[] fields)
	@Override pub1ic String generate()
  private String valuesList(Object[] fields, final Column[] colunns)
}

public class SelectWithCriteriaSql extends Sql {
	public SelectWithCriteriaSql (
		String table, Column[] co1umns, Criteria criteria)
	)
	@Override pub1ic String generate()
}

public class SelectWithMatchSql extends Sql {
	public SelectWithMatchSql(
		String table, Column[] columns, Column column, String pattern)
  )
  @Override public String generate()
}

public class FindByKeySql extends Sq1 {
	public FindByKeySql (
		String table, Column[] columns, String keyColumn, String keyValue)
  )
	@Override public String generate()
}

public class PreparedInsertSql extends Sql {
  public PreparedInsertSq1(String table, Column[] columns)
  @Override pub1ic String generate()
  private String placeholderList(Column[] columns)
}

public class Where {
	public Where(String criteria)
	public String generate()
}

public class ColumnList {
  public ColumnList(Column[] columns)
	public String generate()
}

每个类中的代码都变得极为简单。理解每个类花费的时间缩减到近乎为零。函数对其他函数造成毁坏的风险也变得几近于无。从测试的角度看,验证方案中每一处逻辑都成了极为简单的任务,因为类与类之间相互隔离了。

当需要增加 update 语句时,现存类无需做任何修改,这也同等重要!我们在 Sql 类的新子类 UpdateSql 中构建 update 语句的逻辑。系统中的其他代码都不会因为这个修改而被破坏。

重新架构的 Sql 逻辑百利而无一弊。它支持 SRP。它也支持其他面向对象设计的关键原则,如开放-闭合原则(OCP): 类应当对扩展开放,对修改封闭。通过子类化手段,重新架构的 Sql 类对添加新功能是开放的,而且可以同时不触及其他类。只要将 UpdateSql 类放置到位就行了

我们希望将系统打造成在添加或修改特性时尽可能少惹麻烦的架子。在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性。

posted @ 2023-08-28 14:01  CoolGin  阅读(26)  评论(0编辑  收藏  举报