清洁代码之道:一份实用关于如何编写和维护干净整洁的好代码的的方法 The Art Of Clean Code...

Posted 禅与计算机程序设计艺术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了清洁代码之道:一份实用关于如何编写和维护干净整洁的好代码的的方法 The Art Of Clean Code...相关的知识,希望对你有一定的参考价值。

我们大多数程序员都发现自己处于必须使用杂乱代码的情况,这使得我们很难理解我们正在阅读的各个行的功能。有时,我们会问自己为什么要对变量或调用进行某些更改,但我们害怕干预,因为害怕破坏生产环境中使用的代码。

生产代码:在我们的服务器或产品的生产版本上运行的代码,即我们的产品或服务的真实受众在真实环境中使用的代码。

此外,我们必须承认,在某些情况下,我们是当今困扰我们的东西的创造者,我们宁愿重新编码也不愿添加新功能。

这些情况应该让我们得出这样的结论:稍后修复代码可能不是一个好主意,我们可以做一些小的改动或投入实践。这将帮助我们构建更易于理解的代码,并且将来更容易处理。

在etermax,我们始终致力于生成干净的代码,换句话说,如果需要,易于理解、更改和扩展的代码。这样,如果我们必须继续我们同行所做的一个功能,我们将能够轻松地理解它是如何工作的,并且我们将确定我们的修改不会在我们没有意识到的情况下影响系统的其他部分。在许多情况下,没有必要与我们的同行检查该功能是如何构建的,因为代码本身会逐行向我们解释发生了什么。

让我们从一个不太干净的例子开始

在构建方法时,我们喜欢让它们简短而甜美,以实现一个目标为导向。

我们以下面的函数为例:

private void ReceiveDamage(float damage)

    _life -= damage;
    if (_life <= 0)
    
        _view.ShowDeath();
        //Add permanent injuries
        
        //Reduce experience by half on death
        _experiencePoints /= 2;
        //Lose 5 coins
        _accumulatedCoins = Math.Max(0, _accumulatedCoins - 15);
    
    else
    
        _view.ShowReceiveDamageFeedback();
        
        //Enter injured condition if life is low
        if (_life <= _maxLife * 0.3f)
        
            //injured modifiers
            _movementSpeed = 0.5f;
            _attackSpeed = _weapon.GetAttackSpeed() * 0.6f;
            _view.ShowCriticalCondition();
        
    

我们无法一眼看懂里面发生了什么。我们必须在它的每一行上停下来,主要是如果存在名称不那么直观的变量,以区分方法的目的。

即使有一些有用的评论,在这个例子中也不是所有的行都被评论了,并且像“增加永久伤害”和“失去 5 个硬币”这样的评论也没有更新为最新的变化。这迫使我们将真正解释片段的评论与导致混淆的评论区分开来。出于这个原因,我们可以考虑减少行数,并专注于确保我们的每个方法都遵循单级抽象原则。换句话说,每个函数都应该处理与一个抽象级别相关的概念。我们可以通过提取隐藏与当前分析的上下文无关的细节或实现的新方法来实现这一点。例如,如果我们对上述代码执行此过程,我们可以获得以下内容:

private void ReceiveDamage(float damage)

    LowerLife(damage);
    if (LifeBelow0())
    
        _view.ShowDeath();
        //Add permanent injuries


        //Reduce experience by half on death
        OnDeath();
    
    else
    
        _view.ShowReceiveDamageFeedback();
        
        //Enter injured condition if life is low
        OnDamage();
    



private void OnDamage()

    if (LifeBelow30Percent())
    
        //injured modifiers
        Injured();
        _view.ShowCriticalCondition();
    

通常,我们还旨在减少此方法提取期间的代码重复。例如,如果用于执行相同计算或检查或执行某种行为的代码片段在整个代码库中多次出现,则最好在一种方法中提取它并用调用替换它的所有外观。这样,这些方法将在我们下次需要复制该行为或检查时可用,从而加快我们的工作并避免查找所述片段以将其复制并粘贴到新位置。更重要的是,我们还会防止由于业务需求发生变化而需要对这些方法进行更改时出现错误。例如,如果我们没有提取函数,我们将冒险只修改一些可以找到此代码的地方,

我们不将这种做法限制在功能上;我们也将它们应用于一般的类。在这种情况下,我们遵循一个重要的概念:SOLID原则。我们要强调的是,“S”代表单一职责。根据这个原则,我们应该确保类只有一个职责,并且它的所有方法都与它相关。出于这个原因,如果我们正在构建一个类并且遇到不符合该类目的的功能,我们会将它们提取到新的类中。这样,我们将避免在难以找到所需片段的情况下编写冗长的脚本,并且在不适用创建新类的情况下选择类时我们将更加确定。

方法名称和注释

回到前面的例子,我们可以看到,即使我们设法总结了我们的功能并更有效地分离了不同的上下文,我们还没有实现一个干净易懂的代码。这给我们带来了另一个问题:命名方法。如示例中所示,如果我们的方法没有描述性名称,我们仍然被迫检查它们中的每一个并通过它们的行来掌握所述代码片段的功能。然而,如果我们花时间给我们的函数起描述性的名字,我们可以得到类似的东西:

private void ReceiveDamage(float damage)

    ReduceLifeBy(damage);
    if (HasDied())
    
        _view.ShowDeath();
        ApplyDeathPenalties();
    
    else
    
        _view.ShowReceiveDamageFeedback();
        CheckCriticalCondition();
    



private void CheckCriticalCondition()

    if (LifeBelowCriticalThreshold())
    
        ApplyInjuredPenalties();
        _view.ShowCriticalCondition();
    

实现这些小改动只用了几分钟,我们实现了一个一眼就能理解的代码。此外,现在只有当我们想了解特定实现或更改它们时,才需要浏览这些新方法的定义。

我们还注意到用于解释此代码的注释不再需要,因此我们选择删除它们。我们通常认为注释是一种症状,表明代码的结构方式可能不正确。例如,它们可能表明我们可以在某个地方提取方法,或者我们应该问自己如何简化该片段或使其更具描述性。

遵循这些做法会不断阻止注释的生成和维护,如上所示,这些注释会在代码未更新时对代码实际执行的操作产生混淆。

魔术数字

让我们仔细看看其中一个功能,以便我们继续评估可能的改进:

private void ApplyInjuredPenalties()

    _movementSpeed = 0.5f;
    _attackSpeed = _weapon.GetAttackSpeed() * 0.6f;

很难确定 0.5f 和 0.6f 的含义,为什么要添加这些数字,以及它们是否在整个代码中重复。出于这个原因,如果我们要更改这些值,我们将不得不寻找它的每一次出现。在这些情况下,我们选择添加一个具有描述性名称的常量。对于此示例,它可能如下所示:

private const float InjuredSpeed = 0.5f;
private const float InjuredAttackSpeedReduction = 0.6f;


private void ApplyInjuredPenalties()

    _movementSpeed = InjuredSpeed;
    _attackSpeed = _weapon.GetAttackSpeed() * InjuredAttackSpeedReduction;

代码异味:如何识别不良代码?

一般来说,脏代码的指标有很多,比如:

  • 刚性:更改代码是困难的,因为每次我们想要进行更改时,我们都必须对代码的其他部分进行更改,这在很多情况下与原始更改不完全相关。

  • 脆弱性:每次我们进行更改时,我们的代码部分都会损坏,引入新的错误和意外行为。

  • 不必要的复杂性:很多时候,有比实施的更简单的解决方案。有时,使用糟糕的代码会迫使我们寻找不是最合适的解决方案,但要避免破坏以不理想方式添加的其他功能。

  • 不必要的重复:如前所述,相同的代码片段存在于项目的各个部分,因此无法找到所有重复的片段,以防我们需要修改它们。

  • 不透明度:代码难以理解。在处理一个部分时,我们必须花时间尝试了解它的作用、它的依赖关系、我们可以修改和不能修改的内容等等。

维护代码

我们必须澄清,试图编写好的代码是不够的;我们必须保持它的质量。这解释了童子军规则的重要性,它说我们应该始终让代码比我们发现的更好。这条规则促使我们做出一些小的改变——就像我们在整篇文章中提到的那样——这样在某些类上的工作就会变得越来越简单,不仅对我们来说,而且对我们未来可能需要做出改变的同事来说也是如此。

结论

正如整篇文章所讨论的,我们可以遵循许多实践来创建易于理解、扩展和维护的代码。通过提高对这些实践的认识,我们可以创建一个旨在保持代码干净、整洁和不断更新的团队。此外,遵循这些做法可确保每个人都感到舒适并防止冲突。通过这种方式,我们为日常工作奠定了更坚实的基础,促进了团队合作,构建了错误更少的功能,并节省了尝试理解我们想要修改的现有代码的时间。

【更多阅读:禅与计算机程序设计艺术】

  1. 清洁代码之道:一份实用关于如何编写和维护干净整洁的好代码的的方法 The Art Of Clean Code

  2. 来自软件架构大师的 4 个真理

  3. 程序员架构修炼之道:软件架构设计的37个一般性原则

  4. 软件架构设计的核心:抽象与模型、“战略编程”

  5. 软件架构的本质

  6. 快看软件架构风格总结: 各种历史和现代软件架构风格的快速总结

  7. 软件架构师成长之路: Master Plan for becoming a Software Architect

  8. 软件架构设计杂记:  好作品是改出来的,好的代码是不断重构打磨出来的, 心性是历经艰难困苦修炼出来的


Clean Code: A practical approach

How to generate and maintain good code?

Most of us programmers have found ourselves in a situation where we have to work with a messy code, making it difficult to understand the function of the various lines we are reading. On occasions, we ask ourselves why certain changes are being made to variables or calls, but we are scared to intervene out of fear of breaking the code used in the production environment.

Production code: the code that runs on the production versions of our servers or products, i.e., the one that is being used by the real audience of our product or service in a real environment.

Also, we must admit that, in some cases, we were the creators of what troubles us nowadays, and we would rather code again than add new functionalities.

Those situations should make us reach the conclusion that fixing the code later on might not be a great idea, and that we could make small changes or put practices in use instead. This will help us build a more understandable code, and it will be easier to work on in the future.

At etermax we always aim at generating clean code, in other words, one that’s easy to understand, change, and extend if needed. This way, if we have to continue a feature made by our peers, we will be able to understand how it works easily, and we will be certain that our modifications will not affect other parts of the system without us realizing it. On many occasions, it won’t be necessary to check with our peers how the feature was built, as the code itself will explain to us, line by line, what’s going on.

Let’s start with a not-so-clean example

When building methods, we like to keep them short and sweet, oriented towards following one goal.

Let’s take the following function as an example:

private void ReceiveDamage(float damage)

    _life -= damage;
    if (_life <= 0)
    
        _view.ShowDeath();
        //Add permanent injuries
        
        //Reduce experience by half on death
        _experiencePoints /= 2;
        //Lose 5 coins
        _accumulatedCoins = Math.Max(0, _accumulatedCoins - 15);
    
    else
    
        _view.ShowReceiveDamageFeedback();
        
        //Enter injured condition if life is low
        if (_life <= _maxLife * 0.3f)
        
            //injured modifiers
            _movementSpeed = 0.5f;
            _attackSpeed = _weapon.GetAttackSpeed() * 0.6f;
            _view.ShowCriticalCondition();
        
    

We can’t understand what’s going on inside of it at a glance. We have to stop on each of its lines, mostly if there are variables whose names are not so intuitive, to distinguish the purpose of the method.

Even if there are some helpful comments, not all the lines are commented on in this example, and some comments like “Add permanent injuries” and “Lose 5 coins” haven’t been updated with the latest changes. This forces us to distinguish comments that actually explain a fragment from those leading to confusion. For this reason, we could consider cutting down the amount of lines and focusing on making sure each of our methods follow the Single Level of Abstraction principle. In other words, each function should deal with concepts related to just one level of abstraction. We can achieve this by extracting new methods that hide details or implementations irrelevant to the context we are currently analyzing. For example, if we carried out this process for the code mentioned above we could obtain the following:

private void ReceiveDamage(float damage)

    LowerLife(damage);
    if (LifeBelow0())
    
        _view.ShowDeath();
        //Add permanent injuries

        //Reduce experience by half on death
        OnDeath();
    
    else
    
        _view.ShowReceiveDamageFeedback();
        
        //Enter injured condition if life is low
        OnDamage();
    


private void OnDamage()

    if (LifeBelow30Percent())
    
        //injured modifiers
        Injured();
        _view.ShowCriticalCondition();
    

Usually, we also aim to reduce code repetition during this method extraction. For example, if a fragment of code to carry out the same calculations or checks, or execute a certain behavior, appears multiple times throughout a codebase, it would be ideal to extract it in one method and replace all its appearances with a call. This way, these methods will be available the next time we need to replicate that behavior or check, speeding up our work and avoiding having to look up said fragment to copy and paste it in a new place. More importantly, we will also prevent errors from arising when changes need to be made to these methods because the needs of the business have changed. For instance, if we didn’t have extracted functions, we would risk modifying only some places where this code could be found, making only some parts of our application behave as expected.

We don’t limit this kind of practice to functions; we also apply them to classes in general. In this case, we follow an important concept: the S.O.L.I.D. principles. We’d like to stress that the ‘S’ stands for Single Responsibility. According to this principle, we should make sure classes have only one responsibility, and that all its methods are related to it. For this reason, if we’re building a class and encounter functions that are not in line with the purpose of said class, we will extract them into new classes. This way, we will avoid having long scripts where finding the fragments we need is difficult, and we will have more certainty when choosing classes in cases where creating a new class doesn’t apply.

Method names and comments

Going back to the previous example, we can see that even if we managed to summarize our functions and separate the different contexts more effectively, we have not achieved a clean and understandable code yet. This brings us to another issue: naming methods. As seen in the example, if our methods don’t have descriptive names, we are still forced to inspect each of them and go through their lines to grasp the functionality of said fragment of code. However, if we take the time to give our functions descriptive names, we can obtain something like:

private void ReceiveDamage(float damage)

    ReduceLifeBy(damage);
    if (HasDied())
    
        _view.ShowDeath();
        ApplyDeathPenalties();
    
    else
    
        _view.ShowReceiveDamageFeedback();
        CheckCriticalCondition();
    


private void CheckCriticalCondition()

    if (LifeBelowCriticalThreshold())
    
        ApplyInjuredPenalties();
        _view.ShowCriticalCondition();
    

Implementing these small changes only took a couple minutes, and we achieved a code that’s understandable at a glance. In addition, navigating the definitions of these new methods is now necessary only when we want to know particular implementations or change them.

We also noted that the comments used to explain this code were no longer necessary, so we chose to remove them. We normally think comments are a symptom indicating that the way the code was structured may not be the right one. For example, they may indicate that we can extract methods in a certain place, or that we should ask ourselves how we can simplify that fragment or make it more descriptive.

Following these practices constantly prevents the generation and maintenance of comments that, as seen above, generate confusion regarding what the code actually does when they’re not updated.

Magic numbers

Let’s have a closer look at one of these functions so we can continue assessing possible improvements:

private void ApplyInjuredPenalties()

    _movementSpeed = 0.5f;
    _attackSpeed = _weapon.GetAttackSpeed() * 0.6f;

It’s difficult to determine what 0.5f and 0.6f mean, why these numbers were added, and whether they are repeated throughout the code or not. For this reason, if we were to change these values we would have to look for each of its occurrences. In these cases, we choose to add a constant with a descriptive name. For this example, it could look something like this:

private const float InjuredSpeed = 0.5f;
private const float InjuredAttackSpeedReduction = 0.6f;

private void ApplyInjuredPenalties()

    _movementSpeed = InjuredSpeed;
    _attackSpeed = _weapon.GetAttackSpeed() * InjuredAttackSpeedReduction;

Code smells: How to recognize bad code?

In general, there are many indicators of a dirty code, such as:

  • Rigidity: Changing the code is difficult, as every time we want to make a change we have to make changes to other sections of the code, which are not completely related to the original changes in many cases.

  • Fragility: Sections of our code get broken every time we make a change, introducing new bugs and unexpected behaviors.

  • Unnecessary complexity: Many times, there are simpler solutions than those implemented. Occasionally, working with bad code forces us to look for solutions that are not the most appropriate, but avoid breaking other functionalities added in an unideal way.

  • Unnecessary repetition: As already mentioned, the same fragment of code is found in various parts of the project, making it impossible to locate all the repeated fragments in case we need to modify them.

  • Opacity: The code is difficult to understand. When working with a section, we must spend time trying to understand what it does, its dependencies, what we can and can’t modify, among other things.

Maintaining code

We must clarify that trying to write good code is not enough; we have to maintain its quality. This explains the importance of the Boy Scout Rule, which says we should always leave the code better than we found it. This rule motivates us to make small changes — like the ones we mentioned throughout this article — so that working on certain classes is constantly made simpler, not just for us, but for our coworkers who might need to make changes in the future.

Conclusion

As discussed throughout the article, there are many practices we can follow to create a code that’s easy to understand, extend, and maintain. By raising awareness of these practices we can create a team that aims to maintain codes clean, neat and constantly updated. In addition, following these practices ensures everyone feels comfortable and prevents conflicts. This way, we build stronger foundations on which we work daily, fostering teamwork, building features with less errors, and saving time trying to understand existing code we want to modify.

以上是关于清洁代码之道:一份实用关于如何编写和维护干净整洁的好代码的的方法 The Art Of Clean Code...的主要内容,如果未能解决你的问题,请参考以下文章

干净的代码——一种实用的方法

清洁代码,清洁架构和清洁项目布局/项目结构

架构整洁之道总结

15 个书写 JavaScript 代码的整洁之道(实用!)

《架构整洁之道》

读《代码整洁之道》有感