程序员的代码洁癖

身边有些程序员写代码的时候有代码洁癖,这到底是不是一个好习惯呢?

绝对是好的习惯。因为他有在考虑维护性的东西。且我通常都是建议,新写的代码,要有代码洁癖的意识习惯。

这种程序员他脑子里到底在想啥呢?或者会有什么样的表现呢?

  • 一个变量的命名如果拿捏不准,他会思考良久,或者到处找资料问高手,到底如何命名才最贴切;
  • 他会深度考虑一个类、一个方法的责任。「类和方法是要经过组织的」,这点刻在他脑子里,不会违背。因为他不希望别人可以轻易破坏掉他写的代码。他总是会留口子让你改,但不破坏功能;
  • 写「处于同一抽象层次」的代码,是他的职业习惯。别人一看,就知道整块业务的核心流程是什么,而不是一下子陷入细节。

他会深恶痛绝那种随意写代码的写法。

那每个程序员一开始就会这样?大多数不是的。都是挨过N多个坑,被骂过N多次,被一次一次的故障教育得来的。

告诉你,在专业的程序员眼里,你随意写代码,他是无法跟你一起工作的。且说话也很不客气。他心里想:你是干这行的吗?为啥如此随意?

专业的程序员,也经常想一个事情:

他希望这个功能交付后,无论是稳定性、维护性和异常流处理,都是到位的。交付了就交付了,而不是有一堆的后遗症。

为啥?他们是讲究效率的。如果接新需求的时候,还老是去改上一个需求的bug,那会极大影响他的效率和专注性。

他们习惯交付一个就是一个。他们不会在这件事上妥协。

同一抽象层次:流程一眼能看懂

给你们看一段代码片段。下面体现的是「处于同一抽象层次」的写法:

java 复制代码
public Long submitOrderDocs(SaveOrderDocsCommand saveOrderDocsCommand) {
    //1、检查来创建订单的参数
    checkCreateOrderParam(saveOrderDocsCommand);

    //2、核心方法:计算单据推送到低代码平台的时间
    buildOrderCanPushTime(saveOrderDocsCommand);

    //3、创建订单
    Long docsId = createOrder(saveOrderDocsCommand);

    //4、特殊订单走OA审批流程
    createOaWorkFlow(saveOrderDocsCommand, docsId);

    //5、发布领域事件,用于数据统计
    eventPublisher.publishCreateEvent(docsId);

    return docsId;
}

上面的代码,体现的是门店店员下一个订货单需要走的流程。注意,是流程。我并不需要立刻知道代码细节,我要的是核心业务流程。够了,可以了。我需要的话,再深入进去就可以了。

而不是一来,就让我看到一大堆代码细节。

至少我看过的专业程序员,不会这么玩。代码要清晰体现业务流程。这个也很容易理解:业务就是那样走的,你为啥不想办法清晰体现呢?

类是经过组织的:钉钉审批的例子

经过设计和组织过的代码,你想轻易破坏,还破坏不了。

我举个连锁门店系统里的例子。对接钉钉、创建OA审批单,这块一想就知道是通用的:都要调钉钉、都要建审批流程。同时扩展点也容易知道,因为连锁店业务,还有排班、门店营业时间变更等必走审核流程的业务。像我这种经常做连锁店业务的,是非常清楚的。

这种「通用 + 必扩展」的场景,才值得用工厂和策略去组织。不是为设计而设计。

第一步,调用方只管取策略、创建审批:

java 复制代码
IDingTalkWorkFlowStrategy strategy =
    DingTalkWorkFlowFactory.getDingTalkWorkFlowStrategy(bizType);
return strategy.createWorkFlow(createWorkFlowCommand);

第二步,工厂按业务类型分发。 新增一种钉钉审批业务,加一个 XxxWorkFlowStrategy 实现,在工厂里挂一行分支,老策略不用动:

java 复制代码
switch (bizTypeEnum) {
    case SHOP_TIME_CHANGE:
        return SpringUtils.getBean(ShopTimeChangeAuditWorkFlowStrategy.class);
    case SHOP_SCHEDULE:
        return SpringUtils.getBean(ShopScheduleAuditWorkFlowStrategy.class);
    default:
        throw BusinessException.of(...);
}

第三步,才是创建OA审批单的核心骨架。 通用流程放在抽象父类里,子类只填各自业务的表单字段:

java 复制代码
public DingTalkWorkFlowRecordEntity createWorkFlow(CreateWorkFlowCommand cmd) {
    //1、校验能否创建
    checkCanCreateWorkFlow(cmd.getBizId());
    //2、组装申请人、部门、审批模板等公共信息
    CreateWorkFlowDTO dto = assembledBaseInfo(cmd);
    //3、各业务子类填充钉钉表单(排班、改营业时间等字段不同)
    assembledFormComponents(dto);
    //4、落库并调用钉钉创建审批实例
    return doCreateWorkFlow(dto);
}

private DingTalkWorkFlowRecordEntity doCreateWorkFlow(CreateWorkFlowDTO dto) {
    DingTalkWorkFlowRecordEntity root = createAndPersistenceRoot(dto);
    invokeDingTalkCreateWorkFlow(dto, root);
    return root;
}

子类里通常只实现 assembledFormComponents,比如排班审批填周期、班次汇总;门店营业时间变更填另一套字段。公共的取 token、查部门、调钉钉 API、落审批记录,全在父类,不用每个业务抄一遍。

新增业务时,你动的是:一个新 Strategy + 工厂一行 + 表单组装方法。原来的排班策略、改时间策略,不用碰。

这里要强调一点:上面说的洁癖,指的是新写的代码。老代码不能因为所谓代码洁癖就全盘重构,那不现实。除非公司或部门立项做了重构专项,才有条件系统性动老代码。

业界大神怎么看

国外几个影响力很大的程序员,看法大体一致,但也都留了边界。

Robert C. Martin(Uncle Bob) 在《代码整洁之道》里把写整洁代码当成专业素养:代码像被人认真照料过,函数和类专注一件事。Michael Feathers 还补过一句:整洁的代码,是作者在意过细节才会留下的东西。

Bjarne Stroustrup(C++ 发明者)说得更短:整洁的代码只做好一件事。逻辑直截了当,依赖尽量少,方便维护。

Martin Fowler 讲 YAGNI:不要为「以后可能用到」堆复杂度。但让代码容易改的重构,不等于 YAGNI。两者要分清楚。

共识是:代码要可读、可改、能交付。 分歧在:什么时候该停手。

什么时候洁癖会坏事

读者可能会问:既然这么好,为啥还有人反感代码洁癖?

几种常见情况:

格式强迫症。 只改缩进、换命名风格,行为不变。这种,在我眼中,跟代码洁癖,没任何关系。

过早抽象。 需求还没定,就抽一堆接口和父类。这和钉钉工厂不一样:钉钉是通用且扩展点明确;如果只是「万一以后能复用」,不该上策略模式。

无立项地动老代码。 老模块能跑、线上稳定,没有重构专项,却因为看着不顺眼就大改。风险往往大于收益。专业洁癖应该花在新代码和当前需求上。

怎么区分专业洁癖和格式强迫症

维度 专业洁癖(值得坚持) 格式强迫症(别瞎折腾)
目标 可读、可改、可交付 看着顺眼
典型行为 新代码流程清晰、通用处复用、扩展点明确 只改风格、无立项重构老代码
对团队 降低接手成本 CodeReview陷入口水战
判断 不改会增加后续bug或改需求成本 不改只是看着别扭

小结

从维护性和交付效率来看,代码洁癖是好习惯。我通常也建议:新写的代码,带着洁癖的意图去写。命名、职责、抽象层次、该复用的地方留扩展点,这些力气花得值。

同时要有边界:老代码别因为没有立项就硬重构;别为假想需求过度设计。

如果你身边有那种写代码很「讲究」的同事,多半不是天生如此,是被坑出来的。你也可以从新需求开始,练这种习惯。

参考的内容