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