设计一个小接口展开的讨论

蔡了戴着耳机一边哼着歌一边晃着头沉浸在自己的音乐世界里。8点45分,距离早会还有半个多小时,放松一下,抽空自娱自乐!

“蔡了!”办公室传来中气十足的呼唤声,似乎整个写字楼都回荡着蔡了的名字。

“哎,明明就只隔几个工位,还要吼得整栋楼都听得到,不明真相的,还以为我又犯什么错了呢!”蔡了一边腹诽着,一边回应着“来了来了”,丢下耳机就向马大叔的工位跑去——没办法,头儿的呼唤,怎敢不从?

“马大叔!什么事啊?不会我的代码又没写好吧?”蔡了看着电脑屏幕的光线投射在马丁花那张老脸上,阴晴不明,心里一紧,说话也变得小心翼翼起来。

“没事就不能叫你?”马丁花脸一板,像指挥军队的将军一般将手一挥,说到:“过来,过来!我今早一来就pull了代码,正好看到你提交的代码。”

蔡了一听,知道老马又要上课了,赶紧推了一把椅子,收了心里的腹诽,像个乖乖女一般规规矩矩地坐下来,看着老马屏幕上打开的一个类。正是昨天自己写的一个工厂类Kafka251Factory

“这样的工厂类我们写了很多,按理说你可以参考,但我看你写的代码却有些不同,为何这两个创建方法要定义为实例方法呢?”

“老大,我是这样想的。我觉得这两个创建方法传入的参数都一样,为了避免重复,我就将它们定义成了字段。因为要使用字段,就不能用静态方法罗!”蔡了一想到昨天自己灵机一动,通过提取字段有效避免了方法参数的重复定义,不免有些沾沾自喜。

“哦,小蔡不错哟,都有很强的重用意识了!但是——”话锋一转,马丁花开启了循循善诱的教导模式,“你有没有好好比较两种不同定义的调用代码呢?”一边说,一边在电脑上敲出如下代码:

//方式1
new Kafka251Factory(config.kafka(), "topic").createSource();

//方式2
Kafka251Factory.createSource(config.kafka(), "topic");

“这个……”蔡了转着大大的眼睛,努力想说出点颇有见地的答案,只可惜转了半天,还是想不出一二三。

马丁花看蔡了半天回答不上,也不着恼,缓缓说道:“你要学会站在调用者的角度看待API设计。这里虽然都是一行调用代码,但是要创建一个实例和直接调用类型的静态方法给人的观感还是不同的。虽然从可扩展性的角度看,面向对象鼓励我们尽可能定义实例方法,但对于工厂方法而言,方式2定义的静态工厂方法还是要更直接一些。更何况,作为一个工厂对象而言,频繁地创建工厂实例,既无必要,也增加了垃圾回收的负担。”

蔡了点点头,说道:“大叔,我貌似懂了,就是说从调用者的直觉来看,这样的静态工厂方法应该更符合调用者的期望。我说得对吗?”

“不错不错,朽木可雕也!”马丁花哈哈地笑起来,很欣慰自己的一番教导总算没有白费。可惜没有长长的胡须可以抚弄,否则倒有几分爷爷看孙女的感觉,如今这般笑着,无奈只得扫入怪大叔的行列了。

“可是,这样的定义不是导致方法参数的重复了吗?”蔡了仍不甘心推倒自己的重用成果,指着重构为静态工厂方法的Kafka251Factory类,继续问道。

“如果一个类的多个实例方法接收的输入参数相同,确实可以将这些参数当做该类的字段,如此即可减少参数的传递,而且从生命周期来看,既然多个实例方法都会用到它们,说明它们的作用范围要广于方法的范围,可以认为是当前类自身携带的数据,将它们定义为字段,然后在构造函数中接收其值,这一设计是合理的。但是,对于静态方法来说,由于方法参数与当前类的生命周期并非一致,我们就不能贸然地将其改为实例方法,否则就会增加调用者的负担。如果静态方法的参数太多,大可以通过提取Parameter Object封装多个内聚的参数,减少方法参数数量。”

马丁花说到这里,想了想,继续说道:“若要说重复,代码中的config.item(label).topic()出现了两次,有着很明显的重复代码坏味道,可以再改进一下。”

“这个还不简单!”蔡了终于逮住表现自己的机会,一把抢过摆在电脑面前的键盘,熟练地将光标移到这行代码,按下Ctrl+Alt+M快捷键,将其提取为一个方法:

“看起来提取方法确实消除了重复,但你觉得它带来了有效的重用了吗?”马丁花点点头,又摇摇头,继续问道。

蔡了皱起眉:“呃……提取的方法做到了重用,可惜要重用的代码实在太少,看起来区别不大啊!”

“确实如此,然而……关键不在于这里。”马丁花说罢,接过键盘,把光标挪到getTopic方法下,解释道:“你看这里,两个工厂方法实际上希望获得的是topic,这个topic又是通过config的item与传入的label来获得的。虽说这段代码非常简短,实际上它仍然暴露了获取topic的细节。”马丁花的语气开始加重,这是要敲黑板了:“记住!当你发现调用一个对象时,频繁出现多个方法链式的调用,只要不是像Java Stream API那样的流畅接口,都说明它的封装不够彻底,属于Martin Fowler指出的Message Chain坏味道,有人也将这一坏味道称之为‘火车残骸’!

“哦!”蔡了恍然大悟!

“还记得我给你讲过的信息专家模式迪米特法则吗?你结合这两个原则想想这里的问题所在!”马丁花不愧是经验丰富的资深咨询师,逮住机会就开始启发式教育了。

蔡了用手支住自己的小脸,开启大脑的Google搜索模式,似乎都能听到敲木鱼的声音,然后“叮”的一声,欢呼雀跃地叫到:“我知道了!如果根据迪米特法则,KafkaConfigItem对象是Kafka251Factory的陌生人,根本就不需要知道它;根据信息专家模式,无论是Item还是Topic,其实都是KafkaConfig的信息,因而获取topic的职责就应该转移给KafkaConfig来承担,所以……”蔡了拿过键盘,流畅地按下F6,完成方法的移动,再通过ctrl+alt+shift+t调出重构万能键的快捷菜单,迅速通过convert to instance method重构手法将静态方法修改为实例方法。只见她白皙的双手在键盘上跳动着,像弹钢琴一般,动作如行云流水,十分好看,很快就完成了重构:

当然,getTopic方法已经顺理成章转移到了KafkaConfig那里。

“不错,写代码就得有这样的态度!虽然代码的质量仅得到了微量的改进,却只有如此,代码才能不断打磨,趋于完美,你的技能也能不断精进。最关键的是,这样小步地重构既安全,也没有什么成本,何乐而不为呢?好了,要开站会了,赶紧去准备吧。你重构的代码我一会提交!”

“好的!谢谢大叔!”蔡了知道自己水平菜,老马则是逮住机会就给自己学习的机会,这一声感谢,确实由衷而发。说完转过身,连蹦带跳地回到自己的工位。还没坐下来,又听到大叔一声吼:“蔡了,记得查一下资料,搞清楚静态工厂到底用了什么模式,它会对设计带来什么好处。明天的作业,我要检查哈!”

“哦!”蔡了苦着脸应下来,心里不禁骂道:“这个死老头,都不让人轻松一点!我的命怎么这么苦啊!”心里对马丁花积累的5点好感值瞬间烟消云散了。