1.4.3 抽象缺失之基础类型偏执
基础类型偏执(Primitive Obsession)是Martin Fowler在《重构:改善既有代码的设计》一书中提到的一种典型的代码“坏味道”,意思是我们使用了太多的基础类型,导致有些应该被抽象成实体类的概念,却以基础类的形式散落在代码各处,这是一种典型的抽象缺失。
由于抽象缺失,相关的数据和行为将分散在其他抽象概念中,这将导致两个问题。
(1)暴露太多的实现细节,从而违反封装原则。
(2)数据和行为分散在代码的多个地方,导致代码重复、类之间耦合度变高、代码难以维护和理解等问题。
比如在一个图书馆信息管理应用程序中,国际标准书号(International Standard Book Number,ISBN)的存储和处理非常重要。一种自然的做法是将ISBN设计成字符串,毕竟它在数据库中的确也是以字符串形式存储的。然而,这并不是一个好的选择,为什么呢?
ISBN有两种表示方式,分别是10位和13位的,这两种形式之间可以转换。ISBN的各位都有其含义。例如,13位的ISBN由商品编号(图书产品代码978或979)、地区代码、出版社代码、书序码和校验码组成。
比如我写的第一本书《代码精进之路》,它的ISBN是978-7-115-52102-6,如图1-3所示。
图1-3 ISBN示例
ISBN的最后一位是校验码,其计算方式如下:从第一位开始,奇数位的值保持不变,而偶数位的值乘以3,将所有这些值相加再除以10,用10减去得到的余数就是最后一位的值。因此,给定一个ISBN,我们可以通过这种方式校验它是否有效。
对于图书馆管理系统来说,ISBN并不是一个简单的字符串,它本身就是业务核心,包含了一系列业务逻辑,比如关于ISBN的创建、验证、处理和转换,以及通过ISBN获取地区信息、出版社信息、书号等。如果将ISBN设计为基础类型字符串,那么这些处理逻辑将重复分散在很多地方。这种不将ISBN封装为类的行为,将带来因为抽象缺失导致的一系列不良后果。
因此正确的做法是,我们要对ISBN建立合理的抽象(类)概念,创建一个ISBN的接口,其中包含通用的抽象操作:
并创建子类ISBN-10和ISBN-13,它们都扩展了超类ISBN,如图1-4所示。
图1-4 ISBN设计类图
再比如,假设现在要实现一个功能,让A用户可以给用户B支付x元,可能的实现如下:
如果这是境内转账,并且境内的货币永远不变,该方法似乎没什么问题。但如果有一天货币变更了(比如欧元区曾经出现的问题),或者我们需要做跨境转账,该方法有明显的bug,因为money对应的货币不一定是CNY。
在这里,当我们说“支付x元”时,除了x本身的数字,实际上还有一个隐含的概念,那就是货币“元”。但是在原始的入参里,之所以只用了BigDecimal,是因为我们认为CNY货币是默认的,是一个隐含的条件。然而在我们写代码时,需要把所有隐性的条件显性化。
所以当我们实现支付功能时,实际上需要的一个入参是“支付金额+支付货币”。我们可以把这两个概念组合成为一个独立的完整概念——Money。
而原有的代码则变为:
通过将默认货币这个隐性的概念显性化,并且和金额合并为Money这个抽象概念,我们可以避免很多当前看不出来但未来可能会“爆雷”的bug。
将前面的案例升级一下,假设用户可能要做跨境转账(从CNY到USD),并且货币汇率随时在波动:
现在最大的问题在于,金额的计算被包含在了支付的服务中,涉及的对象也有2个Currency、2个Money、1个BigDecimal,总共5个对象。这种涉及多个对象的业务逻辑,需要一个新的抽象概念进行封装。
我们可以考虑将转换汇率的功能封装到一个叫作ExchangeRate的DP(Domain Primitive)[2]里:
ExchangeRate汇率对象通过封装金额计算逻辑及各种校验逻辑,使原始代码变得极其简单: