编程的原则不要重复你自己_不要重复自己

编程的原则不要重复你自己

摘要 (Summary)

  • “Don’t repeat yourself” (DRY) is a widely accepted programming principle, but it has several limitations

    “不要重复自己”(DRY)是一种被广泛接受的编程原理,但是它有一些局限性
  • An alternative derivation of DRY is presented, that aims to alleviate some of these limitations

    提出了DRY的另一种派生形式,旨在减轻其中的一些局限性
  • Programs are made up of functions, and to make functions maximally reusable they should:

    程序由功能组成,并且为了最大程度地重用功能,它们应该:

    - Have the most lenient preconditions

    -有最宽松的前提

    - Have the most strict postconditions

    -具有最严格的后置条件

    - Perform the smallest amount of non-trivial work

    -执行最少的琐碎工作

If you think this article is too long, you can skip to Should I refactor my code? below

如果您认为本文太长,可以跳至我应该重构代码吗? 下面

介绍 (Introduction)

In software engineering, “clean code” is a sought after ideal. Code that is “clean” is easy to read, understand and maintain. Unfortunately, what clean code looks like is often disputed. Some commonly accepted software engineering best practices have lost sight of their original goals, or do not represent a complete picture. I believe Don’t Repeat Yourself (DRY) may be one of these principles.

在软件工程中,“干净的代码”是一种理想的追求。 “干净”的代码易于阅读,理解和维护。 不幸的是,干净的代码看起来经常是有争议的。 一些普遍接受的软件工程最佳实践忽视了其最初的目标,或者不能代表全部。 我相信“不要重复自己”(DRY)可能是这些原则之一。

DRY is a widespread and generally well accepted best practice in software engineering. The main stated advantage to DRY is that code does not need to be modified in multiple locations. If code has been de-duplicated, then bug-fixes and new features only need to be added once. In addition, code only needs to be tested once rather than multiple times. The end result is that code is less error prone and faster to write.

DRY是软件工程中广泛且普遍接受的最佳实践。 DRY的主要优点是无需在多个位置修改代码。 如果对代码进行了重复数据删除,则仅需要添加一次错误修复和新功能。 另外,代码仅需要测试一次,而无需多次测试。 最终结果是,代码不易出错 ,编写起来更快

Unfortunately, it is also well known that the overzealous application of DRY can lead to poor abstractions, and code that is difficult to modify. If DRY were to be taken to the extreme, then it’s aim is just to increase the entropy of written code and turn it into code golf. In some instances this is a good thing, for example: obfuscation, or minified JavaScript, but in most cases, code written in this style is obviously detrimental. This indicates that DRY has some limitations as a principle.

不幸的是,众所周知,过度使用DRY会导致不良的抽象以及难以修改的代码。 如果将DRY发挥到极致,那么它的目的只是增加已编写代码的熵并将其转化为代码高尔夫。 在某些情况下,这是一件好事,例如:混淆或缩小JavaScript,但在大多数情况下,用这种风格编写的代码显然有害。 这表明DRY原则上具有一些局限性。

This article will explore some of the limitations of DRY and provide an alternative perspective that does not have the same limitations.

本文将探讨DRY的一些局限性,并提供一个没有相同局限性的替代观点。

DRY的局限性 (Limitations of DRY)

One of the oft discussed limitations of DRY is that it can lead to poor abstractions. Heuristic solutions like the “rule of three” exist to alleviate this problem, where only code that is duplicated three or more times is worthwhile refactoring. This is not at all perfect. For example, the fourth replication may require an additional parameterization that the first three applications did not.

DRY经常讨论的局限性之一是它可能导致不良的抽象。 存在诸如“三个规则”之类的启发式解决方案来缓解此问题,在该问题中,只有重复三次或多次重复的代码才值得重构。 这一点都不完美。 例如,第四次复制可能需要前三个应用程序不需要的其他参数化。

Often ignored in this discussion, plain, non generic functions are the basic unit of code reuse, and relatively often these functions only have a single call site. Following a strict “rule of three” application, it would follow that a function with a single call site should instead be written inline, and only factored out once it has been written three times. Therefore there appear to exist some worthwhile abstractions, even if they do not reduce the repetition of code:

在此讨论中,普通的非泛型函数经常被忽略,它们是代码重用的基本单元,并且相对而言,这些函数通常只有一个调用站点。 遵循严格的“三个规则”应用程序,可以将具有单个调用站点的函数改为内联编写,并且仅在编写三次后才将其分解。 因此,似乎存在一些有价值的抽象,即使它们不会减少代码的重复:

// Large functions that have comments in them are// considered good candidates for refactoringfunction bigFunc() {// Do part A
console.log("Did part A");// Do Part B
console.log("Did part B");// Do Part C
console.log("Did part C");
}// It is recommended to split such a function into// smaller parts. Cited reasoning includes:// - Easier testing// - Self documenting code// - Reusabilityfunction doPartA() {
console.log("Did part A");
}function doPartB() {
console.log("Did part B");
}function doPartC() {
console.log("Did part C");
}function newBigFunc() {
doPartA();
doPartB();
doPartC();
}

Another argument against applying too much DRY is that overly generic code can be more difficult to understand and maintain. I’m a bit skeptical of how significant this really is, as ‘Difficulty’ is a very subjective concept and would vary from person to person, but I suppose there may be some truth to the claim that this negatively affects software development in some measurable capacity.

反对过多使用DRY的另一个论点是,过于通用的代码可能更难以理解和维护。 我有点怀疑这到底有多重要,因为“难度”是一个非常主观的概念,并且会因人而异,但我认为这种说法会对软件开发产生一定程度的负面影响,这可能有些道理。容量。

In addition, I dispute the assertion that using DRY is necessarily faster to write. Copy and paste is quick, and I suspect that the vast majority of developer time is not spent actually typing in the first instance. In the case of modification, a global find and replace works for actually writing the code (and for copying over any relevant tests).

另外,我对使用DRY一定更快地写入的说法提出异议。 复制和粘贴的速度很快,我怀疑大多数开发人员的时间并没有花在第一次打字时。 在进行修改的情况下,全局查找和替换可实际编写代码(并复制所有相关测试)。

Obviously copy and paste, and global find and replace are not the recommended tools for refactoring a program, but the concept here is that writing the code itself is not the bottleneck.

显然,复制和粘贴以及全局查找和替换不是重构程序的推荐工具,但是这里的概念是编写代码本身不是瓶颈。

To prove me wrong (or right) in this regard, someone would have to measure the time it takes to write out duplicated code versus non duplicated code. Additionally, they would need to show that there was a significant difference between them whilst proving that the difference wasn’t just due to chance. I doubt that such a study will be done for some time.

为了证明我在这方面是错误的(或正确的),某人必须测量写出重复代码与不重复代码所花费的时间。 另外,他们需要证明它们之间存在显着差异,同时证明差异不仅仅是由于偶然。 我怀疑这样的研究会持续一段时间。

从第一原则 (From First Principles)

Considering how widespread the principle of DRY is, there is little empirical evidence to say to what extent it improves code quality. The promotion of DRY is largely based on the subjective experiences of software engineers over the course of their work. As such it is unknown exactly how effective it really is.

考虑到DRY原理的普及程度,几乎没有经验证据表明DRY可以在多大程度上提高代码质量。 DRY的提升主要基于软件工程师在其工作过程中的主观经验。 因此,确切有效的确切程度尚不清楚。

Certainly, DRY is not an end goal. End goals include things such as reducing time to market, reducing cost, or increasing developer productivity. The quality of code can also play a part; it could be measured by the frequency and severity of bugs that affect users.

当然,DRY并非最终目标。 最终目标包括诸如缩短上市时间,降低成本或提高开发人员生产力之类的事情。 代码的质量也可以发挥作用; 它可以通过影响用户的错误的频率和严重性来衡量。

Since the empirical evidence is not of sufficient quality, the next best thing would be to start with some small, but fundamental assumptions about how code works, and then derive what good code should look like from those assumptions. Note that I will not discuss how writing code one way will fix errors that writing code in another way exposes, nor will I discuss lessons that I have learned writing software. The conclusions drawn in this article will be drawn without the need to have experienced real world code.

由于经验证据的质量不够高,因此下一个最好的方法是从关于代码如何工作的一些小而基本的假设开始,然后从这些假设中得出好的代码应该是什么样。 请注意,我不会讨论以一种方式编写代码将如何解决以另一种方式编写代码所暴露的错误,也不会讨论已学习编写软件的课程。 本文中得出的结论无需经验丰富的实际代码即可得出。

I will assume that a developer never deliberately introduces bugs into their programs, and would choose to write a program with no bugs if at all possible. This is actually quite an ambitious assumption. Some bugs are just not worth fixing under the scrutiny of a cost-benefit analysis; bugs that are extremely mild in nature, or are rare enough that they are never experienced by an end user might not be worth going through the trouble to fix. However, I will persist with this assumption for the purposes of this article.

我将假定开发人员从不刻意将错误引入其程序中 ,并且将尽可能选择编写没有任何错误的程序。 这实际上是一个雄心勃勃的假设。 在成本效益分析的监督下,有些错误根本不值得修复。 本质上非常温和或非常罕见以至于最终用户从未经历过的错误可能不值得解决。 但是,出于本文的目的,我将坚持这种假设。

In addition, I will assume that the harder it is to prove a program correct, the more likely it contains errors. I define a program as ‘hard’ to prove correct if that program requires a lot of symbols in some theorem proving language to ensure correctness. Conversely, a program that requires few symbols is therefore ‘easy’ to prove correct. I note then that the number of symbols required for a proof is proportional to the number of errors you would find along the way.

另外,我将假设证明程序正确的难度越大,包含错误的可能性就越大 。 如果程序需要某种定理证明语言中的大量符号来确保正确性,则我将程序定义为“难”以证明其正确性。 相反,需要很少符号的程序因此很容易证明正确。 然后我注意到,证明所需要的符号数量与沿途发现的错误数量成正比。

There’s an implicit assumption that during the proving process, bugs can be fixed without adding or removing symbols. In this way it makes sense to talk about an incorrect program whilst also talking about proving it correct.

有一个隐含的假设,即在证明过程中,无需添加或删除符号即可修复错误。 这样,在谈论不正确的程序的同时还要谈论证明其正确是有意义的。

In the biggest logical leap of this article, I will mention something something Curry-Howard, and state without much proof at all that the number of symbols required to prove a program correct is proportional to the number of symbols in the code itself. This is a huge assumption, and probably not at all correct in a lot of instances. However, it does give us a convenient proxy on the number of errors that are in a given piece of code: if the code is longer, or is more complex, then it has more errors.

在本文的最大逻辑飞跃中,我将提到一些库里霍华德(Curry-Howard),并声明根本没有太多证据证明证明程序正确所需的符号数量与代码本身中的符号数量成比例。 这是一个巨大的假设,在很多情况下可能根本不正确。 但是,它确实为我们提供了一段给定代码中的错误数量的便捷代理:如果代码更长或更复杂,则错误更多。

Note that in a functional language with dependent types, where the Curry-Howard isomorphism can actually apply, the proof of a function’s correctness and it’s implementation is literally the same code. So saying that the length of the proof is the length of the code is indeed a reasonable assumption to make.

请注意,在具有依赖类型的函数式语言中,实际上可以应用Curry-Howard同构的情况下,函数正确性及其实现的证明实际上是相同的代码。 因此,说证明的长度就是代码的长度确实是一个合理的假设。

So at least one way to reduce the number of errors in code will be to reduce the number of symbols in the code itself. This sounds a lot like DRY. Note that code that is terse does not fit the bill for having less errors, as it still contains the same number of symbols. In this way, naming, styling, and indentation are ignored.

因此,减少代码错误数量的至少一种方法是减少代码本身中的符号数量。 这听起来很像DRY。 请注意, 简洁的代码因错误较少而无法满足要求,因为它仍包含相同数量的符号。 这样,将忽略命名,样式和缩进。

我的建议 (My Recommendation)

Programs regardless of paradigm are made out of functions. Well, maybe not in some cases, but they must be made of something. And in order for a discussion about code reuse to be meaningful there must be something in a programming language that can be reused. I’ll use “function” to mean the smallest reusable part of a program

程序与范式无关,都是由函数组成的。 好吧,也许在某些情况下不是,但是它们必须由某种东西制成。 而为了使有关代码重用的讨论是有意义的,必须有东西在可重复使用的编程语言。 我将使用“功能”来表示程序的最小可重用部分

Perhaps to the chagrin of an Object Orientated programmer, I will specifically not talk about objects being reusable. Objects can have multiple methods or members, so if an object is reusable, then that means that there is something smaller that is reusable too.

也许让面向对象的程序员感到cha恼,我不会特别谈论对象是可重用的。 对象可以具有多个方法或成员,因此,如果对象可重用,则意味着也有一些较小的对象可重用。

So if we consider a function to be the smallest reusable part of a program, then in order for our program to have the maximum reuse, any given function should:

因此,如果我们认为函数是程序中最小的可重用部分,那么为了使我们的程序具有最大的重用性,任何给定的函数都应:

  • be able to be reused as much as possible, and

    能够尽可能地被重用

  • it should be as useful as possible, and

    它应该尽可能有用 ,并且

  • it should be used as much as possible.

    应该尽可能多地使用它。

In order to achieve these goals respectively:

为了分别实现这些目标:

  • A function should have the smallest possible set of preconditions.

    函数应具有尽可能小的前提条件集。
  • A function should have the largest set of postconditions.

    函数应具有最大的后置条件集。
  • A function should perform the smallest non trivial amount of work.

    一个功能应该执行最少的琐碎工作。

可能的最小前提条件集 (The Smallest Possible Set of Preconditions)

A maximally reusable function should be able to be called wherever it is applicable. Functions can only be called when the caller can fulfill the preconditions of the function. If the caller cannot fulfill the preconditions of a function, but calls the function anyway, then this is an error.

只要适用,就可以调用最大可重用的函数。 仅当调用者可以满足功能的前提条件时,才可以调用功能。 如果调用者不能满足功能的前提条件,但无论如何都会调用该功能,则这是一个错误。

I’ll provide a short and not at all rigorous proof that maximally reusable functions have minimal preconditions using contradiction. Assume that there exists a maximally reusable function f which specifies a precondition that is unnecessary. That means that there exist potentially valid calling contexts where the function cannot be called, because the precondition cannot be satisfied. f can be made more reusable by removing the unnecessary precondition. Because f is already maximally reusable, this is a contradiction.

我将提供一个简短而完全没有严格的证据,证明最大可重用的函数在使用矛盾时具有最小的前提条件。 假设存在一个最大可重用的函数f ,该函数指定了不需要的前提条件。 这意味着存在潜在的有效调用上下文,在该上下文中无法调用该函数,因为无法满足前提条件。 通过删除不必要的前提条件,可以使f更加可重用。 因为f已经可以最大程度地重用,所以这是一个矛盾。

In fact, this must be the only thing that determines if a function is able to be used in as many places as possible. The only other places that f could potentially be used cannot satisfy necessary preconditions. Since it is already known that the function f has a minimal preconditions, any call to f in these locations is invalid.

实际上,这必须是唯一确定功能是否能够在尽可能多的地方使用的东西。 f唯一可能使用的其他位置不能满足必要的前提条件。 由于已经知道函数f具有最小的前提条件,因此在这些位置对f任何调用都是无效的。

In other words a function with minimal preconditions can be used anywhere it would be valid to call such a function, and the only places it cannot be called are all invalid anyway.

换句话说,具有最小前提条件的函数可以在调用该函数有效的任何地方使用,并且唯一不能被调用的地方无论如何都是无效的。

示例1:泛型 (Example 1: Generics)

Say you have a function that takes the maximum of two integers. I use Haskell and Rust, because in a lot of cases, you can specify the preconditions as part of the function type signature.

假设您有一个最多接受两个整数的函数。 我使用Haskell和Rust,因为在很多情况下,您可以将前提条件指定为函数类型签名的一部分。

-- Haskell
maxInt :: Int -> Int -> Int
maxInt a b = if a > b then a else b// Rustfn max_int (a: i32, b: i32) -> i32 {if a > b { a } else { b }
}

In this example, the precondition that the numbers must be integers is unnecessary. The only preconditions that are truly required are that both arguments have the same type, and that they can be compared to each other. We can specify that both arguments have the same type with polymorphism, and specify that they can be compared using a type constraint:

在此示例中,数字必须为整数的前提是不必要的。 真正需要的唯一前提条件是两个参数都具有相同的类型,并且可以将它们彼此比较。 我们可以指定两个参数具有相同的多态类型,并指定可以使用类型约束进行比较:

-- Haskell
maxAny :: (Ord a) => a -> a -> a
maxAny a b = if a > b then a else b// Rust
fn
max_any<T: Ord> (a: T, b: T) -> T {if a > b { a } else { b }
}

The second examples are more reusable. This should be fairly obvious considering the second example is generic, where the first example is not. Not to say that I advocate making absolutely everything as generic as possible, only that more generic functions are more reusable. More specifically, the reason that generic functions are more reusable is because they specify a smaller set of preconditions.

第二个示例更可重用。 考虑到第二个示例是通用的,而第一个示例不是通用的,这应该是相当明显的。 并不是说我主张绝对使所有事物都尽可能通用,而只是说更多的通用函数更可重用。 更具体地说,泛型函数可重用的原因是因为它们指定了一组较小的前提条件。

Recommendation: Use generics to make code more reusable.

建议:使用泛型使代码更可重用。

Although the above reasoning is agnostic regarding the programming paradigm used, those who are familiar with Object Orientated design idioms may find this familiar. Reducing the preconditions by making a function more generic corresponds to the ‘I’ and ‘D’ of SOLID. The ‘Interface Segregation’ principle recommends directly reducing the interface size, whilst the ‘Dependency Inversion’ principle recommends using interfaces in the first instance.

尽管上述推理与所使用的编程范例无关,但熟悉面向对象设计习惯的人可能会发现这很熟悉。 通过使函数更具通用性来减少前提条件对应于SOLID的“ I”和“ D”。 “接口隔离”原则建议直接减小接口大小,而“依赖关系反转”原则建议首先使用接口。

Those that are familiar with functional programming may see that the second example has been universally quantified. If the type signature of the above example can be thought of as a predicate in first order logic, then a universally quantified predicate is a stronger statement than an unquantified one.

那些熟悉函数式编程的人可能会看到第二个示例已被普遍量化。 如果可以将上述示例的类型签名视为一阶逻辑中的谓词,那么与未量化的谓词相比,通用量化的谓词更强。

Recommendation: Use best practice for your preferred programming paradigm.

建议:将最佳实践用于您首选的编程范例。

示例2:不必要的输入 (Example 2: Unnecessary Input)

Although making functions more generic will make them more reusable, it does not cover every use case. Most notably passing too much data into a function limits it’s reuse. I use TypeScript in this example as it is usually easy to understand:

尽管使功能更通用将使它们更具可重用性,但并不能涵盖所有用例。 最值得注意的是,将过多的数据传递给函数会限制其重用。 我在此示例中使用TypeScript,因为它通常很容易理解:

// Typescriptclass Person {private name: string;private phoneNumber: string;private age: number;    constructor(name: string, phoneNumber: string, age: number) {this.name = name;this.phoneNumber = phoneNumber;this.age = age;
} textMessage(message: string) {
console.log(`Message sent to ${this.phoneNumber}`);
console.log(`The message was: "${message}"`);
}
}const person = new Person("John Doe", "0491 570 110", 35);
person.textMessage("Save on a new car with out promotional deal!");

In this case, the textMessage method takes a Person object as this, and a string representing the message. The textMessage function only uses the phoneNumber property of Person. Requiring a Person is an unnecessary precondition. The textMessage function can be made more reusable by removing that precondition:

在这种情况下, textMessage方法将Person对象作为this ,并将字符串表示消息。 textMessage函数仅使用PersonphoneNumber属性。 要求一个Person是不必要的前提。 通过删除该前提条件,可以使textMessage函数更加可重用:

function textMessage(phoneNumber: string, message: string) {
console.log(`Message sent to ${phoneNumber}`);
console.log(`The message was: "${message}"`);
}
textMessage("0491 570 110", "Save on a new car with out promotional deal!")

It should be possible to send a text message to any valid phone number, not just phone numbers that have been assigned to a particular person. It should also be possible to send a text message if the name or age of the person is unknown.

应该可以将文本消息发送到任何有效的电话号码,而不仅仅是已分配给特定人员的电话号码。 如果此人的姓名或年龄未知,也应该可以发送短信。

Recommendation: Pass the minimum required data into a function.

建议:将所需的最少数据传递给函数。

The larger the class is, the more unnecessary data will be passed to any given method. For example: if a class is responsible for two separate items, then methods which regard only the first item, or only the second will be passed unnecessary state as part of this.

类越大,将更多不必要的数据传递给任何给定方法。 例如:如果一个类负责两个单独的项目,那么只考虑第一个项目或仅考虑第二个项目的方法将作为this一部分传递给不必要的状态。

Note that this ties back in with the ‘S’ of SOLID. The ‘Single Responsibility’ principle recommends breaking classes with many responsibilities into smaller classes.

请注意,这与SOLID的“ S”联系在一起。 “单一责任”原则建议将具有许多职责的课程分为较小的课程。

Recommendation: Keep classes and data structures small.

建议:使类和数据结构较小。

其他例子 (Other Examples)

  • Functions that require more arguments than are necessary.

    需要更多参数的函数。
  • Non static methods that can be made static.

    可以使成为staticstatic方法。

  • Making a function async when it doesn’t need to be.

    在不需要时使函数async

  • Using mutable references when immutable references would suffice.

    在不可变引用足够时使用可变引用。

最大的后置条件集 (The Largest Set of Postconditions)

In order for a function to be maximally reusable, it must be useful in as many places as possible. Each potential call site may place requirements on the result of the function. Therefore, it would make sense that the more strict the postconditions, the more places the function can satisfy the requirements of the call sites.

为了最大程度地重用功能,它必须在尽可能多的地方有用。 每个潜在的呼叫站点可能会对功能结果提出要求。 因此,有意义的是,后置条件越严格,功能可以满足呼叫站点要求的位置就越多。

示例1:返回父类 (Example 1: Return the Parent Class)

In Object Orientated programming, a parent class can be used wherever a child class can be used, but not the other way around. To make the function as reusable as possible, return the parent class:

在面向对象的编程中,可以在可以使用子类的任何地方使用父类,但不能相反。 为了使函数尽可能可重用,请返回父类:

For functional programs, returning a base class would be the equivalent of returning an existentially quantified data type.

对于功能程序,返回基类将等同于返回存在的量化数据类型。

// Typescriptclass Child {
childFunc() {
console.log("I'm a Child");
}
}class Parent extends Child {
parentFunc() {
console.log("I'm a Parent");
}
}function factoryChild (): Child {return new Parent();
}function factoryParent (): Parent {return new Parent();
}const p1: Parent = factoryParent();// Error: Child does not implement parentFunc// const p2: Parent = factoryChild();const c1: Child = factoryParent();const c2: Child = factoryChild();

Note that it is often recommended elsewhere to use abstract factories which return interfaces, or virtual base classes instead of concretions. Returning the parent like I recommend in this case is therefore discouraged. If this makes you uncomfortable, you can always write a wrapper function to turn a concretion into an abstraction. This can’t be done the other way around:

请注意,通常建议在其他地方使用返回接口的抽象工厂,或使用虚拟基类而不是concretations。 因此不建议像我建议的那样返回父母。 如果这使您感到不舒服,则始终可以编写包装器函数以将构想转换为抽象。 不能用其他方法做到这一点:

// Convert a concretion to an abstractionfunction factoryChild2 (): Child {return factoryParent();
}// Cannot be done in reverse// function factoryParent2 (): Parent {// return factoryChild();// }

Alternatively if the factory function could return multiple concretions, then the return type has to be an abstraction, so the above code doesn’t even apply:

或者,如果工厂函数可以返回多个concrets,则返回类型必须是抽象,因此上面的代码甚至不适用:

class Parent2 extends Child {
parent2Func() {
console.log("I'm the second Parent");
}
}// Can't return a Parent type here. Returning// a Child is the most minimal postcondition.function abstractFactoryChild(): Child {if (Math.random() > 0.5) {return new Parent();
} else {return new Parent2();
}
}

Recommendation: Return the most specific type possible.

建议:返回最具体的类型。

示例2:验证参数 (Example 2: Validating Arguments)

In the general case, throwing exceptions is a normal part of programming. Some things are just not in our control, so we just do the best we can, handle it and clean up the mess. If a function validates arguments and returns early on failure, then this indicates that the preconditions are too lenient:

在一般情况下,抛出异常是编程的正常部分。 有些事情不是我们所能控制的,因此我们会尽力而为,处理并清理混乱。 如果函数验证参数并在失败时提早返回,则表明前提条件太宽松:

// Typescriptfunction mayHaveBadArgument(argument: number | null): string {if (argument === null) {throw "Bad argument";
}return argument.toString();
}

Perhaps this is too obvious an example and would not appear in the real world, I suspect something like the following is written more commonly:

也许这是一个太明显的例子,并且不会出现在现实世界中,我怀疑这样的写法更常见:

function mayHaveBadArgument(argument: number | null): string | null {if (argument === null) {return null;
}return argument.toString();
}

In this case, null is an invalid value, so why accept it as an argument at all? Maybe it would be better to just take a number and return a string, and make it up to the caller to check for the precondition.

在这种情况下, null是无效值,那么为什么要完全接受它作为参数呢? 可能最好只取一个数字并返回一个字符串,然后由调用方来检查前提条件。

function printNumber(argument: number): string {return argument.toString();
}

Note that this is a bit controversial for two reasons. Firstly it gives stricter preconditions and stricter postconditions, when we would prefer stricter postconditions and more lenient preconditions. Secondly, argument checking code might end up being duplicated at a lot of call sites.

请注意,这有两个方面的争议。 首先,当我们希望使用更严格的后置条件和更宽松的前提条件时,它给出了更严格的前提条件和更严格的后置条件。 其次,参数检查代码可能最终会在许多调用站点上重复出现。

Both of these problems can be fixed by creating a second function that only validates the arguments. Note here, that in this toy example it doesn’t really provide any benefits, but the benefit may become more obvious in larger examples.

这两个问题都可以通过创建仅验证参数的第二个函数来解决。 请注意,在此玩具示例中,它实际上并没有提供任何好处,但在较大的示例中,好处可能会变得更加明显。

function printNumberChecked(argument: number | null): string | null {if (argument === null) {return null;
}return printNumber(argument);
}

On one hand, the callers of printNumber do not have to check the return value for null. On the other hand, callers using printNumberChecked do not have to check argument for null. If argument is known not to be null, then use printNumber, otherwise, use printNumberChecked.

一方面, printNumber的调用者不必检查返回值是否为null 。 另一方面,使用printNumberChecked调用者不必检查argument是否为null 。 如果已知argument不为null ,则使用printNumber ,否则使用printNumberChecked

Recommendation: Don’t write a function that compromises lenient preconditions for strict postconditions or vice versa. Write two functions instead.

建议:不要为严格的后置条件而写一个折衷宽松的前提条件的函数,反之亦然。 而是编写两个函数。

This is the first recommendation that states that something is specifically a bad abstraction. Not every abstraction will be a good one, so there should be rules for things that specifically shouldn’t be done.

这是第一个建议,指出某些东西特别是不好的抽象。 并非每一个抽象都将是一个好的抽象,因此应该为某些特定的事情制定规则。

功能程序的高级用法 (Advanced Usage for Functional Programs)

Note that for most languages printNumberChecked may not be that good an abstraction. It’s not reusable enough and does some fairly trivial checking. I wouldn’t stress if this appears in your code a lot. A functional programmer may notice that T | null is a functor, and printNumberChecked is a poorly written version of map or fmap specialized for printNumber:

请注意,对于大多数语言, printNumberChecked可能不是一个很好的抽象。 它的可重用性不够,并且会进行一些琐碎的检查。 如果这在您的代码中出现很多,我不会强调。 功能性程序员可能会注意到, T | null T | null是函子,而printNumberChecked是专门用于printNumbermapfmap的写得不好的版本:

-- Haskell
printNumber :: Int -> String
printNumber = showprintNumberChecked :: (Functor f) => f Int -> f String
printNumberChecked = fmap printNumber// Typescriptfunction mapOptional<T, R>(func: (arg: T) => R): (arg: T | null) => R | null {return (arg: T | null) => {if (arg === null) {return null;
} return func(arg);
}
}const printNumberChecked = mapOptional(printNumber);
console.assert(printNumberChecked(null) === null);
console.assert(printNumberChecked(5) === '5');

Recommendation: Don’t use a function with side effects when a pure function will do.

建议:纯函数起作用时,请勿使用具有副作用的函数。

Note that this is generally not idiomatic code except for functional languages. I wouldn’t use this unless you, and all of you co-contributors are on the same page.

注意,除了功能语言之外,这通常不是惯用代码。 除非您和所有共同贡献者都在同一页面上,否则我不会使用它。

进行最少的平凡工作 (Perform the Smallest Amount of Non-trivial Work)

In order for a program to actually realize the benefits of reusable code, the program must actually reuse it’s components. Whilst the above principles will help determine what abstractions are good to take, this principle determines how they should be applied.

为了使程序真正实现可重用代码的好处,该程序实际上必须重用其组件。 尽管上述原理将有助于确定哪些抽象适合采用,但该原理决定了应如何应用它们。

If a function provides no benefit over writing the code inline, then I would consider it a trivial amount of work. That means trivial functions do not improve the ergonomics, prevent errors or improve any other measurable (or subjective) element of code.

如果一个函数比编写内联代码没有任何好处,那么我认为这是一件微不足道的工作。 这意味着琐碎的功能不会改善人机工程学,防止错误或改善任何其他可测量的(或主观的)代码元素。

Recommendation: Don’t write trivial abstractions.

建议:不要写琐碎的抽象。

A function does not perform the smallest possible non-trivial work if it can be broken down into at least two smaller non-trivial pieces. For the sake of argument, say that a non-trivial function called f could be refactored into two smaller non-trivial functions called a and b that could be recombined to form f. f must either contain duplicates of a and b inside it’s body, or it must be defined in terms of a and b.

如果一个功能可以分解为至少两个较小的非平凡片段,它就不会执行最小的非平凡作品。 为了论证,可以说一个称为f的非平凡函数可以重构为两个较小的名为ab非平凡函数,它们可以重组为ff必须在其体内包含ab重复项,或者必须根据ab进行定义。

Assume f is defined in terms of a and b. a and b would have smaller preconditions than f, so they would be able to be used in more places. If f is defined in terms of a and b, it either contains the the smallest amount of non trivial work to combine a and b, or it can be broken down further, in which case we do so and re-examine the components.

假设f是根据ab定义的。 ab先决条件比f小,因此它们可以在更多地方使用。 如果f是根据ab定义的,则它要么包含结合ab的最少的琐碎工作,要么可以进一步分解,在这种情况下,我们可以进行重新检查。

Either way, a function either contains duplicate code, contains preconditions that are too strict, or it performs the smallest amount of non-trivial work. In other words, large functions can be broken down into pieces to make them more reusable.

无论哪种方式,一个函数要么包含重复的代码,要么包含过于严格的前提条件,要么执行最少量的非平凡工作。 换句话说,可以将大型功能分解为多个部分,以使其更加可重用。

例子 (Examples)

I won’t talk much more about this topic as there appears to be a broad consensus that smaller composable functions are better than larger ones. I’ll link to NASA’s ten rules for safety critical software, which states that functions written in C should not have more than a single page’s worth of code in them.

关于这个主题,我将不做更多讨论,因为似乎已经达成了广泛的共识,即较小的可组合功能优于大型可组合功能。 我将链接到NASA关于安全性至关重要的软件的十条规则 ,这些规则指出,用C编写的函数中所包含的代码不应超过一页的代码价值。

Recommendation: Break large functions into smaller ones.

建议:将大型功能分解为较小的功能。

我应该重构代码吗? (Should I Refactor my Code?)

  • I have two similar functions, should I refactor them into one?If you can combine the two functions without weakening the postconditions, or strengthening the preconditions: then go ahead. If the function is too big, consider splitting it into parts.

    我有两个类似的功能,是否应该将它们重构为一个? 如果您可以在不削弱后置条件或增强前提条件的情况下结合使用这两个功能,请继续。 如果功能太大,请考虑将其拆分为多个部分。

  • I have two identical functions, should I refactor them into one?Yes.

    我有两个相同的功能,我应该将它们重构为一个吗? 是。

  • I have a function that I think could be refactored based on the above principles but it isn’t duplicated anywhere, should I do it?If you can weaken the preconditions, strengthen the postconditions, or if you think the function is too big, it should be OK to refactor. This assumes that any requirement could change anywhere in your program at any time.

    我有一个可以根据上述原则进行重构的功能,但是在任何地方都不能重复使用,我应该这样做吗? 如果可以削弱前提条件,可以增强后置条件,或者如果您认为功能太大,则可以重构。 假设任何要求都可以随时在程序中的任何地方更改。

  • I have a function that I think might be big enough that it can be split into parts, but it’s borderline. Should I split it?The triviality of a function is subjective. If you think that it improves ergonomics, makes the code easier to read, or reduces the possibility of errors compared to writing it inline, do it.

    我认为我的功能可能足够大,可以拆分为多个部分,但这只是临界点。 我应该拆分吗? 函数的琐碎性是主观的。 如果您认为与内联编写相比,它可以改善人体工程学,使代码更易于阅读或减少出错的可能性,请执行此操作。

  • I have a function that could be more generic, should I do it?If you don’t weaken the postconditions in doing so, or make the function too big, then it should be fine. Caveat: sometimes the function already does it’s job and you don’t need it to be generic. Then it might not be worth the effort.

    我有一个更通用的函数,应该这样做吗? 如果您不这样做会削弱后置条件,或者使函数太大,那么应该没问题。 注意:有时函数已经起作用,您不需要泛型。 那么可能不值得付出努力。

  • I have a function where the return type could be made more specific, should I do it?If you don’t weaken the preconditions, or make the function too big, then go ahead.

    我有一个可以使返回类型更具体的函数,我应该这样做吗? 如果您不削弱前提条件或使函数太大,请继续。

  • What about return type polymorphism? Doesn’t that make code more reusable while making the postconditions weaker?Return type polymorphism is equivalent to passing an (often zero sized) type as an argument, therefore weakening the preconditions. The three principles do not state if this is a good or a bad thing. Consider having both a specialized variant, and a generic one for different circumstances.

    返回类型多态性呢? 这是否会使代码更可重用,同时使后置条件更弱? 返回类型多态性等效于传递(通常为零大小)类型作为参数,因此削弱了前提条件。 这三个原则没有说明这是好事还是坏事。 考虑同时具有专用变体和针对不同情况的通用变体。

  • I have an object that might be too large, should I split it into two?If at least one of the methods does not need access to the whole object, consider splitting it. Exception: delegation, getters/setters.

    我的对象可能太大,应该将其拆分为两个吗? 如果至少一种方法不需要访问整个对象,请考虑将其拆分。 例外:委派,获取者/设置者。

  • Is XXX is a good abstraction?If you can define the preconditions, postconditions, and invariants well, then it should be OK, otherwise: probably not.

    XXX是很好的抽象吗? 如果可以很好地定义前置条件,后置条件和不变量,则应该可以,否则:可能不行。

  • Is XXX object a good abstraction?Examine the methods individually, and apply the principles above. If all of the methods individually are OK, then the whole object should be too. This assumes that you only access the object through its methods.

    XXX对象是好的抽象吗? 分别检查方法,并应用上述原理。 如果所有方法单独都可以,那么整个对象也应该是。 假定您仅通过对象的方法访问该对象。

结论 (Conclusion)

DRY is a programming principle that has many limitations. An often cited limitation is the capacity of DRY to create inefficient abstractions. Heuristic solutions have been adopted to try to find a good medium. This article presents a different way of deriving DRY from some basic assumptions. It also provides three principles for writing abstractions that aim to be reusable. This allows programmers to write code with no information about the calling context whilst providing some guarantees that the code can and will be reused as many times as possible.

DRY是一种编程原则,有很多限制。 一个经常被引用的限制是DRY创建无效抽象的能力。 已采用启发式解决方案来尝试寻找良好的媒介。 本文提出了一些从一些基本假设推导DRY的方法。 它还为编写旨在实现可重用的抽象提供了三个原则。 这允许程序员在编写代码时不提供有关调用上下文的任何信息,同时提供一些保证,使该代码可以并且将被尽可能多地重用。

This post was originally posted on Andrew’s Notepad

该帖子最初发布在安德鲁的记事本上

翻译自: https://medium.com/swlh/dont-don-t-repeat-yourself-799c6a6feaa1

编程的原则不要重复你自己

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在机考中,避免自己的代码与别人重复并查出作弊是非常重要的。以下是一些建议: 1. 自主思考和独立编程:在机考中,尽量独立思考问题,并自己编写代码。避免直接复制粘贴他人的代码或从外部获取完整的解决方案。尽可能根据自己的理解和思路来解决问题。 2. 不要共享代码:在机考过程中,避免与其他考生共享自己的代码或讨论解题思路。保持独立工作,并避免与他人交流答案的机会。这样可以防止自己的代码与别人的代码相似度过高。 3. 注意代码风格和结构:编写代码时,注意良好的代码风格和结构。采用规范的命名方式、适当的注释和缩进等,使得自己的代码与他人的代码在风格上有所差异,减少相似度。 4. 自测和调试:在编写代码的过程中,进行自测和调试,确保代码的正确性和可靠性。这样可以避免因为错误的代码导致被怀疑作弊。 5. 遵守考试规则:在参加机考之前,仔细阅读并理解考试规则和要求。遵守考试规则是非常重要的,不仅可以避免被查出作弊,还能够建立诚信的形象。 6. 尊重知识产权和学术道德:避免使用他人的代码或解决方案,尊重知识产权和学术道德。在机考中,展示自己的能力和独立思考的能力是非常重要的。 总的来说,避免自己的代码与别人重复并查出作弊的关键是独立思考、保持诚实原则并遵守考试规则。只要按照正当的方式进行考试,尊重知识产权和学术道德,就能够避免作弊问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值