代码诊所

几年前,我有机会负责一个项目的咨询。团队很小,目标是对旧有系统的后端用Java改写,而团队的开发人员全为C程序员。我的工作职责是负责项目设计、开发,以及担任项目开发过程敏捷化的教练,并培养Java开发人员。

C程序员的一个特点是基本不具备面向对象知识,在初步掌握Java语法之后,写出来的代码还是过程化的代码。团队开发人员的现状就是:没有Clean Code的意识,不知道何谓TDD与重构,写出来的Java代码质量很糟。如果从项目开初不针对这一问题进行有效的防治,就可能导致整个代码库陷入泥沼之中。

为此,我要求在每日站会之后及时开展了代码评审活动。评审过程中,只能以我为主导,帮助大家发现代码的坏味道。从一开始,具有坏味道的代码可谓俯拾皆是,就像我们每天都在呼吸污染了的空气一般不可避免。要净化空气任重而道远,要让团队成员写出好的代码,同样任重而道远。

得有药方才行。

于是我当起了诊治代码疾病的医生。为了更容易传播医疗知识,我在团队工作室的墙角落,开了一个小小的诊所,广而告之——“每日一贴,包治百病”。刚开张时,诊所门面还没装修好,所以直接找了个白板开出了一个药方:

药方

个人认为,这些处方不仅仅对于当时的客户团队有疗效,可能也适合大多数开发团队。几年过去了,我把这些处方分享出来,也算是一个小小的总结吧。先来看看这些处方吧。

第一条:应随时保持架构的清晰与简单:统一所有查询为Repository。

项目其实并不需要访问数据库,而是通过远程的Telnet(或其他协议)去访问前端的设备。然而,我们可以借鉴DDD中资源库的这个隐喻。至于提到的架构,则是我在架构设计时参考了DDD的分层逻辑架构。

分层逻辑架构

为保证架构的简单与清晰,我做了一些“一刀切”的简化原则:例如对Repository和Service的定义。通过Telnet等网络协议获取设备信息的功能不妨看做是对DB的查询。因而,诸如NodeConfigureGetter这样的类就应该统一命名为NodeConfigureRepository。

该处方的主要目的是为了保持代码的一致性,若不加以规范,就会出现Getter、Finder、Query等各种不统一的类名后缀,让人眼花缭乱。

第二条:依赖注入(对象之间的协作)

很多OO初学者并不能理解依赖注入。我的一个办法是让他们从可测试性的角度出发。例如,倘若在NodeConfigureRepository类中直接实例化了TelnetService(这个类提供连接、登录、执行命令等与Telnet有关的操作),那么该怎样在不需要Telnet环境的基础上为NodeConfigureRepository编写单元测试呢?解决不了这样的问题,就说明设计的可测试性不够好。

解决方案就是依赖注入。当时的项目并未引入第三方IoC容器,原因在于项目的Jar包需要和另一个系统协作,并驻留在Flash中。容量有限,不允许引入太多第三方包,保证Jar包的精悍。

第三条:方法名体现意图。

这个问题是许多开发人员都容易犯的毛病,尤其对于面向过程设计的程序员而言,很少会站在对象的角度去思考方法(即行为,准确地说,从设计的角度讲应该是对象承担的职责)。例如在NodeConfigureRepository类中,开发人员定义了getNodeConfigure方法,但返回值却是void:

public NodeConfigureRepository {
    private NodeConfigure configure;
    public NodeConfigureRepository(NodeConfigure configure) {
        this.configure = configure;
    }
    public void getNodeConfigure() {
        getMasterLogicBoard();
        getMasterIp();
        getEnvId();
        getMasterSlot();
        getSlaveIp();
        getSlaveBoardTypeAndStatus();
    }
}

这个方法调用的诸多私有方法实则都是对构造函数传入的NodeConfigure进行数据收集。这样的定义不仅让代码的调用者感觉怪怪的,测试也变得极为诡异:

@Test
public void should_get_main_ctrl_logic_board_type() {
    configure = Nodeconfigure();
    configureRepository = new NodeConfigureRepository(configure);
    configureRepository.getNodeConfigure();

    assertThat(configure.getMasterLogicBoard(), is(12288));
}

怎么改?

方法就是让getNodeConfigure()方法直接返回组装之后的NodeConfigure对象,并且解除NodeConfigure与NodeConfigureRepository之间的生命周期依赖。有趣的是getNodeConfigure方法内调用的私有方法。它成了一种设计的例外,因为在Java中通常需要避免直接对输入参数进行修改,并将其作为返回结果。而在这里出现的一系列方法,实则是履行对NodeConfigure对象的数据收集,因而可以定义为:

private void collectMasterLogicBoard(NodeConfigure configure) {}
private void collectMasterIp(NodeConfigure configure) {}
private void collectEnvId(NodeConfigure configure) {}
private void collectMasterSlot(NodeConfigure configure) {}
private void collectSlaveIp(NodeConfigure configure) {}
private void collectSlaveBoardTypeAndStatus(NodeConfigure configure) {}

我对这些方法的名称进行了修改,使其能够更好地展现其意图。于是,getNodeConfigure()就变成了:

    public NodeConfigure getNodeConfigure() {
        NodeConfigure configure = new NodeConfigure();
        collectMasterLogicBoard(configure);
        collectMasterIp(configure);
        collectEnvId(configure);
        collectMasterSlot(configure);
        collectSlaveIp(configure);
        collectSlaveBoardTypeAndStatus(configure);
        return configure;
    }

这里实际上是Kent Beck提出的Collected Parameter模式。它是Visitor模式的简化设计。当然,我们也可以运用Builder模式对NodeConfigure对象进行组装。

第四条:同一个方法中的实现代码应处于同一抽象层次。

这其实是老生常谈了。Kent Beck在Smalltalk Best Practice Patterns一书中提到了“组合方法”模式,建议“让一个方法中的所有操作处于相同的抽象层”,即所谓的SLAP原则。在Robert Martin的Clean Code一书中也反复提到这一原则,Neal Ford在Emergent Design也有详细描述。

第五条:避免“哑对象”

这里展现的坏味道,在Martin Fowler的Refactoring一书中已有提及。在项目中,存在一些操作Xml文件的操作,并将这些Xml文件的Element映射为了Java对象。我们没有使用Jaxb,因为对于我们有限的xml操作而言,Jaxb还是显得太重。然而,在我们的代码中,包括PackageStatusFileParser、StoragePackageGenerator、DownloadingConfigureParser等类中都存在着将Xml Element转换为PackageInfo、SoftInfo等对象的重复代码。

原因就在于我们将这些对象看做了“哑”的数据对象,而没有将这种转换行为封装到拥有这些数据的对象中(我们的转换仅牵涉到Xml,没有扩展可能,因而无需使用Visitor模式)。

除了会导致大量的重复代码之外,一旦转换逻辑发生变化,例如XmlElement增加了Attribute,就可能需要到处修改,形成所谓的“霰弹式修改”。因而需要将这些逻辑封装到对象中,例如:

public class PackgeInfo {
    public PackageInfo createFrom(Elment element) {}
}

PackageInfo packageInfo = PackageInfo.createFrom(element);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,498评论 25 707
  • 几年前,我有机会负责一个项目的咨询。团队很小,目标是对旧有系统的后端用Java改写,而团队的开发人员全为C程序员。...
    _张逸_阅读 580评论 0 2
  • 感赏:今天心里感到很欣慰。5:40,儿子听到闹铃声,立即起床,没有磨叽,刷牙洗脸,一切收拾停当后。六点骑车去学校。...
    王建制衣阅读 156评论 0 1
  • “家庭三部曲”今算是全看完了,其中最打动我的应该是《喜宴》了。 影片里的高潮部分是中国人在张罗婚礼的满前忙后中,所...
    鵬一阅读 1,437评论 1 2