JavaScript 优化(一)消灭缩进

阅读 1467
收藏 70
2017-03-27
原文链接:jrsinclair.com

This is part one of a series on how to write less complex code in JavaScript

Introduction

I’ve been working a lot with a legacy code-base lately. And this one is particularly troublesome. It has all the hallmarks of a rush job. The code is brittle. There are no tests. Things often seem to break at random. And to my embarrassment, I wrote most of it.

Part of the problem is that this is a complicated application. There are three different servers involved, and many different application features. But the trouble is not with the scope of the app. The trouble is with the code itself. Everything is intertwined, like spaghetti. And it’s this intertwining that makes it complicated.

Simplicity vs complexity vs ease

Complexity matters. Rich Hickey gave an amazing talk called Simplicity Matters at Rails Conf 2012. If you haven’t seen it, I recommend watching. It’s not about Ruby. It’s about software in general. In the talk, he draws a distinction between simplicity and ease. According to his definition, something is simple when it lacks complexity. And something is complex when it has many intertwined parts. In contrast, he defines ease as being close to hand (either in a metaphorical or literal sense). In this definition, a glass might be easy to reach because it is close by. A concept might be ‘easy to grasp’ because it is familiar (mentally close by). But just because something is easy, it does not mean that it is also simple.

Indentation as a measure of complexity

When it comes to coding in JavaScript, I’ve noticed a pattern. The more indentation in my code, the more complicated it is. And, the less indentation in my code, the simpler it is. Complicated code tends to look like a sideways ‘V’ or an angle bracket. Simple code tends to look more like a brick, or a rectangle.

Complicated code tends to look like a sideways V or an angle bracket. Simple code tends to look more like a brick, or a rectangle.
Complicated code tends to look like a sideways ‘V’ or an angle bracket. Simple code tends to look more like a brick, or a rectangle.

But pause with me a moment, and consider why we indent things in the first place. The compiler doesn’t care about indentation. Heck, we minify our JS code all the time and get rid of all the indentation. There’s nothing in those spaces (or tabs) that makes the code run differently. (This is JavaScript, not Python.) No, the indentations are there for humans. They help us read the code. We indent to signify that this code is grouped together in a block. It says: This code is special. There is something you have to keep in mind while you read this code. It’s different from the other code around it.

So, when you see an indented piece of code, there is something you have to remember while you read that code. We call this something context. And the more levels of indentation, the more context you have to keep in mind. Each level of indentation adds cognitive load. Each level of indentation intertwines some extra stuff. Each level of indentation indicates added complexity.

Now, this is a good thing. The indentation shows us at a glance how complicated our code is. So I must admit here that the title I have chosen is somewhat misleading. Indentation is not the real enemy. The real enemy is complexity. Indentation is the guard dog barking madly to let us know that complexity is creeping in.

There will always be some indentation in our code. There is always some inherent complexity in the systems we build. If there wasn’t, we wouldn’t need to write the software in the first place. But there are ways to write code that reduce complexity. And indentation disappears along with it. Much of the complexity introduced by control structures in our code doesn’t need to be there.

Control Structures

What I’m suggesting is that complexity creeps into our code through control structures. Through if-statements and loops, switches and exception handling. These are the things that we indent. So, if we rework or remove the control structures in our code, then we can reduce the complexity. As a by-product, the indentation tends to disappear too.

Now, we can’t get rid of control structures completely. If we had no control structures all our programs would do nothing but return a constant. We’d never get beyond ‘Hello world’. Programs need to respond to different inputs. So we have to have control structures somewhere. But we can recognise patterns in our code. We can then replace low-level, complicated implementations with less complicated abstractions.

Abstraction

Abstraction is a problematic term. It is an important concept in computer science and mathematics. But it comes with baggage.

To abstract is to consider something theoretically or separately from (something else).[1] When we abstract a code pattern, we separate the use case from the implementation details. This is incredibly useful. But unfortunately, in popular use, the term connotes vagueness and lack of practicality. When someone describes a thing as abstract, we associate it with being impractical. It’s academic; theoretical; hand-wavy; difficult to understand. But abstraction lets us be more expressive, not less. Hiding some of the implementation details lets us see the forest by hiding the trees. We describe what we want to do rather than the specifics of how.

JavaScript itself is an abstraction. Instead of writing assembly code to tell the computer what to do, we code in a higher-level language. We don’t have to worry about the details of what instructions the particular CPU we’re running on supports. We tell the computer what to do, and the JavaScript interpreter figures all that out for us. And when we use a library like jQuery or loadash or Ramda, we are moving up another level of abstraction. With jQuery, I can make an AJAX call with $.get(). But I don’t have to know the specific details of how each browser implements XMLHttpRequest.

Moving up a level of abstraction allows us to express what we want to do with more clarity. Take, for example, the lodash method pluck(). Without lodash, we could write something like this:

const myArray = [{id: 'a'}, {id: 'b'}, {id: 'c'}];
let ids       = [];
for (let i = 0; i < myArray.length; i++) {
    ids.push(myArray[i].id);
}
console.log(ids); //=> ['a', 'b', 'c']

But with lodash we can write:

import {pluck} from 'lodash';
const myArray = [{id: 'a'}, {id: 'b'}, {id: 'c'}];
const ids     = pluck('id', myArray);
console.log(ids); //=> ['a', 'b', 'c']

Now, that may not seem like such a big deal. We saved one or two lines of code. But that pluck() function is more expressive than a for-loop. It conveys more information to the reader about what is going on. We are extracting the id attribute values from the elements of myArray. The function name pluck describes that pattern and makes it clear at a glance. But in the for-loop version, I have to read through the entire loop and recognise the pattern myself. The pluck() function conveys more information in less space. That is the beauty of abstraction.

Choosing the right abstraction has a double benefit:

  1. The code becomes more expressive. It conveys more information to the reader about what we’re trying to achieve; and
  2. We remove complexity by hiding the implementation details.

Now you may be thinking “Wait a second here. Using pluck() doesn’t remove the for-loop, it just buries it inside another function. The loop is still there. It’s just hidden now.” And that is correct. But that is also the point. By using pluck() we made the complexity of that for-loop someone else’s problem. In this case, the maintainers of lodash. They put a lot more effort into optimising these functions than I ever could on a single project.

So yes, most of the time we are burying complexity, rather than removing it completely. But that still has enormous benefits. Even if I write my own version of pluck(), if I use it more than once, then I’ve removed complexity in at least two places. The complexity is now concentrated into one function. And I’ve also increased the expressiveness of my code. Squishing complex code into one function is much better than smearing it everywhere.

Pure Functions

So, we want to reduce complexity, and control structures are a source of complexity. We can wall off complexity by recognising patterns and replacing them with abstractions. But, how do we go about finding these patterns? One way is by simply practicing a lot until you find yourself doing the same things over and over. At the heart of the repetition you will find potential patterns and abstraction. But this isn’t very efficient. Another approach is to do what mathematicians do. They transform the problem into a different representation. Then they examine how that helps to reason about the problem.

In JavaScript, the handiest tool we have for this purpose is the humble function. We can take almost any block of code and wrap it in an immediately invoked function expression (IIFE). An IIFE looks like this:

(function myWrapperFunction() {
  // code in here is executed immediately
}())

Once we’ve wrapped some code up like this, then we can start to reason about its purity. A pure function, by definition, excludes certain sources of complexity. Pure functions don’t access global variables. Pure functions don’t write to the console or manipulate the DOM. Pure functions don’t read or write files, or access the network. We call these things side-effects. By definition, we never have to worry about side effects when dealing with pure functions.

Since there are no side effects, the only thing a pure function can do is transform data into other data. This means that pure functions must always return a value. This might not seem very significant, but knowing this is useful. It gives us an easy method to detect impurity. If a function does not return a value, it is either impure or does nothing.[2]

We’ll see how this works in more detail as we examine each type of control structure. But for now, we can start to simplify our code by using pure functions whoever we can.

Why?

We’ve talked about complexity and how excessive indentation indicates complicated code. But why do we care? Why go to the effort of trying to reduce complexity? It can be a lot of effort. As Dijkstra says:

Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better.[3]

In short, we want to reduce complexity because it makes the world a better place. Simple code has fewer bugs, which provides a better experience for users. Fewer bugs makes life better for your development team who have to maintain the software. This is true even if it’s a team of one. When it does break, simple code is easier to fix.

Of course, this is nothing like fighting hunger or poverty or injustice. If you have the means and inclination to fight those things, please do. But that said, many of us still write software for a living each day. Reducing complexity is a very small way to make the world a better place. If we’re going to be writing code anyway, let’s fight for simplicity.

If making the world a better place isn’t your thing, or you think I’m full of it, then have a listen to Fred George talking about the ‘Secret Assumption of Agile’. He talks about many of the same concepts and describes amazing results. Maybe he will convince you.

Here endeth part one. In the next post we’ll start fighting complexity by removing loops from JavaScript code…


  1. “abstract, v.”, Oxford English Dictionary.  ↩

  2. Technically, if you don’t specify a return value, all JavaScript functions will return undefined. And there are some rare occasions where we do want to return undefined. But even then, it is better to make the return value explicit by writing return undefined.  ↩

  3. Edsger W. Dijkstra, 1984, On the nature of Computing Science (EWD 896)  ↩


本文对你有帮助?欢迎扫码加入前端学习小组微信群:

评论