Skip to content

Sérgio Kopplin

Clean Code - Introduction & Functions

Notebook, Clean Code4 min read

References


Quotes

Bjarne Stroustrup, inventor of C and author of The C++ Programming Language:

"I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete according to an articulated strategy, and performance close to optimal so as not to tempt people to make the code messy with unprincipled optimizations. Clean code does one thing well."

Grady Booch, author of Objected Oriented Analysis and Design with Applications:

"Clean code is simple and direct. Clean code reads like well-written prose. Clean code never obscures the designer’s intent but rather is full of crisp abstractions and straightforward lines of control."

"Big" Dave Thomas, founder of OTI, godfather of the Eclipse strategy:

"Clean code can be read, and enhanced by a developer other than its original author. It has unit and acceptance tests. It has meaningful names. It provides one way rather than many ways for doing one thing. It has minimal dependencies, which are explicitly defined, and provides a clear and minimal API. Code should be literate since depending on the language, not all necessary information can be expressed clearly in code alone."

Michael Feathers, author of Working Effectively with Legacy Code:

"I could list all of the qualities that I notice in clean code, but there is one overarching quality that leads to all of them. Clean code always looks like it was written by someone who cares. There is nothing obvious that you can do to make it better. All of those things were thought about by the code’s author, and if you try to imagine improvements, you’re led back to where you are, sitting in appreciation of the code someone left for you—code left by someone who cares deeply about the craft."


Functions

The first rule of functions is that, they should be small! The second is that they should be even smaller than that.

Blocks and Indenting: They should not have indention level greater than one or two. That way they will be easier to read and understand.

Do One Thing:

Functions should do one thing. They should do it well. They should do it only.


Single Responsability

They should not have sections, like declarations, initializations and sieve. If they do so, they're doing more than one thing. If you can extract part of the functions to another one, the function is not doing a single thing.

Bad:

1function emailClients(clients) {
2 clients.forEach((client) => {
3 const clientRecord = database.lookup(client);
4 if (clientRecord.isActive()) {
5 email(client);
6 }
7 });
8}

Good:

1function emailActiveClients(clients) {
2 clients.filter(isActiveClient).forEach(email);
3}
4
5function isActiveClient(client) {
6 const clientRecord = database.lookup(client);
7 return clientRecord.isActive();
8}

One Level of Abstraction per Function

  • Details should not mix with essential things inside functions. Different levels of abstraction leads to multiple concerns.

Bad:

1function parseBetterJSAlternative(code) {
2 const REGEXES = [
3 // ...
4 ];
5
6 const statements = code.split(" ");
7 const tokens = [];
8 REGEXES.forEach((REGEX) => {
9 statements.forEach((statement) => {
10 // ...
11 });
12 });
13
14 const ast = [];
15 tokens.forEach((token) => {
16 // lex...
17 });
18
19 ast.forEach((node) => {
20 // parse...
21 });
22}
1function tokenize(code) {
2 const REGEXES = [
3 // ...
4 ];
5
6 const statements = code.split(" ");
7 const tokens = [];
8 REGEXES.forEach((REGEX) => {
9 statements.forEach((statement) => {
10 tokens.push(/* ... */);
11 });
12 });
13
14 return tokens;
15}
16
17function lexer(tokens) {
18 const ast = [];
19 tokens.forEach((token) => {
20 ast.push(/* ... */);
21 });
22
23 return ast;
24}
25
26function parseBetterJSAlternative(code) {
27 const tokens = tokenize(code);
28 const ast = lexer(tokens);
29 ast.forEach((node) => {
30 // parse...
31 });
32}

Reading Code from Top to Bottom, "The Stepdown Rule"

  • We want code to be a top-down narrative. Every function should be followed by other ones from the next level of abstraction.

There's a technique for that. The "TO paragraphs" technique.

  • To include the setups and teardowns, we include setups, then we include the test page content, and then we include the teardowns.
  • To include the setups, we include the suite setup if this is a suite, then we include the regular setup.
  • To include the suite setup, we search the parent hierarchy for the “SuiteSetUp” page and add an include statement with the path of that page.
  • To search the parent…

It makes the level of abstraction consistent as we do code.


Switch Statements

Bad:

1const dogSwitch = (breed) => {
2 switch (breed) {
3 case "border":
4 return "Border Collies are good boys and girls.";
5 break;
6 case "pitbull":
7 return "Pit Bulls are good boys and girls.";
8 break;
9 case "german":
10 return "German Shepherds are good boys and girls.";
11 break;
12 }
13};
14dogSwitch("border"); // "Border Collies are good boys and girls."

Good:

1const dogSwitch = (breed) =>
2 ({
3 border: "Border Collies are good boys and girls.",
4 pitbull: "Pit Bulls are good boys and girls.",
5 german: "German Shepherds are good boys and girls.",
6 }[breed]);
7dogSwitch("border"); // "Border Collies are good boys and girls."

Reference: link.


Use Descriptive Names

Bad:

1function addToDate(date, month) {
2 // ...
3}
4
5const date = new Date();
6
7// É difícil dizer pelo nome da função o que é adicionado
8addToDate(date, 1);

Good:

1function addMonthToDate(month, date) {
2 // ...
3}
4
5const date = new Date();
6addMonthToDate(1, date);

Function Arguments

  • zero arguments -> niladic. Ideal.
  • one argument -> monadic. Good.
  • two arguments -> dyadic. Still ok.
  • three or more arguments -> polyadic. Not good. Should avoid.

Monadic cases

Try to explicit say what the function returns, either an argument transformation or as event. The naming should be clear in what the function does and what's the effect.

Bad:

1function insertFileIntoDocumentData("MyFile") {}; // not clear type

Good:

1function formatFile("MyFile") {}; // transformation
2function fileOpen("MyFile") {}; // event

Flag Arguments

Avoid functions that receives flags as arguments. For example, a boolean as a parameter. It will lead to more than one responsability. If this is the case, splitting the function into another ones is better.

Bad:

1function render(true) {} // different results depending on boolean

Good:

1function renderForSuite() {}
2function renderForSingleTest() {}

Dyadic cases

Avoid arguments that don't follows a natural ordering or natural cohesion.

Bad:

1function writeField(stream, name) {} // params without natural cohesion

Good:

1function outputStream(stream) {}
2function fieldWriter(name) {}

Triads

Avoid over thinking on params order. It will take hard work to always remember the params order. In Javascript you can use desestructuring, which can help you.

Bad:

1function createMenu(title, body, buttonText, cancellable) {}

Good:

1function createMenu({ title, body, buttonText, cancellable }) {}
2
3createMenu({
4 title: "Foo",
5 body: "Bar",
6 buttonText: "Baz",
7 cancellable: true,
8});

Have no Side Effects - Part 1

A function produces a side effect if it does anything other than take a value in and return another value or values. A side effect could be writing to a file, modifying some global variable, or accidentally wiring all your money to a stranger.

Bad

1// Global variable referenced by following function.
2// If we had another function that used this
3// name, now it'd be an array and it could break it.
4let name = "Ryan McDermott";
5
6function splitIntoFirstAndLastName() {
7 name = name.split(" ");
8}
9
10splitIntoFirstAndLastName();
11
12console.log(name); // ['Ryan', 'McDermott'];

Good

1function splitIntoFirstAndLastName(name) {
2 return name.split(" ");
3}
4
5const name = "Ryan McDermott";
6const newName = splitIntoFirstAndLastName(name);
7
8console.log(name); // 'Ryan McDermott';
9console.log(newName); // ['Ryan', 'McDermott'];

Have no Side Effects - Part 2

In JavaScript, primitives are passed by value and objects/arrays are passed by reference. In the case of objects and arrays, if your function makes a change in a shopping cart array, for example, by adding an item to purchase, then any other function that uses that cart array will be affected by this addition. That may be great, however it can be bad too.

Bad:

1const addItemToCart = (cart, item) => {
2 cart.push({ item, date: Date.now() });
3};

Good:

1const addItemToCart = (cart, item) => {
2 return [...cart, { item, date: Date.now() }];
3};

Set default objects with Object.assign

Bad:

1const menuConfig = {
2 title: null,
3 body: "Bar",
4 buttonText: null,
5 cancellable: true,
6};
7
8function createMenu(config) {
9 config.title = config.title || "Foo";
10 config.body = config.body || "Bar";
11 config.buttonText = config.buttonText || "Baz";
12 config.cancellable =
13 config.cancellable !== undefined ? config.cancellable : true;
14}
15
16createMenu(menuConfig);

Good:

1const menuConfig = {
2 title: "Order",
3 // User did not include 'body' key
4 buttonText: "Send",
5 cancellable: true,
6};
7
8function createMenu(config) {
9 config = Object.assign(
10 {
11 title: "Foo",
12 body: "Bar",
13 buttonText: "Baz",
14 cancellable: true,
15 },
16 config
17 );
18
19 // config now equals:
20 // {
21 // title: "Order",
22 // body: "Bar",
23 // buttonText: "Send",
24 // cancellable: true
25 // }
26 // ...
27}
28
29createMenu(menuConfig);

Prefer Exceptions to Returning Error Codes

When you return exceptions instead of returning codes, the error processing code can be separated from the happy path.

Bad:

1if (deletePage(page) == E_OK) {
2 if (registry.deleteReference(page.name) == E_OK) {
3 if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
4 logger.log("page deleted");
5 } else {
6 logger.log("configKey not deleted");
7 }
8 } else {
9 logger.log("deleteReference from registry failed");
10 }
11} else {
12 logger.log("delete failed");
13 return "";
14}

Good:

1try {
2 deletePage(page);
3 registry.deleteReference(page.name);
4 configKeys.deleteKey(page.name.makeKey());
5} catch (error) {
6 logger.log(error);
7}

Don't write to global functions

Polluting globals is a bad practice in JavaScript because you could clash with another library and the user of your API would be none-the-wiser until they get an exception in production.

Bad:

1Array.prototype.diff = function diff(comparisonArray) {
2 const hash = new Set(comparisonArray);
3 return this.filter(elem => !hash.has(elem));
4};

Good:

1class SuperArray extends Array {
2 diff(comparisonArray) {
3 const hash = new Set(comparisonArray);
4 return this.filter(elem => !hash.has(elem));
5 }
6}

Favor functional programming over imperative programming

JavaScript isn't a functional language in the way that Haskell is, but it has a functional flavor to it. Functional languages can be cleaner and easier to test. Favor this style of programming when you can.

Bad:

1const programmerOutput = [
2 {
3 name: "Uncle Bobby",
4 linesOfCode: 500
5 },
6 {
7 name: "Suzie Q",
8 linesOfCode: 1500
9 },
10 {
11 name: "Jimmy Gosling",
12 linesOfCode: 150
13 },
14 {
15 name: "Gracie Hopper",
16 linesOfCode: 1000
17 }
18];
19
20let totalOutput = 0;
21
22for (let i = 0; i < programmerOutput.length; i++) {
23 totalOutput += programmerOutput[i].linesOfCode;
24}

Good:

1const programmerOutput = [
2 {
3 name: "Uncle Bobby",
4 linesOfCode: 500
5 },
6 {
7 name: "Suzie Q",
8 linesOfCode: 1500
9 },
10 {
11 name: "Jimmy Gosling",
12 linesOfCode: 150
13 },
14 {
15 name: "Gracie Hopper",
16 linesOfCode: 1000
17 }
18];
19
20const totalOutput = programmerOutput.reduce(
21 (totalLines, output) => totalLines + output.linesOfCode,
22 0
23);

Encapsulate conditionals

Bad:

1if (fsm.state === "fetching" && isEmpty(listNode)) {
2 // ...
3}

Good:

1function shouldShowSpinner(fsm, listNode) {
2 return fsm.state === "fetching" && isEmpty(listNode);
3}
4
5if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
6 // ...
7}

Avoid negative conditionals

Bad:

1function isDOMNodeNotPresent(node) {
2 // ...
3}
4
5if (!isDOMNodeNotPresent(node)) {
6 // ...
7}

Good:

1function isDOMNodePresent(node) {
2 // ...
3}
4
5if (isDOMNodePresent(node)) {
6 // ...
7}

Avoid conditionals

"How am I supposed to do anything without an if statement?" The answer is that you can use polymorphism to achieve the same task in many cases.

Bad:

1class Airplane {
2 // ...
3 getCruisingAltitude() {
4 switch (this.type) {
5 case "777":
6 return this.getMaxAltitude() - this.getPassengerCount();
7 case "Air Force One":
8 return this.getMaxAltitude();
9 case "Cessna":
10 return this.getMaxAltitude() - this.getFuelExpenditure();
11 }
12 }
13}

Good:

1class Airplane {
2 // ...
3}
4
5class Boeing777 extends Airplane {
6 // ...
7 getCruisingAltitude() {
8 return this.getMaxAltitude() - this.getPassengerCount();
9 }
10}
11
12class AirForceOne extends Airplane {
13 // ...
14 getCruisingAltitude() {
15 return this.getMaxAltitude();
16 }
17}
18
19class Cessna extends Airplane {
20 // ...
21 getCruisingAltitude() {
22 return this.getMaxAltitude() - this.getFuelExpenditure();
23 }
24}
© 2020 by Sérgio Kopplin.