代码该合还是分?上下文为王

banq


今天咱们要讲一个程序员小明的血泪史,保证比你们追的连载漫画还精彩!

【第一幕:初来乍到】
小明刚加入新团队,发现前辈们写了个"万能客户引导模块"——就像学校食堂的万能打菜勺,既能舀汤又能盛菜。代码简洁优雅,全组都夸"绝了!"。小明美滋滋地想:"这波稳了!"

【第二幕:暗流涌动】
突然有一天,产品A说要加个"人脸识别"的合规步骤(就像食堂突然要求打饭必须刷脸),产品B却大喊:"我们这穷学校用不起这高科技!"小明手一抖改代码,结果产品B的用户连校门都进不去了——注册系统直接崩了!(画面:学生被卡在校门口哭唧唧)

【第三幕:灾难现场】
现在这个"万能模块"变成了定时炸弹,改哪儿都爆炸。就像你们教室的联排课桌,一个人晃腿全班桌子都在抖!说好的"省事"现在变成"坏事"了。

【DRY原则的陷阱】
老师总说"写作文不能抄袭",编程界也有个铁律叫DRY(别重复)。但你们发现没有?数学老师和政治老师的教案虽然都"讲解知识点",但能用一个模板吗?(台下狂笑)强行合并的下场就是——数学卷子上出现"用辩证法解微积分"!

【什么时候该复制粘贴?】
1️⃣ 当两个功能像双胞胎书包:现在都是装书,但一个要改造成电竞背包,一个要变成买菜包
2️⃣ 当不同部门管不同功能:就像学生会和团委的活动方案,硬要统一只会互相拖后腿
3️⃣ 当代码开始"打补丁":if(is食堂){刷脸}else{刷卡},这种代码就像校服上缝满补丁,丑炸了!

1、当模块独立演进时
乍一看,有些代码片段几乎一模一样。为了避免重复,将它们合并到同一个抽象层级下似乎很自然。但如今的相似之处,背后可能隐藏着截然不同的未来。
以通知系统为例。最初,营销邮件和系统提醒可能遵循相同的步骤:选择模板、填写数据并发送。因此,统一它们似乎是显而易见的。
后来,市场营销部门需要 A/B 测试和交付优化,而系统警报需要即时发送并准确记录。现在,每一次变更都有可能破坏关键功能。曾经“干净”的抽象变成了一种负担。
如果这些流程被复制,那么每个流程都可以自由演进,而不会出现持续的冲突。
当您预期系统朝不同方向发展时,复制通常是更安全的架构选择。

2、当上下文不同时
在领域驱动设计 (DDD) 中,有界上下文围绕系统的特定部分定义了清晰的边界,并具有自己的模型、规则和语言。
为了避免重复,人们很容易将看似相似的逻辑在不同上下文中统一起来。但这样做会引入隐藏的问题;你会造成跨上下文耦合,迫使不相关的团队或模块协调各自的变更。随着时间的推移,共享代码会变成一个被污染的中间地带,不仅无法完美地服务于任何上下文,反而会限制所有上下文。
健康的架构能够接受甚至鼓励跨有界上下文的一定程度的重复。它能够保持自主性,允许每个上下文自由演进,并保障每个领域语言和行为的清晰度。

简而言之:跨有界上下文的共享代码通常比重复的、干净分离的逻辑带来更多的痛苦。

3、当复杂性被隐藏而不是被消除时
好的抽象能够简化系统。糟糕的抽象只会将复杂性隐藏在另一层之下,使代码更难理解,而实际上却没有减少底层实际的工作。
结果是一段看似“DRY”的代码,但实际上脆弱、不透明且容易出错。这里有一个例子;

假设一个应用程序中有两个不同的表单:一个是工作申请表,另一个是客户反馈表。这两个表单都需要进行一些基本的验证,例如检查必填字段和邮箱地址格式。有人创建了一个共享FormValidator实用程序来避免重复。

public class FormValidator {
    public static boolean validate(Form form, boolean validateFiles, boolean allowAnonymous) {
        if (form.getEmail() == null || form.getEmail().isEmpty()) {
            if (!allowAnonymous) {
                return false; // email required unless anonymous allowed
            }
        }
        if (form.getRequiredFields().stream().anyMatch(f -> f.isEmpty())) {
            return false;
        }
        if (validateFiles && (form.getResume() == null || form.getResume().isEmpty())) {
            return false;
        }
        return true;
    }
}

为什么共享 FormValidator 不是一个好主意:

  • 耦合不相关的逻辑:改变一种形式可能会破坏另一种形式。
  • 掩盖了真正的差异:复杂性在悄然增长。
  • 消除局部推理:难以孤立地调试或理解。
  • 难以改变:小的调整会引起连锁反应。

改为:

public class JobApplicationValidator {
    public static boolean validate(JobApplicationForm form) {
        if (form.getEmail() == null || form.getEmail().isEmpty()) return false;
        if (form.getResume() == null || form.getResume().isEmpty()) return false;
        if (form.getRequiredFields().stream().anyMatch(f -> f.isEmpty())) return false;
        return true;
    }
}

public class FeedbackFormValidator {
    public static boolean validate(FeedbackForm form) {
        //  // 注意这一行是重复的,我依靠你的想象力
        
// 想出一个更大范围的类似例子:)
        if (form.getRequiredFields().stream().anyMatch(f -> f.isEmpty())) return false;
        return true;
    }
}

每个验证器都保持简单、专注且可以自由发展——没有令人困惑的标志、没有纠结的逻辑,也没有悄悄破坏其他形式的风险。

【正确复制姿势】
举个栗子:原来写"万能验证器"(既要验学生证又要验食堂卡),不如拆成:

  • 学生证验证器:检查照片+学号
  • 饭卡验证器:检查余额+有效期虽然都有"检查"动作,但就像篮球和足球都会"运球",能是一回事吗?

重复是良好的架构权衡的迹象
虽然复制一开始常常让人感到不舒服,但某些模式表明复制不仅是可以接受的,而且是更明智的长期举措。并且有一些明显的迹象值得关注;

  1. 不同的变化速度如果两个模块的演进速度不同,共享代码会不必要地将它们捆绑在一起。复制可以让每个模块按照自己的节奏发展。
  2. 团队所有权不同跨团队共享代码会产生摩擦。复制可以明确职责,加快变更周期。
  3. 预期分歧当你知道逻辑会随着时间的推移而逐渐分离时,从重复开始可以避免以后痛苦的解开。
  4. 强制抽象如果抽象需要标志、类型检查或尴尬的分支,这表明您正在强制将不属于的东西放在一起。

【人生哲理时间】
好的程序员就像聪明学生:

  • 该合并的笔记就合并(比如公式大全)
  • 但语文摘抄本和数学错题本必须分开!记住:重复不可怕,强行装逼最尴尬!下次见到相似代码,先问它俩会"同年同月同日死"吗?再决定要不要拜把子!

指南:如何明智地复制
复制可能是一个明智的架构选择,但需要良好的沟通。以下是如何进行良好的沟通:

  1. 记录重复代码存在的原因。如果故意重复代码,请留下清晰的注释或架构说明,解释其原因。未来的开发人员应该理解,重复并非偶然,而是为了灵活性、模块化或自主性而做出的刻意权衡。
  2. 定期审查重复代码重复代码并非总是永久性的。随着时间的推移,需求可能会趋于稳定或一致,这使得以后合并重复代码变得安全且有用。

设置定期的架构审查来重新评估重复的区域是否应该保持分离或合并。


复制和抽象都是工具——上下文才是一切
在恰当的时机使用抽象,其威力巨大。它能够降低复杂性,提升清晰度,并以有意义的方式连接系统的各个部分。但抽象并非免费。每一次抽象都是一种赌注,一种对系统不同部分将如何协同运作的猜测。

当猜测错误时,复制并非失败。它是一种防御机制。它保护独立性,保持复杂性的诚实,并允许系统的不同部分按照自己的节奏发展。

好的架构并非总是避免重复,也不是抽象一切。它在于理解背景Context​​,理解权衡,并慎重选择。

有时,正确的做法是统一;
有时,正确的做法是复制。

了解两者之间的区别,正是深思熟虑的设计与教条主义工程的区别所在。