您正在阅读我关于干净代码的书“清洗代码”的摘录。提供 PDF、EPUB、平装本和 Kindle 版本。立即获取副本。
了解如何将代码组织成模块或函数,以及何时引入抽象而不是重复代码,是一项重要技能。编写其他人可以有效使用的通用代码是另一项技能。拆分代码的原因与将代码保持在一起的原因一样多。在本章中,我们将讨论其中一些原因。
我们,开发者,讨厌同样的工作做两次。 DRY 是许多人的口头禅。然而,当我们有两到三段代码做同样的事情时,引入抽象可能还为时过早,无论它感觉多么诱人。
信息: 不要重复自己(DRY)原则要求“每条知识都必须在系统内有一个单一的、明确的、权威的表示”,这通常被解释为 严格禁止任何代码重复。
暂时忍受代码重复的痛苦;也许最终并没有那么糟糕,而且代码实际上并不完全相同。一定程度的代码重复是健康的,可以让我们更快地迭代和改进代码,而不必担心破坏某些东西。
当我们只考虑几个用例时,也很难想出一个好的 API。
管理具有许多开发人员和团队的大型项目中的共享代码很困难。一个团队的新要求可能不适用于另一个团队并破坏他们的代码,或者我们最终会得到一个具有数十个条件的无法维护的意大利面怪物。
假设团队 A 正在向他们的页面添加一个评论表单:名称、消息和提交按钮。然后,团队 B 需要反馈表,因此他们找到团队 A 的组件并尝试重用它。然后,团队 A 也想要一个电子邮件字段,但他们不知道团队 B 使用他们的组件,因此他们添加了一个必需的电子邮件字段并破坏了团队 B 用户的功能。然后,团队 B 需要电话号码字段,但他们知道团队 A 使用的组件没有该字段,因此他们添加了一个选项来显示电话号码字段。一年后,两个团队因破坏对方代码而互相憎恨,而且组件充满了条件,无法维护。如果两个团队都维护由较低级别的共享组件(例如输入字段或按钮)组成的单独组件,那么他们将节省大量时间并拥有更健康的关系。
提示:禁止其他团队使用我们的代码可能是个好主意,除非它被设计并标记为共享。 Dependency Cruiser 是一个可以帮助建立此类规则的工具。
有时,我们必须回滚抽象。当我们开始添加条件和选项时,我们应该问自己:它仍然是同一事物的变体还是应该分离的新事物?向模块添加太多条件和参数可能会导致 API 难以使用,代码也难以维护和测试。
重复比错误的抽象更便宜、更健康。
信息:请参阅 Sandi Metz 的文章《错误的抽象》以获得很好的解释。
代码的级别越高,我们在抽象它之前需要等待的时间就越长。低级实用抽象比业务逻辑更加明显和稳定。
代码重用并不是将一段代码提取到单独的函数或模块中的唯一原因,甚至不是最重要的原因。
代码长度通常被用作我们何时应该拆分模块或函数的指标,但大小本身并不会使代码难以阅读或维护。
将线性算法(即使是很长的算法)拆分为多个函数,然后依次调用它们,很少会使代码更具可读性。在函数(甚至文件)之间跳转比滚动更困难,如果我们必须研究每个函数的实现来理解代码,那么抽象就不正确。
信息: Egon Elbre 写了一篇关于代码可读性心理学的好文章。
这是一个示例,改编自 Google 测试博客:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
我对 Pizza 类的 API 有很多疑问,但让我们看看作者建议的改进:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
原本就复杂的事情现在变得更加复杂,一半的代码只是函数调用。这并不会使代码更容易理解,但确实使其几乎无法使用。文章没有展示重构版本的完整代码,或许是为了让观点更有说服力。
Pierre “catwell” Chapuis 在他的博文中建议添加评论而不是新功能:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
这已经比拆分版本好得多了。更好的解决方案是改进 API 并使代码更加清晰。皮埃尔建议,预热烤箱不应该成为 createPizza() 函数的一部分(我自己也烤了很多披萨,我完全同意!),因为在现实生活中,烤箱已经在那里了,而且可能已经因为之前的披萨而热了。皮埃尔还建议该函数应该返回盒子,而不是披萨,因为在原始代码中,盒子在所有切片和包装魔法之后消失了,我们最终手里拿着切片披萨。
烹饪披萨的方法有很多种,就像解决问题的方法有很多种一样。结果可能看起来相同,但某些解决方案比其他解决方案更容易理解、修改、重用和删除。
当所有提取的函数都是同一算法的一部分时,命名也可能是一个问题。我们需要发明比代码更清晰、比注释更短的名称——这不是一件容易的事。
信息:我们在避免注释章节中讨论注释代码,并在命名很难章节中讨论命名。
你可能在我的代码中找不到很多小函数。根据我的经验,拆分代码最有用的原因是更改频率和更改原因。
让我们从更改频率开始。业务逻辑的变化比效用函数更频繁。将经常更改的代码与非常稳定的代码分开是有意义的。
我们在本章前面讨论的评论表单就是前者的一个例子;将camelCase 字符串转换为kebab-case 的函数就是后者的一个示例。当出现新的业务需求时,评论表单可能会随着时间的推移而发生变化和分歧;大小写转换函数根本不可能改变,并且可以在很多地方安全地重用。
想象一下我们正在制作一个漂亮的表格来显示一些数据。我们可能认为我们永远不会再需要这个表设计,因此我们决定将表的所有代码保留在单个模块中。
下一个冲刺,我们的任务是向表中添加另一列,因此我们复制现有列的代码并更改其中的几行。下一个冲刺,我们需要添加另一个具有相同设计的表格。下一个冲刺,我们需要改变表格的设计......
我们的表格模块至少有三个更改原因,或职责:
这使得模块更难理解并且更难更改。展示性代码增加了很多冗长的内容,使得业务逻辑更难理解。要更改任何职责,我们需要阅读和修改更多代码。这使得迭代变得更加困难和缓慢。
将通用表作为单独的模块可以解决这个问题。现在,要向表中添加另一列,我们只需要了解并修改两个模块之一。除了其公共 API 之外,我们不需要了解有关通用表模块的任何信息。要更改所有表的设计,我们只需要更改通用表模块的代码,并且可能根本不需要触及各个表。
但是,根据问题的复杂性,从整体方法开始并稍后提取抽象是可以的,而且通常更好。
甚至代码重用也可以成为分离代码的有效理由:如果我们在一个页面上使用某些组件,我们可能很快就会在另一个页面上需要它。
将每个函数提取到自己的模块中可能很诱人。然而,它也有缺点:
我更喜欢将仅在一个模块中使用的小函数保留在模块的开头。这样,我们不需要导入它们以在同一个模块中使用,但在其他地方重用它们会很尴尬。
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
在上面的代码中,我们有一个组件(FormattedAddress)和一个函数(getMapLink()),它们仅在该模块中使用,因此它们定义在文件的顶部。
如果我们需要测试这些函数(我们应该!),我们可以将它们从模块中导出并与模块的主函数一起测试它们。
这同样适用于仅与特定函数或组件一起使用的函数。将它们放在同一个模块中可以更清楚地看出所有函数属于同一组,并使这些函数更容易被发现。
另一个好处是,当我们删除模块时,我们会自动删除其依赖项。共享模块中的代码通常会永远保留在代码库中,因为很难知道它是否仍在使用(尽管 TypeScript 使这变得更容易)。
信息:此类模块有时称为深层模块:封装复杂问题但具有简单API的相对较大的模块。深层模块的反面是浅层模块:许多需要彼此交互的小模块。
如果我们经常需要同时更改多个模块或函数,那么将它们合并到单个模块或函数中可能会更好。这种方法有时称为共置。
以下是几个托管示例:
以下是文件树如何随着共置而变化:
Separated | Colocated |
---|---|
React components | |
src/components/Button.tsx | src/components/Button.tsx |
styles/Button.css | |
Tests | |
src/util/formatDate.ts | src/util/formatDate.ts |
tests/formatDate.ts | src/util/formatDate.test.ts |
Ducks | |
src/actions/feature.js | src/ducks/feature.js |
src/actionCreators/feature.js | |
src/reducers/feature.js |
信息:要了解有关托管的更多信息,请阅读 Kent C. Dodds 的文章。
关于托管的一个常见抱怨是它使组件太大。在这种情况下,最好将某些部分连同标记、样式和逻辑一起提取到它们自己的组件中。
共置的想法也与关注点分离相冲突——这是一个过时的想法,导致 Web 开发人员将 HTML、CSS 和 JavaScript 保存在单独的文件中(并且通常保存在文件树的不同部分中)太长了,迫使我们同时编辑三个文件,甚至对网页进行最基本的更改。
信息:更改原因也称为单一职责原则,该原则规定“每个模块、类或函数都应该对功能的单个部分负责”由软件提供,并且该责任应该完全由类封装。”
有时,我们必须使用特别难以使用或容易出错的 API。例如,它需要按特定顺序执行多个步骤,或者调用具有多个始终相同的参数的函数。这是创建效用函数以确保我们始终做对的一个很好的理由。作为奖励,我们现在可以为这段代码编写测试。
字符串操作——例如 URL、文件名、大小写转换或格式——是很好的抽象候选者。最有可能的是,已经有一个库可以满足我们正在尝试做的事情。
考虑这个例子:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
需要一些时间才能意识到此代码删除了文件扩展名并返回基本名称。它不仅没有必要且难以阅读,而且还假设扩展名始终为三个字符,但事实可能并非如此。
让我们使用库(内置 Node.js 的路径模块)重写它:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
现在,很清楚发生了什么,没有神奇的数字,并且它适用于任何长度的文件扩展名。
其他抽象候选包括日期、设备功能、表单、数据验证、国际化等等。我建议在编写新的实用函数之前查找现有的库。我们经常低估看似简单功能的复杂性。
以下是此类库的一些示例:
有时,我们会得意忘形并创建既不能简化代码也不能缩短代码的抽象:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
另一个例子:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
在这种情况下我们能做的最好的事情就是应用全能的内联重构:用它的主体替换每个函数调用。没有抽象,没问题。
第一个示例变为:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
第二个例子变成:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
结果不仅更短且更具可读性;现在读者不需要猜测这些函数的作用,因为我们现在使用 JavaScript 原生函数和特性,而不需要自制的抽象。
在很多情况下,重复一点是有好处的。考虑这个例子:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
它看起来非常好,并且在代码审查期间不会提出任何问题。但是,当我们尝试使用这些值时,自动补全仅显示数字而不是实际值(参见插图)。这使得选择正确的值变得更加困难。
我们可以内联 baseSpacing 常量:
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
现在,我们的代码更少了,也更容易理解,并且自动补全显示了实际值(参见插图)。而且我认为这段代码不会经常更改——可能永远不会。
考虑表单验证函数的摘录:
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
很难理解这里发生了什么:验证逻辑与错误消息混合在一起,许多检查都是重复的......
我们可以把这个函数分成几个部分,每个部分只负责一件事:
我们可以将验证以声明方式描述为数组:
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
每个验证函数和运行验证的函数都非常通用,因此我们可以抽象它们或使用第三方库。
现在,我们可以通过描述哪些字段需要哪些验证以及当某个检查失败时显示哪些错误来为任何表单添加验证。
信息: 有关完整代码和此示例的更详细说明,请参阅避免条件章节。
我称这个过程“什么”和“如何”的分离:
好处是:
许多项目都有一个名为 utils.js、helpers.js 或 Misc.js 的文件,开发人员在找不到更好的位置时会在其中添加实用程序函数。通常,这些函数永远不会在其他地方重用,并永远保留在实用程序文件中,因此它不断增长。这就是怪物实用程序文件诞生的方式。
怪物实用程序文件有几个问题:
这些是我的经验法则:
JavaScript 模块有两种类型的导出。第一个是命名导出:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
可以这样导入:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
第二个是默认导出:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
可以这样导入:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
我确实没有看到默认导出有任何优势,但它们有几个问题:
信息:我们在其他技术章节的编写greppable代码部分详细讨论了greppability。
不幸的是,一些第三方 API,例如 React.lazy() 需要默认导出,但对于所有其他情况,我坚持使用命名导出。
桶文件是一个模块(通常命名为index.js 或index.ts),它重新导出一堆其他模块:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
主要优势是更清洁的进口。而不是单独导入每个模块:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
我们可以从桶文件导入所有组件:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
但是,桶文件有几个问题:
信息: TkDodo 详细解释了桶文件的缺点。
桶状锉刀的好处太小,不足以证明其使用的合理性,因此我建议避免使用它们。
我特别不喜欢的一种类型的桶文件是那些导出单个组件只是为了允许将其导入为 ./components/button 而不是 ./components/button/button。
为了攻击 DRYers(从不重复代码的开发人员),有人创造了另一个术语:WET,将所有内容写两次,或者 我们喜欢打字,建议我们应该在以下位置复制代码至少两次,直到我们用抽象替换它。这是一个笑话,我并不完全同意这个想法(有时重复一些代码两次以上是可以的),但它很好地提醒我们,所有美好的事物都最好适度。
考虑这个例子:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
这是代码干燥的一个极端例子,它不会使代码更具可读性或可维护性,特别是当大多数常量仅使用一次时。在这里看到变量名称而不是实际字符串是没有帮助的。
让我们内联所有这些额外的变量。 (不幸的是,Visual Studio Code 中的内联重构不支持内联对象属性,因此我们必须手动执行此操作。)
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
现在,我们的代码显着减少,并且更容易理解正在发生的事情,也更容易更新或删除测试。
我在测试中遇到了很多无望的抽象。例如,这种模式很常见:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
此模式试图避免在每个测试用例中重复 mount(...) 调用,但它使测试变得比实际需要的更加混乱。让我们内联 mount() 调用:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
此外,beforeEach 模式仅在我们想要使用相同的值初始化每个测试用例时才起作用,但这种情况很少发生:
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
为了避免在测试 React 组件时一些重复,我经常添加一个 defaultProps 对象并将其传播到每个测试用例中:
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
这样,我们就不会出现太多的重复,但同时每个测试用例都是隔离且可读的。测试用例之间的差异现在更加清晰,因为更容易看到每个测试用例的独特属性。
这是同一问题的更极端的变体:
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
我们可以像上一个示例中一样内联 beforeEach() 函数:
const findByReference = (wrapper, reference) => wrapper.find(reference); const favoriteTaco = findByReference( ['Al pastor', 'Cochinita pibil', 'Barbacoa'], x => x === 'Cochinita pibil' ); // → 'Cochinita pibil'
我会更进一步,使用 test.each() 方法,因为我们使用一堆不同的输入运行相同的测试:
function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: () => {} };
现在,我们已将所有测试输入及其预期结果收集到一个地方,从而可以更轻松地添加新的测试用例。
信息:查看我的 Jest 和 Vitest 备忘单。
抽象的最大挑战是在过于僵化和过于灵活之间找到平衡,并知道何时开始抽象事物以及何时停止。通常值得等待,看看我们是否真的需要抽象某些东西——很多时候,最好不要这样做。
有一个全局按钮组件很好,但如果它太灵活并且有十几个布尔属性在不同变体之间切换,那么它将很难使用。但是,如果过于严格,开发人员将创建自己的按钮组件,而不是使用共享的按钮组件。
我们应该警惕让其他人重用我们的代码。通常,这会在应该独立的代码库部分之间造成紧密耦合,从而减慢开发速度并导致错误。
开始思考:
如果您有任何反馈,请发送给我、发推文、在 GitHub 上打开问题或给我发送电子邮件至 artem@sapegin.ru。获取您的副本。
以上是清洗代码:分而治之,或合并并放松的详细内容。更多信息请关注PHP中文网其他相关文章!