话题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中