It has 5,914 words and takes eight minutes to read. Compiled from Eric Elliott’s article, the code written by a good programmer is like a beautiful poem that is very enjoyable to read. How can we get to that level? To figure this out, look at how good writing is written.
William Strunk listed seven principles for good writing in his 1920 book The Elements of Style, and nearly a century later, they haven’t gone out of Style. For engineers, code is an important output that is written once, revised many times, and read many times. Readability is critical, and we can use these writing principles to guide our daily coding and write highly readable code.
It’s important to note that these principles are not laws, and it’s fine to violate them if it makes your code more readable, but we need to be vigilant and introspective, because these time-tested principles are usually true, and it’s best not to violate them out of whimsy or personal preference.
The seven principles of writing are as follows:
- Make paragraphs the basic units of writing, each saying only one thing;
- Omit unnecessary words;
- Use active;
- Avoid a series of loose sentences;
- Put related content together;
- Multipurpose affirmative statements;
- Make good use of parallel structure;
Correspondingly, when coding:
- Let functions be the basic unit of coding, and each function does only one thing;
- Omit unnecessary code;
- Use active;
- Avoid a series of loose expressions;
- Put related code together;
- Multipurpose affirmative statements;
- Make good use of parallel structure;
1. Make functions the basic unit of coding, and each function does only one thing
The essence of software development is composition. We build software by composing modules, functions, and data structures together.
The essence of software development is composition. We construct software by combining modules, functions and data structures. Understanding how to write and combine functions is a basic skill for software engineers. A module is usually a collection of one or more functions and data structures, and data structures are our way of representing the state of the program, but usually nothing happens until we call a function. In JS, we can divide functions into three types:
- 3. I/O Functions: Do DISK or network I/O;
- Procedural Functions: organize sequences of instructions;
- Mapping Functions: compute and convert inputs to return outputs;
While useful programs require I/O, most programs have procedural instructions, and most functions in programs are mapping functions: given input, the function returns the corresponding output.
Each function does one thing: if your function does network requests (I/O type), don’t mix in data conversion code (mapping type). If strictly defined, procedural functions clearly violate this principle, but they also violate another: avoiding a series of loose expressions.
The ideal function should be simple, deterministic, and pure:
- With the same input, the output is always the same;
- There are no side effects;
More on pure functions can be found here.
2. Skip unnecessary code
“Vigorous writing is concise. A sentence should contain no unnecessary words, A paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. This requires not that the writer make all sentences short, or avoid all detail and treat subjects only in outline, But that every word tell.”
Clean code is critical to software quality because more code equals more bug hiding places, in other words: less code = fewer bug hiding places = fewer bugs.
Clean code reads clearer because it has a higher signal-to-noise Ratio: it’s easier to read code and sort out the meaningful bits from the less syntactical Noise, so to speak, less code = less syntactical Noise = higher Signal strength
To paraphrase The Elements of Style: Simple code is more powerful, like this:
function secret (message) { return function () { return message; }};Copy the code
Can be simplified as:
const secret = msg => () => msg;Copy the code
Obviously, the simplified code is more readable to those familiar with arrow functions because it omits unnecessary syntactic elements: curly braces, the function keyword, and the return keyword. The syntax elements contained in the pre-simplified code are of little use in conveying the meaning of the code itself. Of course, if you’re not familiar with the syntax of ES6, this might seem weird to you, but ES6 has been the new language standard since 2015, and if you’re not already familiar with it, it’s time to upgrade.
Omit unnecessary variables
It’s tempting to impose names on things that don’t really need to be named. The problem is that a person’s working memory is finite, and every variable takes up space in it when reading code. For this reason, experienced programmers try to eliminate unnecessary variable naming as much as possible.
For example, in most cases you don’t need to name a variable that is just a return value. The function name should be sufficient to indicate what you are returning. Consider the following example:
// const getFullName = ({firstName, lastName}) => {const fullName = firstName + "+ lastName; return fullName; }; Const getFullName = ({firstName, lastName}) => (firstName + ' '+ lastName);Copy the code
Another way to reduce variables is to use point-free style, a concept from functional programming.
Point-free-style is a way of defining a function that does not refer to arguments operated by a function. Common ways to implement point-free-style include function composotion and function currying.
Let’s look at an example of a function corrification:
const add = a => b => a + b; // Now we can define a point-free inc() // that adds 1 to any number. const inc = add(1); inc(3); / / 4Copy the code
Careful students will notice that the inc function is not defined using the function keyword or the arrow function syntax. Add also does not list the parameters required by Inc, because the add function does not need to use these parameters internally. It just returns a new function that can handle the parameters itself.
Function composition is the process of taking the output of one function as the input of another. Whether you realize it or not, you’re already using function combinations a lot, and the code for chain calls is basically this pattern, like map for array operations and then for Promise operations. Function combination is also called higher-order function in functional language, and its basic form is: F (g(x)).
When you combine two functions, you eliminate the need to store intermediate results in variables. Here’s an example of how combining functions makes code cleaner:
Start by defining two basic operation functions:
const g = n => n + 1;
const f = n => n * 2;Copy the code
Our calculation requirements are: given the input, first +1, then x2, the common practice is:
// Const incThenDoublePoints = n => {const increed = g(n); return f(incremented); }; incThenDoublePoints(20); / / 42Copy the code
To use a combination of functions, write:
Const compose = (f, g) => x => f(g(x)); const compose = (f, g) => f(g(x)); const incThenDoublePointFree = compose(f, g); incThenDoublePointFree(20); / / 42Copy the code
A similar effect can be achieved by using the funcot function, which wraps parameters into traversable arrays and then uses map or Promise’s then to implement chained calls as follows:
const compose = (f, g) => x => [x].map(g).map(f).pop(); const incThenDoublePointFree = compose(f, g); incThenDoublePointFree(20); / / 42Copy the code
If you choose to use the Promise chain, the code will look very similar.
Almost all libraries that provide functional programming tools provide at least two modes of function composition:
- Compose: executes the function from right to left
- Pipe: Executes functions from left to right;
Compose () and flow() in Lodash correspond to these two modes, respectively. Here’s an example using flow() :
import pipe from 'lodash/fp/flow'; pipe(g, f)(20); / / 42Copy the code
If you don’t use LoDash, you can do the same thing with the following code:
const pipe = (... fns) => x => fns.reduce((acc, fn) => fn(acc), x); pipe(g, f)(20); / / 42Copy the code
If the combination of functions described above strikes you as alien, and you are not sure how you would use them, consider the following sentence:
The essence of software development is composition. We build applications by composing smaller modules, functions, and data structures.
From this, it is not hard to infer that understanding how functions and objects are combined is as important to an engineer as understanding electric drills and percussion drills are to a decorator. When you use imperative code to combine functions with intermediate variables, it’s like using duct tape to force them together, and the combination of functions looks more natural.
Without changing the way your code works or making your code less readable, here are two things you should always keep in mind:
- Use less code;
- Use fewer variables;
3. Use active
“The active voice is usually more direct and vigorous than The passive.”
Active is usually more direct and powerful than passive, and variable names should be as direct as possible without beating around the bush, for example:
myFunction.wasCalled()
Better thanmyFunction.hasBeenCalled()
;CreateUser () is better than that
The User. The create () `;notify()
Better thanNotifier.doNotification()
;
Name Boolean values as if they were questions with only yes and no answers:
isActive(user)
Better thangetActiveStatus(user)
;isFirstRun = false;
Better thanfirstRun = false
;
Use verbs whenever possible when naming functions:
increment()
Better thanplusOne()
unzip()
Better thanfilesFromZip()
filter(fn, array)
Better thanmatchingItemsFromArray(fn, array)
Event Handlers and Licecycle Methods are special because they are more about when to do something than what to do, and their names can be simplified to “< timing >, < verb >”.
Here is an example of an event listener function:
element.onClick(handleClick)
Better thanelement.click(handleClick)
component.onDragStart(handleDragStart)
Better thancomponent.startDrag(handleDragStart)
If you look closely at the second half of the above two examples, they read more like triggering events than reacting to them.
As for life cycle functions, consider the name of the function that should be called before the React component is updated:
- componentWillBeUpdated(doSomething)
- componentWillUpdate(doSomething)
- beforeUpdate(doSomething)
ComponentWillBeUpdated is passive, meaning to be updated, not to be updated, a bit tongue-tied and obviously not as good as the latter two.
ComponentWillUpdate is better, but the name is more like calling doSomething. Calling doSomething beforeComponent updates makes our intent clear beforeComponentUpdate.
To simplify things further, since these lifecycle methods are built into Component, adding Component to a method is redundant. Imagine calling this method directly on a Componenent instance: Component.com ponentWillUpdate, we don’t need to repeat the subject two times. Obviously, component. The beforeUpdate doSomething () than component. BeforeComponentUpdate doSomething () is more direct, simple and accurate.
There is also [8] [Functional Mixins], which act as an assembly line for adding methods or properties to objects passed in. Such functions are usually named using adjectives, such as various words with the suffix “ing” or “able”. Examples:
const duck = composeMixins(flying, quacking); Const box = composeMixins(iterable, mappable); const box = composeMixins(iterable, mappable); // traversableCopy the code
4. Avoid a series of loose expressions
“… A series soon becomes really up to date and poignant.”
Loose strings of code often become tedious, and piecing together loosely related but sequentially executed statements into procedural functions makes it easy to write spaghetti code. This is often repeated many times, with only subtle differences, if not strictly repeated.
For example, if different components on an interface share almost identical logical structures, consider the following example:
const drawUserProfile = ({ userId }) => {
const userData = loadUserData(userId);
const dataToDisplay = calculateDisplayData(userData);
renderProfileData(dataToDisplay);
};Copy the code
The drawUserProfile function actually does three different things: loads the data, calculates the view state from the data, and renders the view. In most modern front-end frameworks, these three things are nicely separated. By separating concerns, there are many more ways to extend and combine each concern.
For example, we can completely replace the render part without affecting the rest of the program, as illustrated by the various render engines of the React family: ReactNative is used for rendering apps in iOS and Android, AFrame for rendering WebVR, and ReactDOM/Server for server-side rendering.
Another problem with the drawUserProfile function is that there is no way to calculate the state of the view to finish rendering until the data has been loaded, which is a lot of duplication and waste if the data has been loaded elsewhere.
The separation of concerns allows each piece to be tested independently, and I like to add unit tests to my application and see the results every time I change the code. Imagine that if data retrieval and view rendering code were written together, unit testing would be difficult, either passing in fake data or switching to clunky E2E tests, which are often harder to give immediate feedback to because they take longer to run.
In the React scenario, drawUserProfile already has three separate functions that connect to the Component lifecycle methods. Data loading can be triggered when the Component is mounted, and data calculation and rendering can be triggered when the view state changes. As a result, the responsibilities of different parts of the program are clearly delineated, and each Component has the same structure and lifecycle methods, so programs run more stably and we have less repetitive code.
5. Put related code together
Many frameworks and project scaffolding dictate how to organize files by code category, which is fine if you’re developing a simple TODO application, but in large projects it’s often better to organize code by business function. Most of you may ignore the relationship between code organization and readability. Have you ever been on a project where you didn’t know where the code you were trying to fix was? What causes it?
Here are two ways to organize a TODO application code by code category and business function:
Organized by code category
├ ─ ─ components │ ├ ─ ─ todos │ └ ─ ─ the user ├ ─ ─ reducers │ ├ ─ ─ todos │ └ ─ ─ the user └ ─ ─ tests ├ ─ ─ todos └ ─ ─ the userCopy the code
Organized by business function
├ ─ ─ todos │ ├ ─ ─ component │ ├ ─ ─ reducer │ └ ─ ─ the test └ ─ ─ the user ├ ─ ─ component ├ ─ ─ reducer └ ─ ─ the testCopy the code
When we organize code by business function, we don’t have to jump up and down the file tree to find code to change a feature. There is also a section on Code organization in The Art of Readable Code for those who are interested.
6. Use positive statements
“Make definite assertions. Avoid tame, colorless, hesitating, Committal language. Use the word > not> as a means of denial or in antithesis, never as a means of evasion.”
To make firm assertions, avoid meek, colorless, hesitant statements and use not when necessary to deny, reject, or evade. A typical:
isFlying
Better thanisNotFlying
late
Better thannotOnTime
If statement
Handle error cases first, then normal logic:
if (err) return reject(err);
// do something...Copy the code
Better than handling the normal first and then the error :(the negative judgment of the error is really tiring to read)
if (! err) { // ... do something } else { return reject(err); }Copy the code
Ternary expression
Put the positive first:
{
[Symbol.iterator]: iterator ? iterator : defaultIterator
}Copy the code
Better than putting the negative first (there is a design principle called Do not make me think, which applies appropriately) :
{ [Symbol.iterator]: (! iterator) ? defaultIterator : iterator }Copy the code
Use negation appropriately
Sometimes we only care if a variable is missing, and using a positive name forces us to negate the variable. In this case, using the “not” prefix and negating operator is less straightforward than using negating statements, such as:
if (missingValue)
Better thanif (! hasValue)
if (anonymous)
Better thanif (! user)
if (isEmpty(thing))
Better thanif (notDefined(thing))
Make good use of named parameter objects
Do not expect function callers to pass undefined or null to fill in optional arguments. Instead, learn to use named argument objects such as:
const createEvent = ({
title = 'Untitled',
timeStamp = Date.now(),
description = ''
}) => ({ title, description, timeStamp });
// later...
const birthdayParty = createEvent({
title: 'Birthday Party',
description: 'Best party ever!'
});Copy the code
It’s better than the following:
const createEvent = ( title = 'Untitled', timeStamp = Date.now(), description = '' ) => ({ title, description, timeStamp }); // later... Const birthdayParty = createEvent('Birthday Party', undefined, // Avoid the case 'Best Party ever! ');Copy the code
7. Use parallel structures
“… parallel construction requires that expressions of similar content and function should be outwardly similar. The Likeness of form enables the reader to recognize more readily the likeness of content and function.”
Parallel structure is a concept in grammar. In English, parallel structure refers to compound sentences that are similar in content, the same in structure, without sequence or causal relationship. Both design patterns and programming paradigms can be thought about and understood in this context. If there is repetition, there must be patterns. Parallel structures are very important for reading comprehension.
Most of the problems encountered in software development have been solved before, and if you find yourself doing the same thing over and over again, it’s time to stop and abstract: Find the same place and build one that makes it easy to add different layers of abstraction, which is essentially what many libraries and frameworks do.
Componentalization is a good example: 10 years ago it was common to write code that mixed interface updates, application logic, and data loading with jQuery, but then people realized that we could apply the MVC pattern to the client and started stripping the data layer from interface updates. Finally, we have componentization, which allows us to express the update logic and lifecycle of all components in exactly the same way, instead of writing a bunch of imperative code.
For those familiar with the concept of componentization, it’s easy to understand how components work: part of the code is responsible for declaring the interface, and part of the code is responsible for doing what we expect it to do throughout the component’s life cycle. When we use the same coding pattern on repeated problems, students familiar with the pattern quickly understand what the code is doing.
Bottom line: Code should be simple, not oversimplified
Vigorous writing is concise. A sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. This requires not that the writer make all sentences short, or avoid all detail and treat subjects only in outline, but that every word tell.
Concise code is powerful, it should not contain unnecessary variables, syntax structure, does not require programmers to write the code must be the shortest, or omit many details, but requires that every variable, function in the code can be clear, intuitive to convey our intention and ideas.
Code should be clean because clean code is easier to write (usually with less code), easier to read, and easier to maintain. Clean code is code that is harder to bug and easier to debug. Fixing bugs usually takes time and effort, and the process can lead to more bugs, which can affect the normal development schedule.
Those who think that writing familiar code is more readable code are wrong. Readable code must be concise and simple. Although ES6 has become the new standard as early as 2015, by 2017, There are still a lot of students who don’t use concise syntax like arrow functions, implicit return, rest, and spread operators. Getting familiar with the new syntax takes practice, time invested in learning and getting familiar with the new syntax and the ideas and techniques of function composition. Once you get familiar with the new syntax, you’ll find that the code could have been written this way.
Finally, the code should be concise, not oversimplified.
One More Thing
The author of this article is Wang Shijun. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source. If you found this article helpful, please give it a thumbs up! If you have any questions about the content of this article, please leave a comment. Want to know what I’ll write next? Please subscribe to my nuggets column or Zhihu column: Front End Weekly: Keeping you Up to date on the Front End.