您见过这样的代码吗?
我敢打赌你有(或者你会在职业生涯的某个时候看到)。 这样的代码存在于某些旧系统中,并且通常已经很旧了。 最有可能的是,当您看到这样的代码时,会感觉不太舒服。
这段代码的问题在于,它不仅太冗长,而且更重要的是, 它隐藏了业务逻辑 (此代码还有其他一些问题,我们将在本文后面看到)。 在企业应用程序中, 我们编写代码来解决问题 。 因此,我们不应该对代码造成新的问题。 请注意,当我们编写旨在提高性能的“系统代码”或库或我们解决的问题在技术上过于复杂时,可以牺牲可读性,但是即使如此,我们也应谨慎行事,以免编写晦涩难懂的代码来隐藏逻辑。
罗伯特·C·马丁(Robert C. Martin)(鲍勃叔叔)在他的《 干净的代码:敏捷软件 技巧 手册 》中说: “读(写代码)与写代码所花费的时间之比超过10:1” 。 在某些遗留系统中,我发现自己大部分时间都在花时间来尝试理解如何阅读代码,而不是实际阅读代码。 测试和调试此类系统也可能非常棘手。 在大多数情况下,有一种特殊的,不常见的方式与您到目前为止所完成的一切完全不同。
我们写的一切都讲述一个故事
该代码也不例外。 该代码不应隐藏业务逻辑或用于解决问题的算法。 相反,它应该以清晰的方式指出。 所使用的名称,方法的长度,甚至代码的格式都应该看起来像问题已经通过谨慎和专业的方式解决了。
您对这段代码有什么看法?
这段代码看起来像是战后的战场。 似乎每个使用此代码的开发人员都讨厌这样做,并试图摆脱这种困境,使它处于更加糟糕的状态。 不同的格式和不良的命名清楚地表明,有一个以上的开发人员生活在这个地狱中。 听起来像是破窗理论 ,不是吗? 很难说出代码的作用(不仅因为查看代码时眼睛受伤)。 此代码段返回数组的总和减去元素数。 让我们尝试以更方便的方式做到这一点:
现在,我们正在使用Java 8流,这些流使我们的代码更加简洁和易读。
干净的代码!
干净的代码并不是要使我们的代码看起来漂亮 。 干净的代码是关于使我们的代码更具可维护性 。 如果代码晦涩难懂,则大部分时间都花在阅读上。 因此,降低了开发商的生产率。 代码晦涩难懂的结果是,使用它的开发人员通常会使它变得更糟,如我们先前所见。 这样做的原因不是由于它们无法清除代码,而是通常是由于截止日期的压力而导致时间不足。 当我们使用晦涩的代码时,由于系统的体系结构/设计被隐藏在代码中,因此很难估计修复错误或实现新功能所花费的时间。 因此,我们最终只是为了完成工作而进行了丑陋的修改,从而增加了技术负担。 另一方面,纯净的代码表明了作者的意图,因此即使代码中存在错误,也更容易找到并修复它。 从长远来看,干净的代码可以帮助我们更快地发展。 我绝对推荐的两本好书是: Robert C. Martin的 “ Clean Code:敏捷软件技巧手册 ”和Martin Fowler和Kent Beck的 “ 重构:改进现有代码的设计 ”。
解决晦涩的代码的可维护性问题的一种方法是花费几个月(或更长时间)来重构代码并清理代码,但是对于企业来说,接受开发将暂停的机会确实很小。开发人员正在重构代码。 所以,我们能做些什么?
童子军规则
正如鲍伯叔叔所说的,童子军规则背后的想法很简单: 让代码比您发现的干净! 每当您触摸旧代码时,都应正确清理它。 不要仅仅应用会使代码更难以理解的快捷解决方案,而要当心处理。 该规则更侧重于开发人员应该具有的心态,以便他们可以通过使系统更易于维护来从长远来看使他们的生活更轻松。
我会坦白地说,在大多数情况下,处理遗留系统绝非易事,尤其是在没有测试或不再维护测试套件的情况下,但我们仍应寻找机会使代码更整洁。 在使用遗留系统时,人们可以采用许多技术(一本很棒的书是: Michael Feathers的 “ 使用遗留代码有效地工作 ”),但是在这篇文章中,我想着重介绍一些我认为有用的一般建议。编写更具表现力的代码。
写之前先想一想
关于软件开发存在一个误解,即开发人员(仅)编写代码。 我们没有。 相反,我们使用code解决问题 。 代码是媒介,而不是实际的解决方案。 按下随机键是否被视为编写代码? 当然不是,因为几乎不可能用计算机来解释这种胡言乱语。 这同样适用于编写的代码,而无需先考虑我们要解决的问题。 因此,在编写代码时,我们必须格外注意,以使我们通过此代码提供的解决方案清晰明确。 我们不应该仅仅为了编写代码而编写代码。 该代码应解决问题,而不是创建新问题。
您是否曾经被要求进行代码审查,只是意识到代码完全错误,唯一的解决方案是从头开始编写代码? 我已经看到许多开发人员,他们一旦获得任务,便开始在IDE中输入内容。 他们认为,如果这样做,他们看起来就像在工作。 在大多数情况下,这被证明是错误的方法,因为编写代码时如果不加思考,就会使他们朝错误的方向发展。 当然,一些非常有经验的开发人员可以立即开始编写代码,并且仍然朝着正确的方向发展,但是大多数开发人员需要在进行实际键入之前进行周密的计划。
考虑以下示例:
这个示例中的代码没有什么不好的,对吧? 好吧,实际上有! 这里使用策略模式的事实表明,这段代码需要具有一定的灵活性。 在此示例中,与Wikipedia的原始示例不同,我们只有该策略的一种实施方式,而没有针对更多实施方式的短期计划。 这里的策略模式的意图可能会误导读者。 模式的实现需要一些努力,因此读者自然会想知道做出该决定的原因是什么。 YAGNI原则代表“您不需要它”,并且是指不要做不必要的事情。 很难预测我们将来需要什么。 有时经验会有所帮助,但在大多数情况下,使事情简单一点是更安全的。
设计模式可以帮助我们以易于交流的优雅方式解决特定问题。 如果不存在这样的问题(在前面的示例中,则不需要扩展),那么代码的读者将被误导并认为该问题确实存在。 请注意,我对模式没有任何要求。 我爱他们! 问题是,当人们试图发明模式可以解决的问题时,仅仅是因为他们知道模式。
当我们尝试同时将具有业务需求的解决方案与模式混合在一起时,也会出现相同的问题。 我发现首先很容易看到应该如何以“肮脏”的方式解决问题。 只有到那时,我才研究哪些模式和抽象可以帮助代码变得更加灵活和易读。 无论我是否实践TDD,我都会遵循的规则是首先使其工作,然后使其变得干净(在TDD中,当然,这是由TDD的3条定律驱动的)。
记得! 仅仅因为代码有效,并不意味着我们已经完成工作! 实际上,当代码运行时,我们只完成了一半。 我们必须研究代码如何将我们的意图传达给读者。
我们的工具集中有很多工具,只有在适当的时候才使用它们是我们的责任。 仅仅因为每个人都使用框架和库是没有意义的。 我们必须学习解决问题的方式,并以不隐藏业务逻辑的方式来使用它们。 关于如何处理框架和库的一篇很棒的文章是: Bob叔叔的 “ 让魔术消失了 ”。
力求表现力!
如今,许多编程语言都附带了流支持,例如Java,Kotlin,JavaScript等,以帮助我们编写表达性代码。 流已经用“ if”语句代替了冗长的循环。 流帮助我们以比声明式方式更具声明性的方式思考数据转换。 遍历集合以查找所有小于值的元素是没有意义的。 只需将过滤器应用于流。
映射,过滤和归约几乎支持流的所有语言。 因此,每个人都可以理解您编写的内容,就像每个人看到for循环或if语句一样。 关于该主题的一篇精彩文章是: Martin Fowler的 “ Collection Pipeline ”。
采用这种表达方式来处理数据非常强大。 首先,您不必测试此功能。 您是否注意到第一个示例the中的错位错误? 这也使我们朝着程序的功能编程方法前进。 函数式编程具有太多好处,无法满足本博客文章的要求(如果您有兴趣学习更多有关函数式编程的知识,我建议您发布“ 实用函数式编程 ”一书,当然,也推荐一本有关函数式编程的好书:“ 结构化和解释性“计算机程序 ”(作者Harold Abelson , Gerald Jay Sussman和Julie Sussman ),但我将重点介绍它如何帮助提高代码的可读性。
以下是第一个示例的基于流的解决方案:
简单干净。 很容易理解它的作用。 现在,考虑以下示例:
您是否希望在调用该方法时更改第二个参数? 这种方法能做到吗? 方法名称是否合适? 您实际上“得到”了什么吗?
现在呢?
在此示例中,返回值是一个新列表。 不影响任何参数。 我们只是读取参数并产生新的结果。 了解这种方法现在做什么以及如何使用它要容易得多。 该方法可以容易地与其他方法组合。 一般而言,组合是流和功能编程的最重要优点之一。 组合使我们可以在更高级别上进行数据转换,过滤等方面的思考,并编写比命令式方法更具声明性和表达性的代码。 我们编写的代码表达了我们想要做的事情,而不是如何完成! 这是代码可读性的重大改进。
将问题分解为子问题,解决每个子问题,然后组合这些解决方案以创建初始问题的解决方案要容易得多。 另一方面,当主要目标是表现时,命令式风格可能至关重要。 关于这个问题的一个有趣的故事是著名的McIlroy vs Knuth的故事 。
请注意,Java 8中的toList()
收集器返回一个可变列表,而在函数式编程中,我们通常使用不可变的数据结构。 尽管如此,我们产生新数据并将参数视为只读的事实改善了方法的可读性和行为。 尽管某些方法可能会产生副作用,但对于一种方法来说,要么具有副作用(作为命令 ),要么具有返回值(作为查询 ),但在可能的情况下不同时具有这两者是很重要的。 有关此主题的更多信息,请参见本文 。
编写表达性代码并非易事。 阿尔伯特·爱因斯坦(Albert Einstein)的一句名言说: “如果您不能简单地解释它,那么您就不会足够理解它。” 。 因此,当我看到混合了抽象级别的代码,例如与DAO交互或直接与数据库交谈的UI类,或者在不需要的时候暴露了低级别的细节时,我可以知道不仅存在违反SOLID的单一责任原则 原则,但在问题上也有些困惑。 在代码中使用注释来解决此问题并不是解决方案,正如我们将在以后的文章中看到的那样。 我相信,某人编写的代码越简单,表达能力越强,他或她就越能理解问题。
拥抱不变
当对象的状态发生变化而我们没有注意到时,这确实令人困惑。 使用可以在返回时一半构造的对象也是很危险的,尤其是当我们处理具有多个线程的程序时。 共享此类对象确实很难正确完成。 另一方面,不可变对象是线程安全的,并且由于其状态不变,因此也是缓存对象的理想选择。
但是为什么人们选择易变的物体? 我认为,最有可能的原因是,他们认为它们会获得更好的性能,因为使用的内存会更少,因为修改是在原地执行的。 而且,在整个生命周期中更改对象的状态是很自然的。 这是我们在OOP中学到的。 这些年来,我们一直在编写程序,使我们使用的大多数对象都是可变的。
如今,系统拥有的内存量比几十年前大了几个数量级。 我们面临的真正问题是可伸缩性。 处理器的速度不再像以前几年那样得到改善,但是现在我们有了带有数十个内核的设备。 因此,对于要扩展的程序,我们需要利用当前的情况。 由于我们的程序需要能够在多个内核上运行,因此我们需要以一种安全的方式编写它们。 通过使用可变对象,我们必须处理锁定以确保其状态的一致性。 并发并不是要解决的琐碎问题。 如果您对并发感兴趣,那么您绝对应该阅读Brian Goetz的 “ Java Concurrency in Practice ”。 另一方面,由于不变对象的本质,它们固有地可以在多个线程和处理器之间共享。 同样,不需要同步这一事实为创建具有低延迟和高吞吐量的系统提供了机会。 因此,不变性是实现可伸缩性的更安全选择。
除了可扩展性优点之外,不变性还使我们的代码更加整洁。 在上一节的第一个示例中,作为参数传递的集合在方法调用之后发生了变化。 如果集合是不可变的,则将被禁止。 因此,不变性将驱使我们寻求更好的解决方案。 另外,由于状态不变,因此读者不必在心中跟踪状态变化。 读者只需要将名称与值相关联,而不必记住变量的最新值。
通常,可以在Joshua Bloch撰写的“ Effective Java(第二版) ”一书中找到有关不变性和编程建议的更多信息。 另外,您绝对应该注意的一个很棒的话题是“ Rich Hickey的价值观的价值 ”。
必须编写程序供人们阅读,并且只能偶然地使机器执行。
― Harold Abelson,计算机程序的结构和解释
这篇文章更多地是关于编写更具可读性和表达力的代码的一般建议。 在以后的文章中,我们将在生产代码和测试代码中讨论气味。 我们还将看到仅通过查看测试,如何在生产代码中发现可能的设计问题。 敬请关注!
进一步阅读
- 干净代码: Robert C. Martin 撰写的敏捷软件工艺手册
- 重构:改进 Martin Fowler和Kent Beck 的现有代码设计
- 通过迈克尔·费瑟斯 有效地使用遗留代码
- Harold Abelson , Gerald Jay Sussman和Julie Sussman 编写 的计算机程序的结构和解释
- Brian Goetz的 Java并发实践
- Joshua Bloch撰写的 Effective Java(第二版)
- 使魔术消失
- 收集管道
- 实用函数式编程
- 多壳,少鸡蛋 (麦克罗伊与克努斯的故事)
- CommandQuerySeparation
- Rich Hickey的价值观
这篇文章的原始图片来源是 twemoji
From: https://hackernoon.com/let-the-code-speak-52d1cebf0394