Best practices in programming: Clean code for you and your team π
Discover coding best practices! Learn how to write readable, maintainable and clean code that is not only understandable for you, but also for your team. π
In software development, it is crucial to write code that not only works, but is also well-structured, readable and maintainable. This applies not only to team collaboration, but also in the event that you come back to your own code months later. In this article, I will introduce you to the best practices and principles you should follow when programming.
Using JavaScript examples, I'll show you how to turn bad code into readable code and the benefits of functional programming.
I also go into the most important soft skills that are essential for a developer. Programming is a craft and just as much love should be put into the code β€οΈ
There is a very good book on this topic by Robert C. Martin:
Why good code is important π€
Readability
Readable code allows you and other developers to quickly understand and use the code. When you look at your own code again after a few months, you don't want to have to think for hours about what this or that part of the code does. Readable code saves time and nerves. Readable code also helps to find and fix bugs faster, as the logic and structure are clearly visible.
Maintainability
Maintainable code is crucial for fixing bugs and adding new features. Unstructured and unclear code makes maintenance more difficult and increases the likelihood of bugs being introduced when new features are added. Maintainable code should be modular so that changes in one module do not have unforeseen effects on other parts of the code.
Collaboration
In most projects, you are not working alone. Clear and well-structured code facilitates team collaboration. Other developers can read, understand and further develop your code. A uniform code base with consistent naming conventions and formatting makes it easier to familiarize new team members and promotes a productive working environment.
Reusability
Well-written code is often reusable. By applying principles such as DRY (Don't Repeat Yourself) and KISS (Keep It Simple, Stupid), you can ensure that your code blocks can be reused in different parts of your project or even in other projects.
Code for less experienced developers
A good developer writes code in such a way that it is also understandable for developers with a lower level of knowledge. This means that the code is well documented, clearly structured and free of unnecessary complexity.
Principles of good code π οΈ
Clarity before cleverness
It is tempting to write clever and complicated code that looks impressive at first glance. But clarity should always take precedence. Simple and clear code is often the better way to go. Code that is easy to understand makes debugging and further development easier.
// Clever, but possibly difficult to understand
const isValid = (str) => !/[^\w]/.test(str);
// Clear and understandable
const isValid = (str) => {
const regex = /[^\w]/;
return !regex.test(str);
};
Always put yourself in the shoes of another programmer and ask yourself three questions:
- Would a beginner understand my code?
- Will I still understand my code in 6 months?
- Can I train someone in the code without having to train them?
If you can answer one of these questions with no, then your code is probably too complicated.
Consistency
A consistent style makes the code easier to read. Use consistent naming conventions, indentation and formatting. This makes it easier to understand and maintain the code. Tools such as ESLint or Prettier can help to format the code automatically and keep it consistent.
// Inconsistent
const userName = "John";
const UserAge = 30;
// Consistent
const userName = "John";
const userAge = 30;
DRY (Don't Repeat Yourself) π
Avoid redundancies in code. Repeated code should be outsourced to functions or classes. This reduces errors and makes changes easier. If there is a change in the code, it only needs to be made in one place.
// Redundant code
function getUserName(user) {
return user.firstName + ' ' + user.lastName;
}
function getUserAddress(user) {
return user.street + ', ' + user.city;
}
// DRY principle applied
function getFullName(user) {
return `${user.firstName} ${user.lastName}`;
}
function getAddress(user) {
return `${user.street}, ${user.city}`;
}
KISS (Keep It Simple, Stupid) π€―
Keep your code as simple as possible. Complexity increases the susceptibility to errors and makes it more difficult to understand. Simple code is often more robust and efficient code. Use simple and clear logic instead of complex and nested structures.
// Complex and difficult to understand
function getDiscount(price, isMember) {
return isMember ? (price > 100 ? price * 0.9 : price * 0.95) : price;
}
// Simple and clear
function getDiscount(price, isMember) {
if (isMember) {
if (price > 100) {
return price * 0.9;
} else {
return price * 0.95;
}
}
return price;
}
YAGNI (You Aren't Gonna Need It) β
Implement only the functionalities that you currently need. Superfluous features increase complexity and maintenance costs. This principle helps to keep the code lean and focused.
// Overengineering
function calculatePrice(price, tax, discount, isMember) {
let finalPrice = price + (price * tax);
if (isMember) {
finalPrice -= discount;
}
return finalPrice;
}
// YAGNI principle applied
function calculatePrice(price, tax) {
return price + (price * tax);
}
Code is like a good book π
The correct naming of variables, functions and classes is essential for the readability and comprehensibility of the code, similar to a well-written book. Clear and precise names reflect the intention of the code block and facilitate maintenance and team collaboration. For example, a function that calculates the square of a number should be called calculateSquare
, not doSomething
.
A class for calculating squares could be called SquareCalculator
. Well-named and structured code saves time and nerves and makes working in a team more pleasant and productive. By writing your code in such a way that it is understandable even for less experienced developers, you avoid technical debt and improve the quality of your project in the long term.
Names often follow the rules of language, similar to adjectives, nouns and verbs, which makes them more intuitive and easier to understand.
Adjectives
Adjectives describe properties or states. In programming, they are often used to name variables that store certain states or properties.
let isActive = true;
let userName = "JohnDoe";
In this example, isActive
describes the state of an object (whether it is active or not) and userName
stores a property of a user.
Verbs
Verbs describe actions or processes. In programming, verbs are often used to name functions that perform certain actions.
function calculateArea(width, height) {
return width * height;
}
function fetchData(url) {
// Retrieve data from a URL
}
Here, calculateArea
and fetchData
describe the actions that the respective functions perform (calculating an area and retrieving data respectively).
Nouns
Nouns denote people, places or things. In programming, nouns are often used to name classes and objects that represent certain entities or concepts.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class Product {
constructor(id, price) {
this.id = id;
this.price = price;
}
}
In this example, User
and Product
denote concrete entities in a system, similar to nouns in language.
Adverbs
Adverbs modify verbs and describe how an action is performed. In programming, adverbs can help to specify function names and clarify how an action is performed.
function calculateAreaQuickly(width, height) {
// Quickly calculate the area
return width * height;
}
function fetchDataSafely(url) {
// Fetch data safely from a URL
}
Here, quickly
and safely
modify the actions and provide additional information about how the actions should be performed.
Bad vs. good code in JavaScript π»
Example of bad code
function processData(input) {
let output = [];
for (let i = 0; i < input.length; i++) {
let processedData = input[i] * 2;
output.push(processedData);
}
console.log(output);
return output;
}
Example of good code
function processData(input) {
return input.map(item => item * 2);
}
const input = [1, 2, 3, 4, 5];
const output = processData(input);
console.log(output); // [2, 4, 6, 8, 10]
In this example, the code has been simplified by using Array.prototype.map
, which increases readability and maintainability.
Technical debt π¦
Technical debt occurs when short-term solutions or compromises are made in programming to achieve quick results instead of implementing long-term, sustainable solutions. This can be caused by time pressure, lack of resources or lack of planning. Like financial debt, technical debt must also be "paid back", which takes the form of additional work for maintenance and refactoring. Technical debt can affect code quality, slow down development and make it difficult to introduce new features. Therefore, it is important to make conscious decisions and, where possible, favor long-term and clean solutions to avoid accumulating technical debt.
Hardcoded values
Bad solution
function calculateTotalPrice(quantity) {
return quantity * 9.99;
}
In this example, the price of the product is hardcoded in the code. This is a quick solution, but it leads to technical debt because any change to the price requires a change in the code.
Sustainable solution
const PRODUCT_PRICE = 9.99;
function calculateTotalPrice(quantity) {
return quantity * PRODUCT_PRICE;
}
By using a constant for the product price, the code becomes more flexible and easier to maintain. Changes to the price only need to be made in one place, which reduces technical debt.
Duplicated code
Technical debt
function calculateRectangleArea(width, height) {
return width * height;
}
function calculateTriangleArea(base, height) {
return (base * height) / 2;
}
There is duplicated code here that contains the calculation of the area. This repetition leads to technical debt, as changes to the logic have to be made in several places.
function calculateArea(shape, ...dimensions) {
switch (shape) {
case 'rectangle':
return dimensions[0] * dimensions[1];
case 'triangle':
return (dimensions[0] * dimensions[1]) / 2;
default:
throw new Error('Unknown shape');
}
}
By merging the calculation logic into a single function, the code becomes DRY (Don't Repeat Yourself). This reduces technical debt and makes the code easier to maintain and extend.
Disadvantages of kilometer-long one-liners π«
Kilometer-long one-liners can be difficult to read and debug. They tend to become complex and opaque, making maintenance difficult.
Example of a bad one-liner
const result = array.map(x => x * 2).filter(x => x > 10).reduce((acc, x) => acc + x, 0);
Split-and-readable-code
const doubled = array.map(x => x * 2);
const filtered = doubled.filter(x => x > 10);
const result = filtered.reduce((acc, x) => acc + x, 0);
By splitting the code into multiple lines, it becomes much more readable and easier to debug.
Functional programming π§βπ»
Functional programming can help to write clean and efficient code. It encourages the use of immutable data and pure functions.
Functional programming in JavaScript
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
const even = doubled.filter(num => num % 2 === 0);
console.log(even); // [4, 8]
In this example, the original data is not changed and the operations are clear and comprehensible.
Advantages of functional programming
Invariability
Invariability means that data is no longer changed after it has been created. Instead, new data structures are created. This reduces errors and makes the code more predictable.
Pure functions
Pure functions are functions that have no side effects and always return the same result for the same input. This makes the code easier to test and debug. The expected mode of operation is deterministic.
Negative example β
Here the value of the global variable counter
is changed, which does not make the function pure, as the state outside the function is affected and the result differs for the same calls.
let counter = 0;
function incrementCounter() {
// This function increases the value of counter and returns the new value
counter++;
return counter;
}
// Testing the function with side effect
console.log(incrementCounter()); // Output: 1
console.log(incrementCounter()); // Output: 2 (not the same result with the same calls)
Positive example β
function add(a, b) {
// This function returns the sum of a and b
return a + b;
}
// Testing the pure function
console.log(add(2, 3)); // Output: 5
console.log(add(2, 3)); // Output: 5 (always the same result)
Higher-order functions
Higher-order functions are functions that take other functions as arguments or return functions. This enables flexible and reusable code blocks.
const add = (a) => (b) => a + b;
const add5 = add(5);
console.log(add5(10)); // 15
Soft skills for developers π€
Communication
Good communication is essential. You need to be able to convey your ideas and solutions clearly and precisely. This applies to working with other developers as well as communicating with non-technical people.
Teamwork
The ability to work effectively as part of a team is crucial. This includes sharing knowledge, supporting colleagues and solving problems together. Teamwork also encourages development and learning within the team.
Problem solving
Problem solving skills are central to the work of a developer. You need to be able to analyze complex problems, break them down and find effective solutions. A structured approach to problem solving helps you work more efficiently.
Adaptability
Technology is constantly evolving. As a developer, you must be willing to learn new technologies and methods and adapt to change. Adaptability allows you to respond flexibly to new challenges.
Code-Reviews and pair programming π§βπ€βπ§
Code reviews
Code reviews are an important part of software development. They make it possible to detect errors at an early stage and improve the code. Regular code reviews increase code quality and knowledge is shared within the team.
Pair programming
Pair programming is a technique in which two developers work together on one computer. One writes the code while the other reviews it. This method promotes knowledge sharing and collaboration within the team.
Test-Driven Development (TDD) π§ͺ
Advantages of TDD
Test-Driven Development (TDD) is a method in which tests are written before the actual code is developed. This helps to define clear requirements and ensure that the code delivers the expected results.
Example of TDD in JavaScript
//write test
const assert = require('assert');
function add(a, b) {
return a + b;
}
assert.strictEqual(add(1, 2), 3);
assert.strictEqual(add(-1, 1), 0);
// Implement code
function add(a, b) {
return a + b;
}
TDD leads to more robust and less error-prone code, as every functionality is covered by tests.
More-helpful-principles-and-techniques
Modularity
Modularity means dividing the code into small, independent modules. Each module should have a clearly defined task. This makes it easier to test, maintain and reuse code.
// Unmodular code
function processData(data) {
// Data validation
if (!Array.isArray(data)) {
throw new Error('Invalid data');
}
// Data processing
const processed = data.map(item => item * 2);
// Data output
console.log(processed);
return processed;
}
// Modular code
function validateData(data) {
if (!Array.isArray(data)) {
throw new Error('Invalid data');
}
}
function processData(data) {
return data.map(item => item * 2);
}
function logData(data) {
console.log(data);
}
const inputData = [1, 2, 3, 4, 5];
validateData(inputData);
const processedData = processData(inputData);
logData(processedData);
Automated testing
Automated testing is essential to ensure that the code works correctly and that new changes do not break existing functionalities. Unit tests, integration tests and end-to-end tests are common types of tests.
const { expect } = require('chai');
// Function for testing
function add(a, b) {
return a + b;
}
// unit test
describe('add', () => {
it('should return the sum of two numbers', () => {
expect(add(1, 2)).to.equal(3);
expect(add(-1, 1)).to.equal(0);
});
});
Documentation
Good documentation is crucial to help other developers (and your future self) understand the code. This includes both comments in the code (e.g. for JavaScript JSDoc) and external documentation such as README files.
/**
* Adds two numbers.
*
* @param {number} a - The first number.
* @param {number} b - The second number.
* @return {number} The sum of the two numbers.
*/
function add(a, b) {
return a + b;
}
Conclusion π
Good code is clear, readable and maintainable. It enables efficient collaboration and facilitates the maintenance and further development of projects.
You can significantly improve the quality of your code by applying best practices and principles such as clarity over cleverness, consistency and DRY. Functional programming and the development of important soft skills will also help you become a successful developer.
Remember that you are not only writing code for yourself, but also for other developers and for your future self.