No history yet

代码复用与组合

告别重复:编写优雅代码的艺术

当你刚开始学习编程时,目标很明确:让代码跑起来。但随着项目越来越复杂,你会发现自己总是在不同的地方写着相似的逻辑。这不仅浪费时间,也让维护和调试变得像一场噩梦。一段逻辑的修改,可能需要你在十几个地方同步更新。

为了解决这个问题,软件开发领域诞生了一个核心原则:。它的理念很简单:系统中的每一处知识都必须有单一、明确、权威的表示。换句话说,不要重复你自己。

想象一下,你在为一个电商网站计算商品价格,需要分别处理普通商品和促销商品。你可能会写出这样的代码:

// 不好的示范:大量重复
function calculateNormalPrice(basePrice) {
  const tax = 0.06;
  const shipping = 5;
  let finalPrice = basePrice * (1 + tax);
  finalPrice += shipping;
  console.log(`最终价格是: 💲${finalPrice.toFixed(2)}`);
  return finalPrice;
}

function calculatePromoPrice(basePrice) {
  const tax = 0.06;
  const shipping = 5;
  let finalPrice = basePrice * 0.8 * (1 + tax); // 促销打八折
  finalPrice += shipping;
  console.log(`促销最终价格是: 💲${finalPrice.toFixed(2)}`);
  return finalPrice;
}

注意到了吗?除了计算折扣的部分,税率和运费的逻辑是完全一样的。根据 DRY 原则,我们可以把通用逻辑抽象成一个更灵活的函数:

// 好的示范:提取通用逻辑
function calculatePrice(basePrice, discount = 1) {
  const tax = 0.06;
  const shipping = 5;
  let finalPrice = basePrice * discount * (1 + tax);
  finalPrice += shipping;
  return finalPrice;
}

const normalPrice = calculatePrice(100);
const promoPrice = calculatePrice(100, 0.8);

console.log(`最终价格是: 💲${normalPrice.toFixed(2)}`);
console.log(`促销最终价格是: 💲${promoPrice.toFixed(2)}`);

通过一个 discount 参数,我们用一个函数就解决了两种情况,代码变得更简洁、更易于维护。如果税率或运费规则改变,我们只需要修改一个地方。

然而,DRY 并非绝对的。有时,为了避免错误的抽象,适度的重复比引入复杂的依赖关系更好。关键在于判断重复的逻辑是否真的代表同一份“知识”。

组合与继承:选择正确的工具

在面向对象编程中,我们通常通过两种方式实现代码复用:继承和组合。

继承是一种 “is-a”(是一个)的关系。例如,Poodle (贵宾犬) “is-a” Dog (狗)。子类自动获得父类的所有属性和方法,这是一种紧密的耦合关系。

组合是一种 “has-a”(有一个)的关系。例如,一辆 Car (汽车) “has-a” Engine (引擎)。汽车通过包含一个引擎实例来获得动力,而不是继承自引擎。这是一种更松散、更灵活的关系。

继承的问题在于它的刚性。如果父类的设计发生改变,所有子类都会受到影响。更糟糕的是,当子类不需要父类的某些功能时(比如鸵鸟不会飞),就会产生混乱。这就是为什么许多经验丰富的开发者提倡“组合优于继承”的原则。

将代码分解成专注的、可重用的模块。清晰地命名函数和变量,以便立即理解。移除冗余代码并强制执行一致的风格。随着项目的发展,定期重新审视和调整函数。把它想象成整理一张杂乱的桌子——整理好的工具能加快每一项任务的速度。

模块化与高阶函数

除了面向对象,我们还可以通过函数式编程的思路来实现代码复用。核心工具是和模块化。

模块化是指将程序分解为独立的、可互换的“模块”,每个模块包含执行特定功能所需的一切。在现代 JavaScript (ES6) 中,我们可以使用 exportimport 来创建和使用模块。

// utils.js (模块)
export function logWithTimestamp(message) {
  const timestamp = new Date().toISOString();
  console.log(`${timestamp}: ${message}`);
}

// main.js (使用模块)
import { logWithTimestamp } from './utils.js';

logWithTimestamp('用户已登录。');
logWithTimestamp('数据加载完成。');

在这里,utils.js 文件就是一个模块,它导出了一个可重用的函数。任何其他文件都可以导入并使用这个函数,而无需关心它的内部实现。这就是命名空间 (Namespace) 的概念,模块创建了独立的命名空间,避免了全局变量污染和命名冲突。

而高阶函数则将复用的思想提升到了新的层次。它允许我们将行为(逻辑)作为参数传递。例如,我们可以编写一个通用的数据处理函数,它接受一个“规则”函数作为参数。

// 高阶函数,用于创建带特定前缀的日志记录器
function createLogger(prefix) {
  // 返回一个新的函数,这个新函数“记住”了 prefix
  return function(message) {
    console.log(`[${prefix}] ${message}`);
  }
}

// 创建两个不同的日志记录器
const systemLogger = createLogger('System');
const errorLogger = createLogger('Error');

systemLogger('应用启动');
errorLogger('数据库连接失败');

createLogger 就是一个高阶函数,因为它返回了另一个函数。我们通过它复用了“创建带前缀的日志记录器”这一行为,轻松地生成了多个具有特定功能的函数。这种方式比复制粘贴代码或使用复杂的类结构要灵活得多。

Quiz Questions 1/4

DRY (Don't Repeat Yourself) 原则的核心思想是什么?

Quiz Questions 2/4

在面向对象编程中,“组合优于继承”原则的主要原因是什么?

掌握代码复用和组合的艺术,是从“会写代码”到“写好代码”的关键一步。它能让你的代码库更健康、更灵活,也让你作为开发者变得更高效。