Why You Shouldn't Nest Your Code

CodeAesthetic
6 Dec 202208:29

Summary

TLDR本视频探讨了编程中的“永不嵌套”(never nester)现象,即避免代码过度嵌套以提高可读性。视频中通过实例展示了如何通过提取和反转条件来简化嵌套代码,从而使得代码结构更加清晰。提到了Linux内核风格指南中对嵌套层数的限制,强调了限制嵌套层数有助于编写更优质的代码。

Takeaways

  • 🚫 从不嵌套(never nester)是指那些不喜欢在代码中使用多层嵌套的程序员。
  • 📈 嵌套代码时,每增加一个内层块(如if语句或循环),就会增加代码的深度。
  • 🔢 从不嵌套的程序员通常对代码的嵌套深度感到厌恶,尤其是当嵌套超过三层时。
  • 🔄 解决嵌套问题的两种方法是提取(extraction)和反转(inversion)。
  • 📂 提取是将函数的一部分抽取到自己的函数中,以减少嵌套。
  • ↔️ 反转是通过翻转条件并使用早期返回(early return)来简化代码结构。
  • 🔄 应用反转可以创建一个验证门卫部分的代码,它声明了函数的要求,然后是实际功能的核心。
  • 📊 在阅读代码时,反转可以使人更容易忽略条件,专注于核心代码。
  • 🔍 通过提取和反转,可以将一个大的、处理多个任务的函数重构为多个小的、单一职责的函数。
  • 📝 Linux内核风格指南建议,如果代码需要超过三层的缩进,应该重构代码。
  • 📈 限制嵌套可以迫使程序员写出更好的代码,因为它们需要考虑如何简化代码结构。

Q & A

  • 什么是“never nester”?

    -“never nester”是指那些不喜欢嵌套代码的程序员。他们通常会避免在函数中添加过多的内部代码块,以减少缩进级别。

  • 为什么“never nester”不喜欢嵌套代码?

    -“never nester”认为嵌套代码会增加代码的复杂性,使得阅读和理解代码变得更加困难。他们倾向于保持代码的简洁和清晰。

  • 在脚本中提到的“disgust-o-meter”是什么?

    -“disgust-o-meter”是一个比喻,用来形象地表示程序员对代码嵌套深度的厌恶程度。嵌套层次越多,这个“厌恶计”就越高。

  • 脚本中提到了两种减少代码嵌套的方法,它们是什么?

    -两种减少代码嵌套的方法是提取(Extraction)和反转(Inversion)。提取是将函数的一部分抽取成独立的函数;反转是通过改变条件判断的顺序,使用早期返回(early return)来简化代码结构。

  • 如何在代码中应用提取方法?

    -应用提取方法时,可以将循环或条件判断中的内部代码块移动到一个新的函数中,从而减少原有函数的嵌套深度。

  • 反转条件(Inversion)在代码重构中有什么作用?

    -反转条件可以帮助程序员将代码的“快乐路径”(即正常执行路径)放在更浅的嵌套级别,而将错误处理和异常路径放在更深层次。这样可以使得核心逻辑更加清晰,易于理解和维护。

  • 脚本中提到的Linux内核风格指南对缩进有什么建议?

    -Linux内核风格指南建议,如果代码需要超过三层的缩进,那么应该重构代码。它还通过将制表符设置为8个字符宽来视觉上强制限制缩进。

  • 在脚本中提到的下载文件的代码示例中,主要的处理流程是怎样的?

    -下载文件的代码示例主要处理流程包括:从队列中获取待下载的URL,管理当前下载任务,处理下载状态(如待处理、进行中、已完成),以及处理错误和重试逻辑。

  • 脚本中提到的重构前后的代码有什么不同?

    -重构前的代码高度嵌套,难以追踪和理解。重构后的代码通过提取和反转方法,将大的函数拆分成了多个小的、单一职责的函数,使得代码结构更加清晰,逻辑更易于理解。

  • 脚本中提到的“happy path”和“unhappy path”分别指什么?

    -“happy path”指的是代码的正常执行路径,即没有错误和异常发生的情况。而“unhappy path”则是指错误处理和异常情况的路径。

  • 为什么限制代码嵌套深度有助于编写更好的代码?

    -限制代码嵌套深度迫使程序员思考如何简化逻辑,避免复杂的条件判断和深层嵌套。这通常会导致代码更加模块化,每个函数都有明确的职责,从而提高代码的可读性和可维护性。

Outlines

00:00

😎 从不嵌套的编程哲学

本段介绍了所谓的“从不嵌套者”(never nester),这是一种编程风格,避免在函数中使用过多的嵌套。作者通过比喻和实例解释了嵌套代码的复杂性,并提出了两种减少嵌套的方法:提取(extraction)和反转(inversion)。提取是将函数的一部分抽离成独立的函数,而反转则是改变条件判断的顺序,使用早期返回(early return)来简化代码结构。

05:01

🔍 应用提取和反转重构代码

这段内容继续探讨如何通过提取和反转来重构代码,使其更加清晰易读。作者通过一个下载文件的例子,展示了如何将复杂的嵌套逻辑分解成更小、更易于管理的函数。这个过程不仅提高了代码的可读性,还符合Linux内核风格指南中关于缩进级别的建议。作者强调,限制嵌套层数可以迫使开发者编写更优质的代码。

Mindmap

Keywords

💡从不嵌套

“从不嵌套”是指一种编程风格,避免在代码中使用过多的嵌套结构。这种风格强调代码的平坦化,以提高可读性和维护性。在视频中,讲述者通过展示嵌套代码和重构后的非嵌套版本,强调了减少嵌套深度的重要性。例如,通过提取函数和条件反转来降低函数的嵌套深度。

💡厌恶计量器

“厌恶计量器”是讲述者用来形象描述当代码的嵌套层数增加时,开发者感到的不适感增加的概念。这个概念强调了嵌套深度对代码可读性和维护难度的负面影响。在视频中,这一概念用来解释为什么“从不嵌套”的开发者倾向于避免深层嵌套的代码。

💡提取

在视频中,“提取”是一种重构技术,指将代码块从一个较大的函数中移出,创建一个新的函数。这有助于减少代码的嵌套深度,使代码更易于理解和维护。通过示例,讲述者展示了如何通过提取代码中的某个部分到独立的函数来减少嵌套。

💡条件反转

“条件反转”是一种编程技巧,通过改变条件语句的逻辑来减少嵌套。这通常涉及将深层嵌套的条件逻辑改为提前返回(早退模式),从而减轻代码深度。视频中提到,通过将深层条件逻辑反转,可以显著改善代码结构和可读性。

💡快乐路径

在编程中,“快乐路径”指的是代码执行时没有遇到错误或异常情况的理想路径。视频中提到,通过将条件反转应用于快乐路径,将非理想情况(错误或异常处理)提前处理,可以减少嵌套并简化代码结构。

💡代码重构

“代码重构”指的是修改代码的结构而不改变其外部行为的过程,以提高代码的可读性、可维护性或性能。视频中,通过提取和条件反转技巧的应用,展示了如何重构深层嵌套的代码,以达到减少嵌套深度的目的。

💡异步下载

“异步下载”在视频中指的是一种编程模式,用于在不阻塞主线程的情况下进行文件下载。视频通过一个例子展示了如何管理和处理多个异步下载任务,包括使用状态管理和重构技巧来提高代码的可读性和维护性。

💡Linus Torvalds

Linus Torvalds是Linux内核的创造者,视频中提到他可能也是一个“从不嵌套”的支持者。这一点通过引用Linux内核风格指南中关于限制嵌套深度的建议来说明,强调了即使在复杂系统的开发中,限制嵌套深度也是一种重要的代码质量控制手段。

💡失败队列

在视频中提到的“失败队列”,是一种用于处理异步操作失败任务的数据结构。通过将失败的下载任务移入失败队列,可以分离正常逻辑和错误处理逻辑,有助于简化代码结构并提高可维护性。这一概念展示了如何在实践中应用提取和条件反转技巧来处理复杂的异步流程。

💡主线程

“主线程”在视频的上下文中指的是程序的主要执行线程,通常负责处理用户界面和事件循环。在讨论异步下载管理时,视频强调了在主线程中有效管理异步任务的重要性,以及如何通过代码重构技巧减少主线程的负担,使程序更加响应用户操作。

Highlights

Never nesters are programmers who dislike deeply nested code.

Linus Torvalds is suspected to be a never nester, as per Linux kernel style guidelines.

Deeply nested code increases cognitive load, making it harder to follow.

A never nester's disgust-o-meter grows with the number of nested tabs.

The maximum nesting level a never nester can handle is three deep.

Two methods to reduce nesting are extraction and inversion.

Extraction involves pulling out part of a function into its own function.

Inversion involves flipping conditions and using early returns to simplify code structure.

Applying extraction and inversion can lead to a validation gatekeeping section in the code.

The happy path of code should be at the main level, while error paths are indented.

Limiting indentation can force programmers to write better, more concise code.

The Linux kernel style guidelines recommend against more than three levels of indentation.

The tab size in the Linux kernel is set to eight characters wide to visually enforce indentation limits.

Breaking down a complex function into smaller, single-responsibility functions improves code clarity.

The main function should outline the high-level logic of the program.

Refactoring a complex, nested function can make it easier to understand and maintain.

Transcripts

00:00

I have to admit, I'm a never nester.

00:04

I know.

00:05

Shocking.

00:06

But there are more of us than you think.

00:08

Dozens. Even Linus Torvalds is one.

00:11

I mean, I haven't asked him, but I'll show you what I mean in a little bit.

00:16

You might be wondering, well, what is a never nester.

00:18

A never nester never nests their code.

00:22

Okay. Not never.

00:24

But we do have a disgust-o-meter which grows

00:27

uncontrollably as the number of tabs go up.

00:31

Nesting code is when you add more inner blocks to a function.

00:35

We’ll consider each open brace to be adding one more depth to the function.

00:41

So this function is one deep because there's no inner blocks.

00:45

And if we add an if statement, we've made it two deep.

00:49

If we add a loop, we've now made this function three deep.

00:54

And this, my fellow

00:55

programmers, is the maximum a never nester can handle.

00:59

A never nester doesn't dare to go four deep.

01:02

Now the perverse among you might wonder what four deep even looks like.

01:07

And while it brings me great pain to do,

01:09

I understand that I must show you for science.

01:13

Here is four deep.

01:27

We've now taken a reasonably readable function and dramatically increased

01:31

the amount of conditions your brain must simultaneously hold.

01:35

But what can we do about it?

01:37

Well, there's two methods you can use to denest: Extraction.

01:42

This is where you pull out part of the function into its own function.

01:46

And inversion, which is simply flipping conditions

01:49

and switching to an early return.

01:52

Let's look at extraction first.

01:55

We can extract the inner part of the loop into its own function.

02:02

Now we can apply inversion.

02:05

When you put the happy path of code

02:07

within deeper and deeper blocks, it creates a lot of nesting.

02:11

Instead, we'll invert that condition and put the unhappy first.

02:16

First, we'll flip our if else, by inverting the condition.

02:21

Now, since we can return here, we know that the else block isn't

02:24

actually needed so we can flatten our else into the main level.

02:29

Now, if we hit our unhappy case

02:31

condition here, we simply get out of the way.

02:35

And then the main part of the code can do its job.

02:39

When you have a lot of conditions to check like this,

02:42

we can apply inversion over and over again

02:45

and we end up with a sort of validation gatekeeping section of the code

02:50

which sort of declares the requirements of the function.

02:54

And then we have the

02:55

crux of the real functionality here.

02:58

And you'll notice that the happy path moves

03:00

down the function and all of the error paths.

03:04

They're indented.

03:07

When reading this code, I find I can mentally discard

03:10

the condition and focus on the core code versus when it's nested.

03:14

I find myself having to hold these ideas in my head.

03:17

I'm curious if you’ve experienced the same thing?

03:21

Let's look at a larger example.

03:24

All right.

03:25

Look at this beauty.

03:28

Before we go refactoring it,

03:30

let me walk you through what's happening.

03:34

The goal of this code is to download a bunch of files from the web.

03:38

It talks with this download class that we can't alter.

03:43

It's an async download.

03:45

So when we start the download, you have to call process over and over again.

03:50

And each

03:50

time it gives you one of these results.

03:54

If it returns InProgress, we'll need to keep calling process() more.

03:59

On top of that, we want to download multiple files at once in the background.

04:04

So we've created a thread that manages all of them.

04:09

The way new downloads enter the system is through this append download method,

04:13

which puts the requested URLs onto a queue.

04:17

The thread then wakes up and grabs the URLs from the queue

04:20

and then adds them to this list of current downloads.

04:25

Each download is given a state

04:27

which is either pending InProgress or Complete.

04:31

So in each cycle of the main loop, the thread walks

04:33

through each download and checks what it needs to do with it.

04:38

If it's pending we start a new download.

04:42

If it's complete,

04:44

we simply remove it from the list.

04:48

If it's InProgress, we call that process method we mentioned earlier

04:52

and figure out what's happening with the download.

04:55

If it's completed successfully, we mark it as complete

04:58

so it can get removed from the list and InProgress means

05:01

we do nothing because it's still ongoing.

05:04

But things get interesting if we hit an error,

05:08

if the connection was okay, but we got an unhappy HTTP response.

05:12

We determine whether the air is retriable

05:15

If it is, we retry up to three times,

05:18

setting the download back to Pending.

05:21

Once it's failed plenty, we ejected from our download list

05:25

and push it to a failure queue for someone else to deal with.

05:29

For connection error, we retry three times.

05:31

Then we set the special connection disabled flag and this causes us

05:36

to basically give up on every download and clear the list.

05:41

Okay, so there's a lot going on here and it's all heavily

05:45

nested in this function, which makes it hard to follow.

05:48

So let's apply extraction and inversion to flatten

05:53

the first two big candidates.

05:55

Here are the two big branches of download processing:

05:58

Pending and InProgress.

06:01

So let's extract these out.

06:04

We'll move the pending part to processPending()

06:12

and InProgress

06:14

to processInProgress().

06:22

That's a bit better.

06:24

But this in-progress function is still too deep for my liking.

06:28

The worst offender is this HTP error section.

06:31

So let's move that out as well.

06:35

Now we'll keep extracting further in our run function.

06:38

We have four major sections of our code.

06:42

Where we process incoming requests from the queue;

06:46

Where we deal with our current downloads;

06:50

where we clear out the InProgress downloads,

06:53

and where we wait for the signal that there's new downloads to look at.

06:56

So let's do it.

07:09

So now

07:10

our main function clearly outlines the steps that are happening.

07:14

You can see the high level logic I described before.

07:18

And if you were to dig into any of these functions,

07:20

there are also concise.

07:24

At the beginning of this video, I mentioned that Linus

07:27

Torvalds is a suspected never nester.

07:31

And I say this because in the Linux kernel style guidelines they state

07:35

if you need more than three levels of indentation, you're screwed anyway

07:39

and should fix your program.

07:43

The kernel dudes are always so dramatic.

07:47

They visually enforce this

07:48

by making the tab size eight characters wide.

07:52

This is what eight characters look like with heavy nesting.

07:56

Yeah.

07:58

I'll admit I'm not that committed to the cause, but I am into limiting indentation.

08:05

I believe that constraining

08:06

how much you nest forces you to write better code.

08:11

If you notice, instead of one large function that handles many things.

08:15

We now have small, concise functions that have one responsibility.

08:20

What do you think?