Some simple tips and techniques to help you write cleaner code today.
If you are reading this article, the chances are that you write code. Maybe even quite a lot of it... Hopefully writing that code is something that you enjoy and care about. As much as you may (or may not) love coding, as Developers we actually spend about 10x as much of our time reading code compared to writing it.
Reading other people’s code, and unfortunately even our own code, can sometimes be a miserable experience. Incomprehensible, messy, smelly code. We have all seen it. We have all written it…
Without a bit of care, our codebases can quickly become a mess. The cost of owning a mess is high! High cost to our companies. High cost to our mental wellbeing. Rotting code turns our projects into Haunted Graveyards, where even our most experienced Engineers dare not venture. Luckily we can quickly improve our code with a few simple techniques:
Abstract Conditional (if) statements into descriptive methods Replace Comments with declarative code Write cleaner functions Eliminate switch statements using Polymorphism Start at the end — Think about your destination code and then work out how to get there I will explain these 5 tips with real code examples. The examples use JavaScript since most people are familiar with it, however they could be applied to any language just as easily. The code for all examples can be found on my GitHub here.
Before we get going, a few words on Clean Code and techniques to help get there.
一些简单的提示和技巧,帮助您编写更简洁的代码。
如果您正在阅读这篇文章,那么您很可能正在编写代码。甚至可能写了很多... 希望编写代码是你喜欢和关心的事情。尽管您可能(或可能不)热爱代码编写,但作为开发人员,我们阅读代码的时间实际上是编写代码时间的 10 倍。
阅读别人的代码,甚至是我们自己的代码,有时是一种痛苦的经历。难以理解、杂乱无章、臭气熏天的代码。我们都见过。我们都写过...
如果稍不注意,我们的代码库很快就会变得一团糟。拥有一团糟的代价是高昂的!我们的公司要付出高昂的代价。我们的精神健康也要付出高昂的代价。腐烂的代码会让我们的项目变成闹鬼的墓地,即使是最有经验的工程师也不敢轻易涉足。幸运的是,我们可以通过一些简单的技术快速改进我们的代码:
- 将条件(if)语句抽象为描述性方法
- 用声明性代码代替注释
- 编写更简洁的函数
- 使用多态性(Polymorphism)消除开关语句
- 从头开始--想想你的目标代码,然后想办法到达那里
我将用实际代码示例来解释这 5 个技巧。这些示例使用的是 JavaScript,因为大多数人对它都很熟悉,但它们也可以很容易地应用于任何语言。所有示例的代码都可以在我的 GitHub 上找到。
在我们开始之前,请先了解一下清洁代码(Clean Code)以及帮助您优化代码的技巧。
Clean Code The ultimate goal here is Clean Code. I see lots of Junior Developers who think that writing Clean Code is a skill that we either just have, or develop over time. In reality, even the most experienced Developers don’t simply churn out clean elegant code all day at first attempt.
Writing good quality code is a process, which generally involves a lot of refactoring. In fact most of the techniques we will go through are refactoring techniques.
Red, Green, Refactor The key to writing better code is to have some reliable tests, so you can refactor whilst knowing your code still works. At first we will often write messy procedural code and some tests to make sure it does what we want. The most important step comes next – that is where we refactor and clean our code so that it is easier to understand and maintain.
简洁代码
这里的终极目标是代码整洁。我看到很多初级开发人员都认为,编写简洁代码是我们必须掌握的技能,或者是随着时间的推移而逐渐掌握的技能。实际上,即使是最有经验的开发人员,也不可能一开始就写出整洁优雅的代码。
编写高质量的代码是一个过程,通常涉及大量的重构工作。事实上,我们将要介绍的大部分技术都是重构技术。
红色、绿色、重构
编写更好的代码的关键是要有一些可靠的测试,这样你就可以在知道你的代码仍然有效的情况下进行重构。起初,我们通常会编写杂乱无章的程序代码和一些测试,以确保它能实现我们想要的功能。接下来才是最重要的一步--重构和清理代码,使其更易于理解和维护。
Too many Developers skip the last step where we clean the code. Without any tests we are unlikely to feel empowered to clean our code at all. Google have a brilliant philosophy on this:
The Beyoncé Rule – “If you liked it, then you shoulda put a test on it.”
太多开发人员跳过了最后一步,即清理代码。如果不进行任何测试,我们根本不可能有能力清理代码。在这方面,谷歌有一个杰出的理念:
碧昂丝法则--"如果你喜欢它,就应该对它进行测试"。
1. Abstract Conditional (if) statements into descriptive methods(将条件(if)语句抽象为描述性方法)
The first refactoring technique is probably the easiest to implement. The code below checks to see if a person can be allowed into a Nightclub. They will either be allowed entry, sent to the queue or rejected.
第一种重构技术可能是最容易实现的。下面的代码会检查一个人是否可以进入夜总会。他们要么被允许进入,要么被送入队列,要么被拒绝。
checkNightclubEntry(person) {
if (person.hasFakeId
|| person.age < 18
|| this.barredList.includes(person.identificationNumber)) {
this.reject();
}
else {
if (this.entrants.length < 500) {
this.allowEntry(person);
}
else {
this.sendToQueue(person);
}
}
}
The first Conditional (if) statement is fairly complex, its pretty hard to work out what’s going on. We can abstract this into a clearly named function, which our readers will be able understand. We also have a few ‘magic’ numbers floating around (18 and 500), it isn’t clean what these are for either.
第一个条件(if)语句相当复杂,很难弄清是怎么回事。我们可以将其抽象为一个明确命名的函数,这样读者就能理解了。此外,我们还有一些 "神奇 "的数字(18 和 500),这些数字的用途也不清楚。
checkNightclubEntry(person) {
if (!this.isAllowedEntry(person)) {
this.reject();
return;
}
if (this.hasCapacity()) {
this.allowEntry(person);
}
else {
this.sendToQueue(person);
}
}
hasCapacity() {
const maxCapacity = 500;
return this.entrants.length < maxCapacity;
}
isAllowedEntry(person) {
const minimumAge = 18;
return !person.hasFakeId
&& person.age >= minimumAge
&& !this.barredList.includes(person.identificationNumber)
}
Abstracting our conditional statements into well named functions makes it much easier to read. The ‘magic’ numbers have also been given useful names which help the reader understand what they are used for.
将条件语句抽象为名称明确的函数,阅读起来就容易多了。此外,"神奇 "的数字也被赋予了有用的名称,有助于读者理解它们的用途。
2. Replace Comments with Declarative code(用声明式代码替换注释)
Comments are one of the most abused language features in programming. Well written rich declarative code should speak for itself and not require any comments at all.
We should only be writing comments in rare scenarios where there are edge cases that can’t be understood from our code.
The following code retrieves the top 10 rated answers relating to a question on a Forum.
注释是编程中最常被滥用的语言功能之一。写得好的丰富声明性代码应该自己会说话,根本不需要任何注释。
我们只应在极少数情况下编写注释,因为在这些情况下存在无法从代码中理解的边缘情况。
下面的代码将检索论坛上与某个问题相关的排名前 10 位的答案。
// get top answers for question
getTopAnswers(question) {
// get answers for question
let questionAnswers = [];
this.answers.forEach(answer => {
// check answer matches question
if (answer.questionId === question.id) {
questionAnswers.push(answer);
}
});
// sort answers by rating
questionAnswers = questionAnswers.sort((a, b) => {
if (a.rating < b.rating) {
return 1;
}
if (a.rating > b.rating) {
return -1;
}
return 0;
});
// if more than 10 answers
// then return top 10
if (questionAnswers.length >= 10) {
return questionAnswers.slice(0, 10);
}
else {
return questionAnswers;
}
}
The previous code is littered with pointless comments. Almost all comments can be eliminated by naming functions and variables better or by extracting the code it is describing into well named functions.
前面的代码充满了毫无意义的注释。几乎所有的注释都可以通过更好地命名函数和变量或将其描述的代码提取到命名良好的函数中来消除。
getTopAnswers(question) {
let questionAnswers = this.getQuestionAnswers(question.id);
questionAnswers = this.sortByBestRatings(questionAnswers);
const top = 10;
return this.takeItems(questionAnswers, top);
}
getQuestionAnswers(questionId) {
const questionAnswers = [];
this.answers.forEach(answer => {
if (answer.questionId === questionId) {
questionAnswers.push(answer);
}
});
return questionAnswers;
}
sortByBestRatings(questionAnswers) {
return questionAnswers.sort((a, b) => {
if (a.rating < b.rating) {
return 1;
}
if (a.rating > b.rating) {
return -1;
}
return 0;
});
}
takeItems(items, count) {
if (items.length >= count) {
return items.slice(0, count);
}
else {
return items;
}
}
The cleaned version has been broken down into smaller functions, with names that clearly describe what they are doing. A top variable has been created to clearly show that only 10 items will be taken.
清理后的版本被分解成更小的函数,其名称清楚地描述了这些函数的作用。创建了一个顶部变量,以明确显示只提取 10 个项目。
3. Write cleaner functions(编写更简洁的函数)
When it comes to functions, generally the smaller they are, the better. Smaller functions are easier for us to understand, easier for us to re-use and easier for computers to optimise.
A function should do one thing and one thing only. If you find yourself needing to use names like getDataAndThenMapAndSendToCustomer then you probably need to consider breaking the function down.
There are a few rules which can be followed to help you design cleaner functions:
Functions should be small (ideally less than 15 lines) Functions should have descriptive meaningful names Functions should either retrieve data or mutate data. Not both. Functions should work at a single level of abstraction Reframe from using else conditions where possible The following code counts the number of words in lines of text. A set of ignoreWords can be passed in to exclude from the count.
一般来说,函数越小越好。函数越小越容易理解,越容易重复使用,也越容易被计算机优化。
函数应该只做一件事。如果您发现自己需要使用 getDataAndThenMapAndSendToCustomer 这样的名称,那么您可能需要考虑分解函数。
有几条规则可以帮助你设计出更简洁的函数:
-
函数应该很小(最好少于 15 行)
-
函数名称应具有描述性意义
-
函数应检索数据或更改数据。不能两者兼而有之。
-
函数应在单一抽象层次上运行
-
尽可能不使用 else 条件重构
下面的代码计算文本行的字数。可以通过一组 ignoreWords 来排除计数。
function countWords(lines, ignoreWords) {
if (lines === null) {
return 0;
}
else {
let wordCount = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i] !== null) {
const words = lines[i].split(" ");
for (let j = 0; j < words.length; j++) {
if (ignoreWords === null || !ignoreWords.includes(words[j])) {
wordCount++;
}
}
}
}
return wordCount;
}
}
The previous example breaks most of our rules and as a result it is really difficult to understand. We have deeply nested loops and if/else statements; this makes the cognitive complexity high. We are also working at many different layers of abstraction.
前面的示例打破了我们的大部分规则,因此非常难以理解。我们有深度嵌套的循环和 if/else 语句;这使得认知复杂度很高。我们还在许多不同的抽象层上工作。
function countWords(lines, ignoreWords) {
if (lines === null) {
return 0;
}
let wordCount = 0;
lines.forEach(line => {
const lineWordCount = getLineWordCount(line, ignoreWords);
wordCount += lineWordCount;
});
return wordCount;
}
function getLineWordCount(line, ignoreWords) {
if (line === null) {
return 0;
}
let wordCount = 0;
const words = line.split(" ");
words.forEach(word => {
if (includeWord(word, ignoreWords)) {
wordCount++;
}
});
return wordCount;
}
function includeWord(word, ignoreWords) {
return ignoreWords === null
|| ignoreWords.length === 0
|| !ignoreWords.includes(word);
}
The 2nd example breaks down the complex method into 3 clear stages: Working out if a word should be included, counting the words on a single line and aggregating the counts from each line.
第 2 个示例将复杂的方法分解为 3 个清晰的阶段: 确定是否应包含某个单词、计算单行的单词数以及汇总每行的计数。
4. Eliminate switch statements using Polymorphism(使用多态性消除 switch 语句)
Switch statements are often seen as a code smell. If we do need to use a switch statement for something, then it should only appear once in our code.
If similar switch statements are littered throughout our code, then it is usually a good sign that we can abstract the code in a better way. Often the best way to do that is using Polymorphism.
Take the following Notifier class, which allows notifications to be published using different channels. The ‘Email’ channel allows notifications to be sent at any time, whereas the ‘SMS’ channel forces notifications to be ‘scheduled’ when outside of business hours.
开关语句通常被视为一种代码气味。如果我们确实需要使用 switch 语句来处理某些事情,那么它应该只在代码中出现一次。
如果我们的代码中到处都是类似的开关语句,那么这通常是一个很好的信号,说明我们可以用更好的方式来抽象代码。通常,最好的方法就是使用多态性。
以下面的 Notifier 类为例,它允许使用不同的通道发布通知。电子邮件 "通道允许在任何时间发送通知,而 "短信 "通道则强制要求在非工作时间 "安排 "通知。
class Notifier{
constructor(scheduler){
this.scheduler = scheduler;
this.sms = new Sms();
this.smtp = new Smtp();
}
publishNotification(message, channelType) {
if (this.notificationsActive(channelType)) {
this.sendNotification(message, channelType);
}
else {
this.scheduleNotification(message, channelType);
}
}
notificationsActive(channelType) {
switch (channelType) {
case "Email":
return true;
case "SMS":
return this.scheduler.isBusinessHours();
default:
throw new Error(`No Notification Type found: ${channelType}`);
}
}
sendNotification(message, channelType) {
switch (channelType) {
case "Email":
this.smtp.send(message);
break;
case "SMS":
this.sms.send(message);
break;
default:
throw new Error(`No Notification Type found: ${channelType}`);
}
}
scheduleNotification(message, channelType) {
switch (channelType) {
case "Email":
throw new Error("Scheduling is not supported for this Notification type");
case "SMS":
this.sms.schedule(message);
break;
default:
throw new Error(`No Notification Type found: ${channelType}`);
}
}
}
In the previous code a similar switch statement is being used in lots of different places. As well as being inefficient, it is also hard to maintain. If a new channel is needed then we will need to remember to update lots of different switch statements.
在前面的代码中,许多不同的地方都使用了类似的 switch 语句。这不仅效率低下,而且难以维护。如果需要一个新的通道,我们就需要记住更新许多不同的 switch 语句。
To clean our code we can break down the channels into different NotificationService implementations: EmailService, SmsService
为了简化代码,我们可以将通道分解成不同的 NotificationService 实现: EmailService, SmsService(电子邮件服务、短信服务)
class NotificationService {
send(message) {
throw new Error("Method 'send()' must be implemented.");
}
isActive() {
return true;
}
schedule(message) {
throw new Error("Scheduling is not supported for this Notification type");
}
}
class EmailService extends NotificationService {
constructor(smtp){
super();
this.smtp = smtp;
}
send(message) {
this.smtp.send(message);
}
}
class SmsService extends NotificationService {
constructor(sms, scheduler){
super();
this.sms = sms;
this.scheduler = scheduler;
}
send(message) {
this.sms.send(message);
}
isActive() {
return this.scheduler.isBusinessHours();
}
schedule(message) {
this.sms.schedule(message);
}
}
Now all we need to do is work out which type of NotificationService to create in our code.
现在,我们需要做的就是确定在代码中创建哪种类型的 NotificationService。
class Notifier{
constructor(scheduler){
this.scheduler = scheduler;
this.sms = new Sms();
this.smtp = new Smtp();
}
publishNotification(message, channelType) {
const notificationService = this.getNotificationService(channelType);
if (notificationService.isActive()) {
notificationService.send(message);
}
else {
notificationService.schedule(message);
}
}
getNotificationService(channelType){
switch (channelType) {
case "Email":
return new EmailService(this.smtp);
case "SMS":
return new SmsService(this.sms, this.scheduler);
default:
throw new Error(`Notification Type not supported: ${channelType}`);
}
}
}
We have eliminated all but one of the switch statements and our resulting code is much simpler.
Another pattern that often fits well for this kind of problem is the ‘Factory’ pattern. The creation of our NoticationService can be extracted into a factory class.
除了一个开关语句外,我们省去了其他所有的开关语句,这样代码就简单多了。
另一种经常适用于此类问题的模式是 "工厂 "模式。我们可以将创建 NoticationService 的过程提取到一个工厂类中。
class NotificationServiceFactory{
constructor(scheduler){
this.scheduler = scheduler;
this.sms = new Sms();
this.smtp = new Smtp();
}
create(channelType){
switch (channelType) {
case "Email":
return new EmailService(this.smtp);
case "SMS":
return new SmsService(this.sms, this.scheduler);
default:
throw new Error(`Notification Type not supported: ${channelType}`);
}
}
}
Our Notifier class can now be simplified even further.
我们的 Notifier 类现在可以进一步简化了。
class Notifier{
constructor(factory){
this.factory = factory;
}
publishNotification(message, channelType) {
const notificationService = this.factory.create(channelType);
if (notificationService.isActive()) {
notificationService.send(message);
}
else {
notificationService.schedule(message);
}
}
}
5. Start at the end(从最后开始)
This last technique is the only one in the list which is not a refactoring technique. It is a technique which really highlights the difference between how an experienced Senior Developer approaches writing code and how a less experienced Juniors Developer approaches it.
Before I explain, let’s walk through some examples.
Often when I see Juniors creating a solution, there is very little planning or design done before writing code. They will write out a load of code first, and then at the end work out how a method or test can be wrapped around it. The resulting classes, APIs and method signatures are usually poorly designed, hard to use and hard to change.
Experienced Developers on the other hand, will first think about the most suitable design for their high level APIs, classes and method signatures, and then work out how it can be achieved with code. Does the problem require Fluent, OOP, Functional, Data Driven programming? How will our consumers use our solution? How will we test it?
If you think this sounds familiar, then you’d be correct. This is the essence of Test-Driven Development (TDD). TDD is great because it gets us thinking about how our code will be consumed (via a test), before we work out how we will implement it.
Not everyone is a fan of TDD, and that’s fine. If you don’t like it, then you can still apply this technique by trying to think about how your code might look at the end, before getting coding. If you ever get the chance to write any shared library code for your team, it can be a great way of starting to put yourself in the shoes of your consumers and get thinking about the end result from their view first.
Remember that writing Clean Code is a process. One that we all must go through, no matter how much experience we have. Hopefully these tips and techniques will help you navigate your way through that process in a better way.
All the code examples and tests can be found in my GitHub repo below.
最后一项技术是列表中唯一一项不属于重构技术的技术。它是一种技术,真正突出了经验丰富的高级开发人员与经验较少的初级开发人员在编写代码时的不同之处。
在解释之前,让我们先看几个例子。
我经常看到初级开发人员在创建解决方案时,在编写代码之前很少进行规划或设计。他们会先写出一大堆代码,最后再研究如何将方法或测试封装在代码中。这样设计出来的类、应用程序接口和方法签名通常都很糟糕,难以使用,也难以更改。
另一方面,有经验的开发人员会首先考虑如何为高级应用程序接口、类和方法签名进行最合适的设计,然后再研究如何用代码来实现。问题是否需要流畅、OOP、功能、数据驱动编程?用户将如何使用我们的解决方案?我们将如何测试?
如果你觉得这听起来很熟悉,那就对了。这就是测试驱动开发(TDD)的精髓。TDD 非常棒,因为它能让我们在确定如何实现代码之前,先思考代码将如何被使用(通过测试)。
并不是每个人都喜欢 TDD,这没关系。如果你不喜欢它,那么你仍然可以应用这种技术,在开始编码之前,试着想想你的代码最终会是什么样子的。如果你有机会编写任何共享库代码,你都可以使用 TDD。
【参考文献】
文章:5 Quick Tips And Techniques To Write Better Code
作者:Matt Bentley
日期:9月29日
上述译文仅供参考,原文请查看下面链接,解释权归原作者所有
⚠️:文章翻译如有语法不准确或者内容纰漏,欢迎评论区指正。
【关于TalkX】
TalkX是一款基于GPT实现的IDE智能开发插件,专注于编程领域,是开发者在日常编码中提高编码效率及质量的辅助工具,TalkX常用的功能包括但不限于:解释代码、中英翻译、性能检查、安全检查、样式检查、优化并改进、提高可读性、清理代码、生成测试用例等。
TalkX建立了全球加速网络,不需要考虑网络环境,响应速度快,界面效果和交互体验更流畅。并为用户提供了OpenAI的密钥,不需要ApiKey,不需要自备账号,不需要魔法。
TalkX产品支持:JetBrains (包括 IntelliJ IDEA、PyCharm、WebStorm、Android Studio)、HBuilder、VS Code、Goland.