Example of Clean Code Implementation with Javascript

Handoko Darmawan
Life at Telkomsel
Published in
12 min readDec 8, 2023

Based on Book by Robert C. Martin ( Clean Code: A Handbook of Agile Software Craftsmanship )

Photo by PAN XIAOZHEN on Unsplash

In this article, we will explain examples of implementing clean code principles, as explained by Robert C. Martin in the book Clean Code: A Handbook of Agile Software Craftsmanship in chapter 17 (Smell & Heuristic). The programming language used as an example in this article is JavaScript. Hopefully this article is useful for all readers.

Thanks in advance for reading this article..

G1: Multiple Languages ​​in One Source File

Minimizes the use of various languages ​​in one source file. Avoid writing code like image G1–1 below, where the HTML text is mixed into an object. We recommend using a method like Figure G1–2 where the HTML text is stored in a separate template file that can be accessed via the mailOptions.template object.

Figure G1–1
Figure G1–2

G2: Obvious Behavior Is Unimplemented

Each function or class should implement the properties/behavior expected by other programmers.

In figure G2–1, the __getCodePartner function should only return the value of codePartner, but inside it also calls the sendNotification function. And implementations like this should be avoided. It’s best to use function names that explicitly state what the function does, as in the example in figure G2–2.

Figure G2–1
Figure G2–2

G3: Incorrect Behavior at the Boundaries

Perform tests for each boundary condition. For example, in the code in Figure G3–1, it must be ensured, created and run a unit test that meets all the required conditions (version variations 11.0.0, 10.0.0, 11.0.1…; page variations 10, 11, 9… )

Figure G3–1

G4: Overridden Safeties

Don’t take risks by ignoring warnings, skipping failed tests, etc. As in the capture in Figure G4–1, make sure the npm audit is passed and there are no vulnerabilities in the application created. Also applies to unit tests, make sure the coverage is met.

Figure G4–1

G5: Duplication

Follow the rule: DRY (Do Not Repeat Yourself) and avoid duplication. To avoid this duplication, we can use tools such as sonarqube (as in image capture G5–1) to help identify duplicate code and immediately fix it if a code smell is found.

Figure G5–1

G6: Code at Wrong Level of Abstraction

Functions should not mix different levels of abstraction. The processData function in Figure G6–1 mixes high-level and low-level operations, namely calculating the result and then checking the result.

Figure G6–1

This function can be refactored to be like in Figure G6–2 where the low-level and high-level functions are made separate, making it easier to understand and maintain.

Figure G6–2

G7: Base Classes Depending on Their Derivatives

The title of sub-chapter G7 is quite self-explanatory.

Figure G7–1

The Shape class in figure G7–1 above has a conditional to determine implementation based on the object type.

Refactor the code above so that it complies and does not depend on derived classes as follows:

Figure G7–2

G8: Too Much Information

Design a module that exposes the minimum possible information needed to carry out its responsibilities. And must hide details for other modules.

Figure G8–1

In Figure G8–1 above, the __utilityFunction() module should not be exposed. The code above can be reacted like Figure G8–2, where the utility function is only used in a single module, where the public function encapsulates the utility function in the module itself and only uses it internally.

Figure G8–2

G9: Dead Code

Dead code is code that is never executed and should be deleted. The dead code in figure G9–1 below is in an if-else block which checks the total for more than 1000 or less than 0, where the evaluation will never be true because the total price calculation always produces a non-negative value. So the code can be refactored like figure G9–2.

Figure G9–1
Figure G9–2

G10: Vertical Separation

Local variables are declared before their first use and private functions are defined after their first use. As shown in Figure 10–1, the payload variable is defined before its use in the ShopService.sendMail function, and the __getShopCode() function is defined under the sendToMail method.

Figure G10–1

G11: Inconsistency

Consistency in naming functions and variables. As can be seen in the two images below, better consistency in function naming is in Figure G11–2, where the sendShopMail and ShopService.sendShopMail functions are consistent in their naming.

Figure G11–1
Figure G11–2

G12: Clutter

Delete unused variables/functions, meaningless comments, which pollute the source code. As in Figure G12–1, the Jwt variable is declared but not used, so it’s best to just delete it.

Figure G12–1

G13: Artificial Coupling

Avoid coupling between modules that do not have a direct relationship. In Figure G13–1, the variable DB_USER_PASSWORD is embedded in the User class. This is not good, because it will create unnecessary coupling to the user.js module. We recommend that the code be refactored as in Figure G13–2

Figure G13–1
Figure G13–2

G14: Feature Envy

The methods of a class only use variables and functions defined in the class, not from other classes. As exemplified in Figure G14–2.

Figure G14–1
Figure G14–2

G15: Selector Arguments

Don’t use booleans as arguments for a function because it will make it difficult for developers to understand, maintain and test the code. As exemplified in Figure 15–2 which is a refactor to the code in Figure 15–1.

Figure 15–1
Figure 15–2

G16: Obscured Intent

Avoid using notations that will make it difficult for other developers to read the code. In Figure 16–1, many of the variable names are abbreviated, making it difficult to understand the code. Refactor the code so that it looks like Figure 16–2 so that it is easy to understand.

Figure 16–1
Figure 16–2

G17: Misplaced Responsibility

Avoid functions or methods that are more responsible than necessary. For the example in Figure 17–1, the saveUser function should only save the user, and not decide which database the user should be saved to. So the code can be refactored to look like Figure 17–2.

Figure 17–1
Figure 17–2

G18: Inappropriate Static

Do not make functions/methods static so that in the future there is the possibility of adding an implementation to that function. An example of an inappropriate static method is in Figure 18–1. Refactor the code, so that the ‘makesound()’ method should be nonstatic as in Figure 18–2.

Figure 18–1
Figure 18–2

G19: Use Explanatory Variables

To make the program more readable, it is best to carry out the calculation process in stages by naming variables meaningfully to accommodate intermediate values. The example code in Figure 19–1 does not use intermediate variables, so it is more difficult to understand than the code in Figure 19–2.

Figure 19–1
Figure 19–2

G20: Function Names Should Say What They Do

Self-explanatory principles. As in Figure 20–2, the function name executeQuery is more descriptive of what the function does than the name of the query function in Figure 20–1.

Figure 20–1
Figure 20–2

G21: Understand the Algorithm

Make sure to understand the algorithm created. The function in Figure 21–1 assumes that its input argument is an array consisting of at least one element. If the array is empty it will error. So validation must be made for the input array as in the example in Figure 21–2.

Figure 21–1
Figure 21–2

G22: Make Logical Dependencies Physical

If a module has a dependency on another module, then the module must not make assumptions about the implementation of the module on which it is dependent.

In Figure 22–1, module B assumes that module A always returns an array with four elements and always in ascending order. This cannot be guaranteed because there could be a change in the implementation of ‘sort’ in module A, and the assumptions made by module B will be wrong.

So module B should be refactored to look like Figure 22–2.

Figure 22–1
Figure 22–2

G23: Prefer Polymorphism to If/Else or Switch/Case

Prioritize the use of polymorphism over If/Else or Switch/Case.

In the code in Figure 23–1, if another payment method is added, you have to change the processPayment() method which adds complexity and has the potential to cause bugs and violates the open-close principle.

We recommend that the code be refactored to look like Figure 23–2.

Figure 23–1
Figure 23–2

G24: Follow Standard Conventions

In a team, it is best to define the rules that must be followed by each member in carrying out coding standards, for example regarding how to declare variables, naming modules, naming functions, etc. The code in Figure 24–1 does not comply with standard conventions. Conventionally in JavaScript, naming for internal methods that are only accessed within the module begins with two underscores ‘__’ followed by the method name in camelCase, while public modules are directly the name of the method that uses camelCase. Figure 24–2 complies with standard conventions in JavaScript.

Figure 24–1
Figure 24–2

G25: Replace Magic Numbers with Named Constants

Each constant must be wrapped in a constant, with the constant writing convention using all-uppercase and if the name is more than one word, it is separated with an underscore

Figure 25–1
Figure 25–2

G26: Be Precise

Make sure that the code you create doesn’t just run, but the developer must ensure precisely that the decisions implemented in the coding must take into account exceptions that may occur. Don’t be lazy to be precise in implementing decisions in coding.

One example in Be Precise is implementing locks or transaction management to avoid race conditions caused by concurrent update processes. In Figure 26–1 transaction management is not implemented, it should be refactored like the code in Figure 26–2.

Figure 26–1
Figure 26–2

G27: Structure over Convention

Emphasize design decisions with structure rather than convention.

An example is that using a base class structure is better than switch/case which is implemented with standard/good conventions. Where the use of a base class structure guarantees that the concrete class has an implementation of all abstract method

Figure 27–1

In the updateEnemy function above, if a new enemy type is added then the switch/case in the function must be changed.

A better approach is to use a base class with abstract methods like the example below:

Figure 27–2

G28: Encapsulate Conditionals

Boolean logic will be difficult to understand if it is inside an if or while statement. To make it more readable, you need to extract a function that explains the meaning of the conditional.

Figure 28–1

In the function above it will be difficult to understand all the boolean flags in the if condition. It would be better if the boolean flags were made into an extract function checkOfferForShopCategory to explain the meaning as in the following code

Figure 28–2

G29: Avoid Negative Conditionals

It is more difficult to understand a negative conditional, so whenever possible always apply a positive conditional

Figure 29–1

The conditions above are more recommended than the conditions below

Figure 29–2

G30: Functions Should Do One Thing

A function should only do one thing (single responsibility principle)

Figure 30–1

In the code in Figure 30–1, the calculateOrderTotal function has 3 responsibilities, namely: Calculate subtotal order, Apply discounts and Add shipping charges. We recommend that the code be refactored as follows:

Figure 30–2

G31: Hidden Temporal Couplings

Temporal coupling refers to dependencies between functions that must be called sequentially. Avoid hiding temporal coupling, including by using a builder pattern where methods are called step by step, and chained to create a fluent API like the code example in Figure 31–1.

Figure 31–1

G32: Don’t Be Arbitrary

Don’t force a function/method that should be separated so that the separation of responsibilities is unclear

Figure 32–1

The calculateDiscount function in Figure 32–1 has an arbitrary structure and does not communicate the logic of the function clearly. This function should be refactored into several smaller functions so that they are easier to understand and maintain as in Figure 32–2

Figure 32–2

G33: Encapsulate Boundary Conditions

Boundary conditions should be encapsulated with other modules/functions

Figure 33–1

The condition above should be refactored to be as follows so that it is easy to understand

Figure 33–2

G34: Functions Should Descend Only One Level of Abstraction

Functions should not mix different levels of abstraction.

Figure 34–1

The processData function mixes high-level and low-level operations, namely calculating the result and then checking the result.

This function can be refactored to be as below where the low-level and high-level functions are made separate, making it easier to understand and maintain

Figure 34–2

G35: Keep Configurable Data at High Levels

Constant data such as configuration and default data must be declared at a high level of abstraction, not created in a low-level function.

As in the example below, the dotenv.config() configuration declaration during initialization of an application makes the configuration values ​​accessible to other parts of the application, including low-level functions.

Figure 35–1

G36: Avoid Transitive Navigation

A module should not need to know about the internal processes of its collaborators. A module should only depend on the objects it needs to complete its task.

Figure 36–1

The getPostsByUser function is responsible for the process of fetching posts from a user, but must carry out the fetch user process first. This creates a transitive dependency between User and Post.

This function can be refactored as shown in Figures 36–2 and 36–3 below.

Figure 36–2
Figure 36–3

--

--