前两天,我所在的项目有一个小的技术改动,打算把访问Redis的密码从数据库挪到配置文件里。以前的代码类似下面这样:用户第一次调用
GetDatabase
时,根据传入的数据库连接字符串访问数据库,从某个表里取出带密码的Redis连接字符串,然后建立Redis链接。至于替换成从配置文件中读取地址和密码,你可能10秒就想到了办法,然后不到10分钟就完成了下面的修改。完美,不是吗?不但达到了目的,还删除了一个无用的方法。但这是最佳的替换方式吗?我是说,要想把某个实现替换成另一个实现,就应该这么直接上吗?前两天我在公司的小电风扇坏了,电源线和风扇接触不良,只有在某一个特定位置才能通电。而我的一个同事的小风扇的电源是可插拔的,如果电源线坏了,换一根就是了。 我看到后惊呼:卧槽,接口隔离!!!对于系统容易发生变化(易坏也属于变化)的部分,使用接口(电源线接头)对其进行隔离,如果变化了(线坏了),就用其他实现了同样接口的东西替换(即便没坏,想换个颜色也是很方便的)。这样的设计才叫设计啊。你也能看到,之前的实现和真正建立Redis连接的代码耦合在一起。本来嘛,先构建一个ConfigurationOptions
,再基于这个options建立连接,很自然的事情。但现在要替换的,其实就是这个构建ConfigurationOptions
的实现方式。以前是基于DB,现在基于配置。一个实现要被替换,说明什么呢?说明这是代码的一个变化方向。遇到变化的时候我们应该怎么处理呢?当然是创建一个接口来隔离这个变化。以前为什么没有隔离而是耦合在一起呢?是因为没有识别出这是一个变化方向。慢着,不要着急吐槽前人,犯这样的错误其实是很正常的事情,甚至都不能称其为一个错误。我们很难一开始就识别出业务或技术上的所有变化方向。但当变化真正出现的时候,也别急着着手修改。它以前不是一个变化方向,但现在是了。既然是了,就用接口隔离出来呗。好在代码是可以随时修改的,比电风扇强。我们重构最一开始的代码,把构建ConfigurationOptions
的代码抽取出来。再把GetRedisConfigurationOptions
方法抽取到新建的RedisConfigurationOptionsProvider
类中。一开始的RedisCacheProvider
类现在变成了下面这样。这时候已经具备了抽取接口的条件。接下来我们为IRedisConfigurationOptionsProvider
接口添加一个新的实现,基于配置来读取Redis连接和密码。再回到RedisCacheProvider
类中,将接口的实现替换为ConfigBasedConfigurationOptionsProvider
。再删除旧的RedisConfigurationOptionsProvider
类,大功告成。由于引入了修改的复杂度(抽取类、接口,引入新的实现),以前10分钟能完成的工作,现在大概需要半个小时了。但这样做的好处也显而易见。我们识别出了一个新的代码变化方向,并用接口对其进行了隔离。以后再发生变化,只需要提供一个新的实现就好了,也遵循了开放-封闭原则。你可能不觉得这样做有什么好处,毕竟混在一起写也没有不会造成什么麻烦。的确这里的逻辑非常简单,即使耦合在一起也没什么大不了。但如果项目大了,很容易处处都是这样的耦合。当一个类里出现多处这样的耦合时,“发散式变化”的坏味道就出来了。如果你熟悉面向对象设计,可能已经脱口而出了,“这不就是抽象分支吗?”。没错,是的。抽象分支就是替换一个实现的最佳实践,小到一个类,大到一个基础设施,都是如此。点击阅读原文,可以看到Martin Fowler关于抽象分支的文章。你是如何替换一个实现的?