抽象
抽象这个东西,说起来很抽象,其实很简单。
WHAT
抽象是什么?按维基百科的说法:“在计算机科学中,抽象化(英语:Abstraction)是将数据与程序以它的语义来呈现出它的外观,但是隐藏起它的实现细节。”这个定义也许还有些“抽象”,举几个例子来看,它其实简单。
“抽象”在我们的日常工作和生活中比比皆是。例如,我们经常会说,“我是一个开发”,“这事儿你得找产品”,这里的“开发”、“产品”,都是一种抽象。它们定义了“开发要写设计、写代码、写单测”、“产品要写ppt、写word、写excel”这样一种“语义外观”,但是它们自己并不会写代码或写文档,这些实现细节隐藏在“职位”之下、由具体的“员工”来完成。
在技术上,这样的例子更是俯拾皆是。例如,Slf4j提供了一个日志的抽象,它定义了“怎么打印日志”这个“语义外观”,但是它隐藏了实际打印日志的实现细节——是log4j、还是logback?使用Slf4j时我们是不知道的。还有,Jdbc的Driver、Connection和Statement定义了“怎么操作数据库”这个“语义外观”,但是它也没有实际去操作数据库。这些实现细节是由抽象之下的具体实现来处理的。
在我们的业务系统中,“抽象”的实例也随处可见。最常见的,一个设计良好的接口就是一个业务的抽象,它定义了这项业务支持哪些操作。
例如,我们有一个短信签约接口定义了submit、sendCode、submitCode三个方法,本质上就是定义了“短信签约操作有三个步骤”这样一个业务抽象。至于每个步骤都是如何实现的,这是底层逻辑的事情了——实际上,这三个步骤的底层用的是同一个方法。
又如,我们有一个冻结订单的接口,定义了frozenFlow、frozenLimit、forzenTransport三个方法,这也就是定义了“冻结一笔订单必须冻结Flow、Limit和Transport这三类数据”这样一个业务抽象。至于这三类数据具体如何冻结么——就我们系统来说,有的业务是直接逻辑删除,有的业务是把数据回滚到初始状态,有的业务则干脆不需要处理Transport数据——这又是底层逻辑需要考虑的事情了。
所以,抽象是什么?抽象就是这样一个东西:它告诉了你自己能做什么、但不告诉你它是怎么做的。
WHY
设计出一个好的抽象,除了能隐藏底层实现之外,还有什么好处吗?我们为什么要在“抽象”这虚的东西上下功夫呢?
借用另一篇文章的话来说:抽象设计得越好,代码就越简单易用;代码可替代性就越好;可扩展性就越好。
简单易用
为什么说抽象设计得越好、代码就越简单易用呢?
因为一个好的抽象设计隐藏了它的底层实现,使得我们在使用它的时候,不需要关注底层的细节。
就好比开自动挡的车时不用关心离合换挡的事儿,开起来当然比手动挡要简单方便啦。
例如,我们看看下面这个接口:
这个接口提供了两个方法,两个方法的入参、出参都是一模一样的,区别只在于方法名——以及名字所暗示的,是从“远程”查询、还是从“本地”查询。如果调用方在使用时,确实需要区分数据来源,这个设计倒也无可厚非。但是,实际上调用这两个方法时,所有的代码都是这个样子的:
这样的代码出现了至少五次。啰嗦吗?啰嗦。麻烦吗?麻烦。闻着臭吗?臭。为什么每次调用这个接口时都要这么写呢?因为这个接口把自己底层的实现——是从远程获取数据、还是从本地获取数据——暴露出来了。换句话说,这个接口的抽象设计得不够好。如果我们把这个接口这样设计:
顺便,底层这样实现:
那么,我们就可以这样调用这个接口了:
这样重新设计/实现过之后,使用起来是不是简单、方便多了?这就是良好的抽象设计的第一个优点。
可替代性
为什么说抽象设计得好,代码的可替代性就越好呢?
这同样是因为一个好的抽象设计隐藏了它的底层实现,无论我们怎么更换实现细节,只要对外抽象不变,调用方都不受影响。
这就好比我们去银行柜台取钱:只要能把钱正确取出来,柜员是男是女、是胖是瘦、甚至于是活人还是机器,这都无所谓。
我参与设计过一套账务系统。在这个系统中,我们把所有账户间的转账操作全部抽象为下面这个接口。这个接口所表达的业务含义是:从账户from向账户to转入金额amount元,记账科目是type。
在这个接口的“语义外观”下,我们更换过很多种底层实现方式:单边账、双边账、会计科目记账;同步操作、异步操作、批量操作;等等等等。没有一次变更影响到了接口调用方,最终找到了既能满足所有业务功能、又提高了处理性能的最佳方案。这就是在好的抽象设计下的代码可替代性带来的好处。
有成功的经验,自然也有失败的教训。我写过一套Java操作Excel文件的工具,底层用的是POI组件。这套工具的核心接口大概是这个样子的:
从这个接口的“语义外观”来看,这个模块的功能,简单来说就是传入一个Excel文件、并把其中的数据解析为一组对象T。
但是,这个接口存在一个很大的问题在于:接口的底层实现——也就是HSSFWorkbook——被暴露出来了。这就导致了这个模块只能解析2003版的Excel文件,面对用户上传的2007版Excel文件,它就无能为力了。
不仅如此,如果要把工具升级到2007版,所有调用方都必须跟着一起改:在我们的系统里,这意味着要多修改二十多处代码、多回归测试几十个功能。其中的困难可想而知。
如果这个接口设计得更好——例如把入参改成FilePath或者InputStream,它的底层代码的可替代性就更高,重构、优化、需求变更时需要修改的地方就更少。改得越少,开发的工作量、加班量就越少,出bug的几率也会更少。
可扩展性
为什么说抽象设计得好,代码的可扩展性就越好呢?
这和可替代性有相似之处:根子上还是因为一个好的抽象设计能隐藏它的底层实现。
就像家里给小孩儿炖汤;妈妈去厨房尝了一勺,然后多撒了一把葱花;姥姥又去尝了一勺,然后多加了点姜片;奶奶又去尝了一勺,然后多加了点花椒……在全家人的不断“扩展”下,最后留给小孩儿的虽然只剩一勺汤,但这一勺绝对是浓缩的精华哈哈哈。
举一个实际业务场景中的例子。我们系统中有一个查银行卡列表的接口,客户端查到列表后,需要根据不同的场景来展示或“置灰”某些卡。例如,划扣场景下,不支持自动划扣的卡就必须置灰;解绑定场景下, 跟某些业务绑定的卡就必须置灰;业务绑卡场景下,已经跟该业务绑定的卡就必须置灰……等等等等。
我们为这个业务所设计的抽象和相关的实现代码是这样子的:
服务端根据客户端传入的Scene来判断这些卡是否应当展示,并返回对应的enabled字段值。而客户端不需要关注List<Card>中的银行卡是不是支持自动划扣、是不是和某个业务绑定,只需要关注返回结果中的enabled字段:enabled为true,则允许用户选择这张卡;enabled为false,则只展示这张卡、但不允许用户点选。
而且,无论哪个Scene下要增加逻辑,或者要增加新的Scene,都只需要服务端做出修改,客户端是不需要变的。而且即使是服务端,需要修改或增加的代码量也不大,非常简单。
简单易用、可替代和可扩展这些特性,对于业务系统来说,有时其重要性甚至比对技术中间件还要高。
业务系统的一个重要特点,就是业务需求在不停变化、频繁变化:今天需求是这样,明天就推翻不做了,后天又重新把需求翻出来出来,大后天再改一版……如果系统的设计和实现被需求牵着鼻子走,那开发就有改不完的代码、加不完的班了。
好好地设计一套业务抽象,让系统和代码简单易用、易于替换、易于扩展,才有可能在少修改代码、甚至不修改代码的基础上去满足多变的业务需求。开发才能从业务代码中释放出来,去提升自己、优化系统。
HOW
怎样设计一个好的抽象呢?其实我们已经有很多方法论/工具箱了:高内聚/低耦合、封装/继承/多态、SOLID、设计模式……等等等等,不一而足。只不过以前讨论它们的时候,更多地是在“就事论事”地讨论它们自身,而并没有考虑到它们与“抽象”的关系。怎样从业务抽象的角度去理解和应用这些方法和工具、又怎样运用它们来建立良好的业务抽象呢?我们下回分解吧。
版权声明: 本文为 InfoQ 作者【落英亭郎】的原创文章。
原文链接:【http://xie.infoq.cn/article/60f7f73a40d00b0a62f351aa5】。文章转载请联系作者。
评论