《程序员修炼之道--通向务实的最高境界》笔记--话题28:解耦

162 阅读4分钟

话题28:解耦

耦合是修改之敌,因为它将事情连接在一起,要改就得一起改。

这使得修改变得更加困难:要么需要花上不少时间,弄清楚所有需要修改的地方到底有哪些,要么又会因为“仅仅只修改一处”而没有跟着改与之相耦合的地方,把时间花在想明白为什么会出问题上。

耦合有传递性:如果 A与B、C耦合,B与M、N耦合,C与X、Y耦合,那么A实际上与B、C、M、N、X及Y耦合。

如何解耦?

任何时候,只要两段代码共享点什么东西,都可能发生耦合,但我们需要注意留心一些耦合的“症状”:

  • 不相关的模块或库之间古怪的依赖关系;
  • 对一个模块进行“简单”修改,会传播到系统中不相关的模块里,或是破坏了系统中的其他部分;
  • 开发人员害怕修改代码,因为他们不确定会造成什么影响;
  • 会议要求每个人都必须参加,因为没有人能确定谁会收到变化的影响。

铁道事故--连串的方法调用:

public void applyDiscount(customer, order_id, discount) {
  totals = customer
    .orders
    .find(order_id)
    .getTotals();
  totals.grandTotal = totals.grandTotal - discount;
  totals.discount = discount;
}

上面的代码,首先从customer对象获取到一组订单的引用,然后获取订单总价totals对象。基于totals对象,从总金额减去折扣金额discount,并更新了totals对象的折扣金额。

这段代码跨越了从客户到总金额的五个抽象层次,顶层代码就必须知道如下知识:

  • customer对象暴露了订单(orders)
  • 订单(orders)有一个find方法,这个方法接收一个order_id对象,并返回一个orders对象
  • orders还有一个total对象,这个对象有getter和setter方法
  • 这个total对象可以用来计算总金额和折扣

这段代码有很多事情是不能改变的,一旦业务上有变动,很容易出问题。

比如公司要求订单折扣不能超过40%,你可以就在刚才的applyDiscount中加上执行代码,但最恶心的是,由于totals是个全局变量,在任何地方的随便一个代码块中都有可能修改totals的字段。

似乎可以从责任的角度看待这个问题——totals对象当然要承担管理汇总的责任。然而这里并非如此:它实际上只是一个容器,容纳了一堆任何人都可以查询和更新的字段。

有如下解决方案:

只管命令不要询问(TDA: tell don't ask)

TDA指的是:不应该根据对象的内部状态做出决策,然后更新状态,而应该把对应的工作委托给改对象自己执行。

对应上面的例子,就是把计算折扣的工作委托给totals对象:

public void appDiscount(customer, order_id, discount) {
  customer
    .orders
    .find(order_id)
    .getTotals()
    .applyDiscount(discount);
}

更进一步,customer不应该先拿到订单列表,再查找订单,而应该直接返回订单

public void appDiscount(customer, order_id, discount) {
  customer
    .findOrder(order_id)
    .getTotals()
    .applyDiscount(discount);
}

同样的,订单的实现使用了一个分离的对象来保存总金额,外部世界为什么必须知道有这么一个对象呢?

public void appDiscount(customer, order_id, discount) {
  customer
    .findOrder(order_id)
    .applyDiscount(discount);
}

虽然我们还可以进一步使用TDA来简化 -- 给customer加上applyDiscountToOrder(order_id)方法,但不必如此。因为TDA不是一个必须要遵守的法则,而是帮助我们识别问题的一种模式。

在这个例子中,客户通过订单id获取到订单,是一个既符合逻辑又符合实际情况的行为。

在每个应用程序中,都有一些通用的顶层概念。在这样的应用程序中,顶层概念包括客户和订单。将订单完全隐藏在客户对象中是没有意义的:它们有自己的存在价值。因此,我们完全可以创建出暴露订单对象的API。

不要链式调用方法

当你访问某样东西时,尽量不要超过一个「.」:

// 比如:
amount = customer.orders.last().totals().amount;
// 或者
orders = customer.orders;
last = orders.last();
totals = last.totals();
amount = totals.amount;

这个规则的例外是:如果你链式调用的东西真的不太可能改变,这个规则就不适用。

在实践中,应用程序中的任何内容,都应该被认为是可能发生改变的。

第三方库中的任何东西都应该被认为是易变的,特别是如果已知该库的维护者在版本之间修改过API。

编程语言附带的库就有可能非常稳定,通常是可以使用链式调用的。

现在的如RxJava之类的库,特地设计成链式调用的模式,但可以通过发布稳定版本来保证对应版本代码稳定性,从而规避「不要链式调用方法」这一原则。

关于解耦合,作者还提出:

  • 避免使用全局变量
  • 如果全局唯一非常重要,那么将它包装到API中