我敢打赌,你确定有过(或者在你的职业生涯中,某个时刻看到过)。这样的代码,一般存在于一些遗留的系统中,而且一般是很旧的。当你须要阅读这样的代码的时候,你可能会感受不太好。面试
这段代码的问题在于,它不只太冗长,而更重要的是,它隐藏了业务逻辑(这短代码还有其余问题,咱们将在后面讲到)。在企业应用程序中,咱们编写代码来解决实际的业务问题。所以。咱们不该该在修改代码的时候产生新的问题。请注意,当咱们编写"系统代码" 或者以高性能为目标的 Library 时,或者咱们解决的问题在技术上太过复杂时,能够适度的牺牲可读性。但即便如此,咱们也应该当心翼翼的避免编写隐藏逻辑的代码逻辑。算法
Robert C.Martin 在它的书《Clean Code : A Handbook of Agile Software Craftsmanship》 中提到过,"阅读(代码)和写做的时间比例,远远超过10 :1"。在一些遗留系统中,我发现本身花费大部分时间试图理解如何阅读代码,而不是能直接去阅读代码自己的逻辑。测试和调试这样的系统也是很是棘手的。在大多数状况下,有一些特殊的、不寻常的方式去处理逻辑,而这将彻底不一样于你以前所理解的一切。数据库
代码不是例外。代码不该该隐藏,用于解决问题的业务逻辑或算法。相反,它应该明确指出这些关键的业务逻辑或算法。代码中使用的方法的名称,方法的长度,甚至代码的格式应该看起来像问题已被处理的谨慎而专业。编程
那接下来看看,你对这短代码有什么感受?设计模式
这段代码,看着像是战后的战场,伤痕累累。我想每一个会阅读并修改这段代码的开发者,都讨厌这样的代码,并视图从这个地狱中逃脱出去,而这将使得状况变的更糟糕。不一样的编码风格和糟糕的命名方式,清晰地代表,这段代码不止让一个开发人员在这个地狱中轮回。这听起来像 破窗理论,不是吗?理解这段代码的功能并不容易(不只由于你看代码时会眼晕)。这段代码返回数组的总和减去元素的数量。让咱们以更方便的方式来作到这一点。
数组
如今,咱们可使用 Java 8 的流式编程方式,使咱们的代码变的更加简洁和可读。缓存
Clean Code 不是为了让咱们的代码看起来漂亮,而是为了让咱们的代码更易于维护。当代码模糊不清时候,咱们的大部分时间都将花在阅读上。安全
所以,开发者的生产力下降了。模糊的代码的后果是,维护过它的开发人员一般会让它变得更糟,就像咱们前面看到的那样。这样作的缘由并非由于他们没法清理并重构这段代码,而是因为时间的限制,一般是时间不够。数据结构
当咱们编写模糊的代码时,因为系统的体系结构/设计隐藏在代码中,因此很难估计修复 Bug 或实现新功能须要多长时间。所以,为了完成工做,咱们最终以打补丁的方式,修复了问题或增长了功能,而这将增长新的技术债务。多线程
另外一方面,简洁的代码显示了做者的想法,因此即便在代码中存在一个错误,也很容易找到并修复它。简洁的代码能够帮助咱们长远地加快编程速度。
对这些模糊的代码,若是想要解决它,可能须要花费几个月(或更多)的时间来重构并清理它。可是公司一般不会接受发展将被暂停的代价,来让开发者重构代码,这样的机会很是的渺茫,除非已经到了业务没法继续推进下去。
因此,咱们还能作些什么?
正如 Robert C.Martin 说说,童子军规则(The Boy Scout Rule)背后的思想至关的简单:让代码比你看到它的时候更干净! 每当你接触旧代码的时候,你应该正确的清理和适当的重构它。不该该以打补丁的方式只改动你必需要改动的地方,这将使代码更难理解。
这个规则更多的是在说开发者应该拥有的心态,经过使系统更易于维护,从而让他们的工做更加轻松容易。
我必须诚实的认可,在大多数状况下,处理遗留系统并不容易,特别是当没有测试资源或者自动化测试代码再也不维护的时候。可是在这篇文章中,我想关注一些我认为有用的通常性建议,来描述如何编写更多具备表达性的代码。
开发人员应该在编写代码以前清晰的认识到你正在作什么?咱们是使用代码在解决问题,代码只是媒介,而不是实际的解决方案。
所以,当咱们编写代码时,咱们必须格外当心,以即可以让咱们编写的代码,清晰的代表咱们须要解决问题的解决方案。代码应该解决问题,而不是带来新的问题。
你有没有被要求作 代码审查(Code Review),当咱们意识到代码中存在错误,惟一的解决办法是从头再次写一遍?我看到许多开发人员一旦获得开发任务,就开始在 IDE 中输入内容。他们认为,若是他们这样作,他们看起来就像在工做。大多数状况下,这被证实是错误的方法,由于没有通过思考,就开始编写代码可能会致使错误向错误的方向发展。固然,一些很是有经验的开发人员能够立刻开始编写代码并朝正确的方向发展,可是大多数开发人员在实际编码以前须要仔细想一想。
这个例子中的的代码,并无什么很差的。对吧?可是实际上,这里使用了 策略模式 ,代表这端代码须要有必定的灵活性。而在这里例子中,咱们只实现了一个策略,没有更多的实现,而这里使用策略模式,可能会误导读者。一个策略模式是须要编写更多的代码的,因此读者天然可能会想到,让前一个开发者使用策略模式的缘由是什么呢?YAGNI 原则表示,"你不会须要它",可是这里却作了更多没必要要的事情。预测将来咱们将须要什么,是很难预测的。在预测将来的需求上,有时候经验是会有帮助的,可是大多数状况下,保持简单是比较安全的。
设计模式帮助咱们以一种通用的优雅方式来解决特定的问题。可是若是这样的问题并不存在(例如前面的例子中,并不须要策略模式),以后代码的阅读者将会被误导,并认为这样作是有必要的,是为了解决实际问题。须要特别说明一下,我并无反对任何设计模式,我也很是喜欢用它们,问题是有时候人们会去套用设计模式来解决问题,只是由于他们知道这个设计模式。
咱们的工具集中有不少工具,咱们应该有能力分清楚,什么时候是使用它们最恰当的时候。仅仅是由于框架或者库被大多数开发者使用,是没有意义的。咱们必须知道它们能解决什么问题,会存在什么问题,并以一种不隐藏业务逻辑的方式来使用它们。
现在,许多编程语言都是支持流的。例如 Java、Kotlin、JavaScript 等来帮助咱们编写表达式代码。流已经用 "if" 语句取代了大段的循环。数据流帮助咱们以一种声明式的方式,更具备说明性的操做来进行数据转换。若是你想找到一个集合中全部小于某个值的元素,循环迭代集合将没有意义,只须要经过过滤器操做数据流就能够了。
Map、Filter 和 Reduce ,几乎每个支持流的语言都支持它。因此,每一个人均可以理解你写的东西,就像每一个人都能理解一个循环或者一个 if 语句同样。
有这样的清晰处理逻辑的方式是强大的。首先,你没必要测试这个功能,由于它们必定是稳定的。而你有没有注意到第一个例子中的问题?当使用函数式编程的方式,它将变的更加简单。函数式编程在这篇文章中,有不少好处,可是我重点介绍它来如何帮助代码提升可读性。
第一个例子中,基于流的解决方案以下:
简单而干净。很容易就理解它在作什么。如今,再来看看下面的例子:
你是否指望当你调用这个方法额时候,方法的第二个参数将会被改变?这个方法是否按照所想的去作?方法名称是否合适?你真的获得预期的结果了吗?
那么,如今呢?
在这个例子中,返回值是一个新的列表,没有参数会受到影响。咱们只是读取参数并产生一个新的结果。理解这个方法如今作什么以及如何使用它将变的更容易。这种方法能够很容易的与其它方法组合。
通常而言,组合是流和函数式编程的最重要的好处之一。组合能使咱们可以在更高级别上进行数据的转换、过滤等操做,并编写更具说明性和表达性的代码,而不是旧的命令式风格。咱们写的代码表达了咱们想要作的而不是如何完成!这是代码可读性的重大改进。
把一个大问题分解成多个子问题,解决每个子问题,而后组合这些解决方案,这将为解决初始问题提供了解决方案。
请注意,Java 8 中的 toList()
会返回一个可变的列表,而在函数式编程中,咱们一般使用不可变数据结构。不过,咱们生成一个新的数据集合和将参数是为制度的,会有利于咱们改进代码的可读性。
编写表达式代码不是一件容易的事情。有句 Albert Einstein 说过的名言,"若是你不能简单的解释它,你并非真正的理解它"。因此,当我看到抽象层混合的逻辑代码时,例如与 DAO 交互的 UI 类,能直接与数据库交互,又或者低层次的细节在不该该被暴露的地方暴露了。咱们都知道单一职责原则的 SOLID 原则,可是关于这个问题一直受人诟病,由于有些时候很难作职责的划分,这部分是它有争议的关键。在代码中使用注释来解释代码,并非一个解决方案,咱们将在后面的文章中看到。我相信有人写的越简单、越具表达性的代码,他或者她对这个问题的理解就越清晰。
当对象的状态发生变化,而咱们没有注意到它的时候,这真的会让人困惑。使用返回值能够构造一半的对象也是很危险的,特别是当咱们处理具备多个线程的程序时。共享这些对象真的很难作到正确。另外一方面,不可变对象是线程安全的,也是缓存的最佳选择,由于它们的状态不会改变。
可是为何人们选择可变对象呢?我相信最有可能的缘由是他们认为他们会得到更好的结果,由于所使用的内存会更少,由于这些更改已经完成了。并且,让一个对象的状态在其整个生命周期中发生变化是很天然的。这是咱们在 OOP 中学到的。这些年来,咱们一直在写程序,其中大部分的对象都是可变的。
现在,一个系统的内存数量比几十年前大了几个数量级。咱们面临的真正问题是可扩展性。处理器的速度再也不像过去几年那样告诉的提升了,可是如今咱们有了几十个内核的盒子。因此,对于咱们的规模来讲,咱们须要利用如今的状况。因为咱们的程序须要可以在多个内核上运行,因此咱们须要以一种安全的方式编写它们。经过使用可变对象,咱们必须处理锁定以确保其状态的一致性。并发并非一个小问题要解决。另外一方面,因为它们的性质,不可变对象在多线程和处理器之间共享是固有安全的。并且,不须要同步的事实为建立具备低延迟和高吞吐量的系统提供了机会。所以,不变性是实现可扩展性的更安全的选择。
除了可扩展性的好处,不变性使咱们的代码更清洁。在上一节的第一个示例中,做为参数传递的集合在方法调用以后发生了更改。若是收藏是不可改变的,那么这是被禁止的。所以,不变性会促使咱们走向更好的解决方案。另外,因为状态不可变,阅读者没必要记住他心中的状态变化。阅读者只须要将一个名称与一个值关联起来,而不记得变量的最新值。
程序必须是为人们阅读而写的,它只是恰巧让机器执行了。
— Harold Abelson
这篇文章更多的是关于编写更具可读性和表达性的代码的通常建议。在未来的文章中,咱们将讨论生产代码和测试代码中的气味。咱们也将看到咱们如何才能经过查看咱们的测试来在咱们的生产代码中找到可能的设计问题。
敬请关注!
原文地址:
以为文章不错的喜欢的小伙伴能够关注加转发,欢迎你们前来探讨交流。