A.2 极简,只公开对外承诺的

假设你要在金融中介系统中设计一个抽象,它的功能是根据设定的一组价格类型,对外公布某种金融票据的不同类型的价格。市场中交易的每一种票据都有好几种价格,比如开盘价、收盘价、当前市价,等等。抽象应该有一个publish方法,可以给它指定一种票据和一个价格类型的列表作为参数。该方法返回一个Map,其中的是价格类型,而是给定票据的相应价格。我们首先会写出这样的Java程序:

  1. class InstrumentPricePublisher {
  2. public HashMap<PRICE_TYPE, BigDecimal> publish(Instrument ins,
  3. List<PRICE_TYPE> types) {
  4. HashMap<PRICE_TYPE, BigDecimal> m =
  5. new HashMap<PRICE_TYPE, BigDecimal>();
  6. //.. →❶ 填充HashMap
  7. return m;
  8. }
  9. }

你的本意是让publish方法返回一个Map,内含给定票据的价格类型和对应价格。但上面的实现返回了HashMap,它是方法内部用来存储数据的一个特化的Map抽象。这个特化的抽象把内部的实现暴露给了客户。返回HashMap ❶成了公开的契约,那么客户代码就和HashMap耦合起来了。

假如后来需要把内部的数据结构换成TreeMap,该怎么办呢?没办法,那样做会破坏已有的客户代码。所以抽象就失去了演化的能力。怎么避免这样的问题呢?

A.2.1 用泛化来保留演化余地

马后炮总是很响,眼前就是这么个情况。当初的抽象设计应该在满足契约承诺的前提下,返回最宽泛的类型。契约原本承诺的是返回一个Map,一种支持键值对和查找策略的数据类型。所以,下面才是正确的写法,尽量不暴露内部实现,并且把返回类型调整到恰当的层次:

  1. class InstrumentPricePublisher {
  2. public Map<PRICE_TYPE, BigDecimal> publish(Instrument ins,
  3. List<PRICE_TYPE> types) {
  4. Map<PRICE_TYPE, BigDecimal> m = Collections.emptyMap();
  5. for(PRICE_TYPE type : types) {
  6. m.put(type, getPrice(sec, type));
  7. }
  8. return m;
  9. }
  10. }

看过具体的例子后,你觉得哪些关键症状可以让你察觉抽象违反了最少公开原则?下面,我们来探讨一些有助于诊断的基本概念。

A.2.2 用子类型化防止实现的泄露

我们都知道,面向对象方法用继承手段来对抽象的共性和变异性建模。在基底抽象中定义的行为,可以被下级抽象用覆盖的方式进行细化。在由此产生的层级结构中,越靠近叶子部分,抽象就越精细,公开的契约也更为特化。用继承的概念来对子类型化建模,正好表现出抽象逐级细化的情形。子类型化,顾名思义,子类型的契约必须从父类型那里继承得来,但仅限于继承契约,契约的具体实现是子类型自己的事情,图A-1演示这个概念。

enter image description here

图A-1 通过接口继承完成子类型化。子类型FixedIncomeEquity仅从父类型Instrument继承其接口,然后自行提供实现

较为特化的类型被称为一般化类型的子类型。子类型化并不表示上下级的类型会共用同一个实现。基于“类”(class)的面向对象语言,如Java和C#通过接口继承[2]来完成子类型化。可是在大多数基于类的面向对象语言中,“子类型化”(subtyping)往往被当做“子类化”(subclassing)的同义词,并因此导致混乱的语义和脆弱的抽象层级结构。如果使用得当,接口继承是一件有力的工具,可以在有亲缘关系的抽象族内建立牢靠的类型层次结构。如果单纯通过子类型化手段扩展抽象,绝无泄露实现之虞,同时得到的抽象是极简特质的最佳范本。

通过继承来对抽象的行为进行细化,很难避免在上下级抽象之间共享实现的情况,这种情况一般称为实现继承

A.2.3 正确实施实现继承

如果能正确实施,实现继承是一种非常有用的技巧。但这种技巧很容易被滥用,一不小心,子类就随意地与基类实现产生耦合。图A-2演示了这种状况。

enter image description here

图A-2 实现继承产生的耦合。FixedIncomeEquityissue()方法重用了基类的实现

图中的做法使子类也成了基类的客户,而且基类的实现被泄露到子类。这就产生了OO建模中称为脆弱基类的问题,对基类实现的任何修改,都有可能打破子类的契约,因而使抽象的演化成为近乎不可能的任务。这个例子的情况和前面的InstrumentPricePublisher相似,根本问题就是基础抽象把实现公开给客户。

从本节的例子可以总结出一个重要原则:只公开有必要的部分,并且只向有必要的对象公开。不遵从此建议,就有曝光过度、实现泄露的危险,也违背极简原则。