设计模式之Copy-and-paste programming

168 阅读6分钟

在开发过程中,我们发现大量的小伙伴通过拷贝代码来完成功能。既然是很多人的选择,那么必然有存在的道理——知道它好用,但是,说不出来它的好。

事实上,经过这么多年的开发工作,我想说:对于业务开发,能不“DRY”(Don't Repeat Yourself),就不“DRY”。

对于这个设计模式的使用,读者可以自行判断。

home (1).png

一、当前时代与经典时代的区别

经典时代认为,复用->降低代码行数->降低人的心智成本。

而当前时代(AI时代)我们发现,越少的代码(越少的提示词)->越少的上下文->越让人难以理解。

首先,我们现在的硬件性能是以前的成百上千倍类似的业务,你复制代码,对于所谓“性能”提升微乎其微。

其次,现代软件开发节奏快,老板催着要上线,客户每天都有新需求,迭代周期短得让人喘不过气。这种环境下,我们更需要快速出活,需要我们写代码的时候“轻装上阵,降低心智”。

最后,软件功能的复杂化。现在的软件,每天都有新功能、新需求,不停地在变化。这种情况下,代码的稳定性和可维护性更重要,我们得随时准备着修改和扩展代码。

吐槽一下,老板们希望复用的是“人”,也就是说,让开发人员更高效地干活。而我们技术人研究的是“代码的复用”——降低代码行数,这是多么的可笑。

说到复用,我们都知道它的目的:提高开发效率、降低维护成本、减少重复劳动。但很多人觉得拷贝代码不算复用,觉得这是偷懒。其实,在很多业务开发场景下,代码拷贝可能比我们想象的更有效。

二、案例

现在我们来看一个具体的案例,来更好地理解为什么复制优于组合,组合优于继承。

假设我们要实现一个业务功能:生产一只母鸡来打鸣。我们可以按照以下步骤来实现:

  1. 创建一个实体(类、函数或其他)。
  2. 让它变成一个动物。
  3. 给它翅膀,变成鸟类。
  4. 让它可以打鸣,表示它是一只公鸡。

我们用“类”来实现这个业务功能,暂时不使用函数。

为了篇幅,我会尽力减少代码示例。忽略了组合部分的代码。

class 母鸡 {
    // 第一步
    动物();
    // 第二步
    翅膀();
    // 第三步
    打鸣();
}
new 母鸡().打鸣();

1. 实现母鸡打鸣

克隆的实现

class 母鸡 {
    // 第一步
    动物();
    // 第二步
    翅膀();
    // 第三步
    打鸣();
}
new 母鸡().打鸣();

继承的实现

带入假设(创造需求 ,我们假设将来我们还要实现,鸭子,小狗。好了,“有经验”的工程师会这样来做:

class 动物{
  动物();
}
class 鸟类 extends 动物{
  翅膀();
}
class 母鸡 extends 鸟类{
  打鸣();
}
new 母鸡().打鸣();

目前,虽然我们还看不出克隆(代码复制)的优势,但在组织代码上,面向对象编程中的继承和组合这些技巧已经开始显露出更多的心智成本。

2. 实现一只鸭子

克隆的实现:

class 鸭子 {
    // 第一步
    动物();
    // 第二步
    四肢();
    // 第三步
    游泳();
}
new 鸭子().游泳();

继承的实现:

class 鸭子 extends 鸟类 {
    游泳();
}

3. 实现一只小狗

克隆的实现:

class 小狗 {
    // 第一步
    动物();
    // 第二步
    哺乳();
    // 第三步
    汪汪();
}
new 小狗().汪汪();

继承的实现:

class 哺乳动物 extends 动物 {
    哺乳();
}

class 小狗 extends 哺乳动物 {
    汪汪();
}
new 小狗().汪汪();

4. 实现“植物狗”(植物人)

假设我们要实现一个“植物狗”(这是一个荒诞的需求)。

最终

克隆的实现

继承的实现

多继承

单继承

如果我们使用继承的方式,问题就来了。在许多编程语言中,不支持多继承,所以我们不能让“动物”继承“植物”!这时,我们需要用到设计模式来整理代码,或者让“植物狗”变成“植物大狗”。

组合的实现

三、数学证明

image.png

首先我们约定一下定义:

  • 顶点,每个代码块;
  • 测点,每个顶点对业务的影响;
  • 业务顶点(叶子顶点),实际要实现的业务;
  • 能力顶点(非叶子顶点),实现业务需要的功能;

我们将每个节点的测点作为维度,来看哪种模型对测点的影响范围大。测点也可以理解为开发的心智成本或工作量。以下是对母鸡、鸭子、小狗、植物狗这些业务进行说明,我们称它们为业务顶点。

克隆

  • 总顶点数:13
  • 业务顶点数:4
  • 能力顶点数:9
  • 测点总数 = 13
  • 平均值 测点总数/顶点总数 = 13/13 = 1

继承

  • 总顶点数:8
  • 业务顶点数:4
  • 能力顶点数:4
  • 测点总数 = 4(业务顶点) + 2(鸟) + 2(哺乳) + 4(动物) + 4(植物) = 16
  • 平均值 测点总数/顶点总数 = 16/8 = 2

组合

  • 总顶点数:8
  • 业务顶点数:4
  • 能力顶点数:4
  • 测点总数 = 4(业务顶点) + 2(鸟) + 2(哺乳) + 4(动物) + 1(植物) = 13
  • 平均值 测点总数/顶点总数 = 13/8 = 1.6

通过上述分析,我们可以得出结论:

  • 复制没有层级结构,直接复用代码,心智成本最低;
  • 组合方式减少了层级结构,心智成本较高。
  • 继承层级结构清晰,但依赖关系紧密,修改某个节点可能影响所有子节点,心智成本高。

总结

本文并不是要否定其他的经典理论,而是根据自己在实际开发业务过程中的一些思考,总结出来的一些感想。希望读者在阅读以上说明时,对各种模式都有一定的了解和认识。

以下是我的几点建议和思考:

  1. 每种模式都有其适用的场景和优缺点。在实际开发中,选择适合当前需求的模式比盲目追求某一种模式更加重要。
  2. 有时候,我们会为了追求“优雅的代码”而引入过多的继承和组合。实际上,这可能会增加系统的复杂度和心智负担。简单的复制,有时是更为高效和直接的解决方案。
  3. 在快速迭代的业务开发中,需求经常会发生变化。此时,过度复杂的继承和组合关系可能会成为负担。灵活的代码结构和适度的复制,能够更快速地响应变化。

希望读者也能抛开一种思维陷阱——用复用来证明复用的好处。实际上,可以用“复用的好处”来证明“复制”的好处。在实际开发中,重要的是找到最适合当前问题的解决方案,而不是拘泥于某种模式或理论(笑。。。)。

扩展阅读

Copy-and-paste programming - Wikipedia