你见过类似这样的代码吗?
我敢打赌,你肯定有过(或者在你的职业生涯中,某个时刻看到过)。这样的代码,通常存在于一些遗留的系统中,并且通常是很旧的。当你需要阅读这样的代码的时候,你可能会感觉不太好。
这段代码的问题在于,它不仅太冗长,而更重要的是,它隐藏了业务逻辑(这短代码还有其他问题,我们将在后面讲到)。在企业应用程序中,我们编写代码来解决实际的业务问题。因此。我们不应该在修改代码的时候产生新的问题。请注意,当我们编写"系统代码" 或者以高性能为目标的 Library 时,或者我们解决的问题在技术上太过复杂时,可以适度的牺牲可读性。但即使如此,我们也应该小心翼翼的避免编写隐藏逻辑的代码逻辑。
Robert C.Martin 在它的书《Clean Code : A Handbook of Agile Software Craftsmanship》 中提到过,"阅读(代码)和写作的时间比例,远远超过10 :1"。在一些遗留系统中,我发现自己花费大部分时间试图理解如何阅读代码,而不是能直接去阅读代码本身的逻辑。测试和调试这样的系统也是非常棘手的。在大多数情况下,有一些特殊的、不寻常的方式去处理逻辑,而这将完全不同于你之前所理解的一切。
我们写的内容应该像在诉说一个故事
代码不是例外。代码不应该隐藏,用于解决问题的业务逻辑或算法。相反,它应该明确指出这些关键的业务逻辑或算法。代码中使用的方法的名称,方法的长度,甚至代码的格式应该看起来像问题已被处理的谨慎而专业。
那接下来看看,你对这短代码有什么感觉?
这段代码,看着像是战后的战场,伤痕累累。我想每个会阅读并修改这段代码的开发者,都讨厌这样的代码,并视图从这个地狱中逃脱出去,而这将使得情况变的更糟糕。不同的编码风格和糟糕的命名方式,清晰地表明,这段代码不止让一个开发人员在这个地狱中轮回。这听起来像 破窗理论,不是吗?理解这段代码的功能并不容易(不仅因为你看代码时会眼晕)。这段代码返回数组的总和减去元素的数量。让我们以更方便的方式来做到这一点。
现在,我们可以使用 Java 8 的流式编程方式,使我们的代码变的更加简洁和可读。
Clean Code(简洁的代码)
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
这篇文章更多的是关于编写更具可读性和表达性的代码的一般建议。在将来的文章中,我们将讨论生产代码和测试代码中的气味。我们也将看到我们如何才能通过查看我们的测试来在我们的生产代码中找到可能的设计问题。
敬请关注!
原文地址:
今天在承香墨影公众号的后台,回复『成长』。我会送你一些我整理的学习资料,包含:Android反编译、算法、设计模式、虚拟机、Kotlin、Linux、Web项目源码。
推荐阅读:
- 手写你的第一个 Dalvik 版的 HelloWorld !
- 学编程,先学如何像程序员一样思考!!!
- 看完九篇字体系列的文章,你还觉得我是在说字体?
- 通过 PackageManager 获得你想要的 App 信息!
- 阿里全球首发的规约插件,所有细节都在这里!!!