Guidelines
Paradigms and Principles

Paradigms and Principles

Prefer functional over object-oriented paradigm

Functional programming is a style of coding that emphasizes the use of pure functions and avoids shared state and side effects. This can make functional code easier to understand, more concise, more resilient to change, and potentially more performant than object-oriented code.

In functional programming, the focus is on specifying the desired result and letting the runtime system determine the sequence of actions needed to achieve it, rather than specifying the steps that should be taken. This can make functional code more declarative and easier to read and understand.

In addition, because functional code is typically divided into small, independent units (functions) that do not depend on shared state, it can be easier to modify or extend functional code without introducing unintended side effects or breaking existing functionality.

However, it is important to choose the right paradigm for the problem at hand, rather than trying to force a particular paradigm onto every problem.

const add = (x, y) => x + y;
 
const subtract = (x, y) => x - y;
 
const multiply = (x, y) => x * y;
 
const divide = (x, y) => x / y;

Prefer declarative over imperative paradigm

Declarative programming is a style of coding that describes what to do instead of how to do it. Making code easier to read and understand. It is often shorter and more straightforward than imperative code, which tells the computer each step to take. Declarative code is easier to write, debug, and change, and is generally a better choice because it is simpler and easier to work with.

const numbers = [1, 2, 3, 4, 5];
 
const doubleNumbers = numbers.map((number) => number * 2);
 
console.log(doubleNumbers); // [2, 4, 6, 8, 10]

Avoid side effects

A side effect is any change that the function makes to the state of the program or to external variables or resources that persist beyond the lifetime of the function call.

There are several types of side effects that can occur in a program:

  • Mutation: Changing the value of a variable, object, or data structure in place, rather than creating a new one.
  • Input/output (I/O): Reading from or writing to external sources, such as files, databases, or the network.
  • Exceptions: Throwing or handling exceptions, which are used to signal and handle errors or abnormal conditions.
  • Function calls: Invoking functions or methods that have side effects, such as those that perform I/O, throw exceptions, or mutate variables.
  • Global state: Accessing or modifying global variables or state that is shared across multiple parts of a program.
  • Time and date: Accessing or modifying the current time or date, or scheduling events to occur at a later time.

Here are some examples of each type of side effects in JavaScript:

const person = {
  name: "John",
  age: 30,
  address: {
    street: "123 Main St",
    city: "New York",
    state: "NY",
  },
};
 
const updateAddress = (person, newAddress) => {
  person.address = newAddress;
};
 
updateAddress(person, {
  street: "456 Main St",
  city: "New York",
  state: "NY",
});
 
console.log(person.address); // { street: '456 Main St', city: 'New York', state: 'NY' }

Side effects can make a function more difficult to understand and test, because the function's behavior is not solely determined by its input parameters. It is often preferable to design functions to be pure, meaning that they do not have any side effects and their output is solely determined by their input parameters. This can make it easier to reason about the function's behavior and to write test cases for it.

However, it is generally not possible to completely eliminate side effects from a real-life program. Most programs need to interact with the outside world in some way, and this often involves side effects such as reading or writing to a file, making a network request, or updating a database record.

If you need to use a side effect in a function, there are several steps you can take to help manage the side effect and make your code easier to understand and maintain:

  • Document the side effect: Make sure to clearly document the side effect in the function's documentation or comments. This will help other developers understand the function's behavior and how it interacts with the outside world.
  • Test the side effect: Make sure to write test cases that cover the function's behavior with the side effect. This will help ensure that the function is behaving correctly and will help you catch any issues early on.
  • Isolate the side effect: Try to isolate the side effect as much as possible to make it easier to understand and test. For example, you might define a separate function that handles the side effect, and then call that function from your main function.
  • Consider alternatives to side effects: In some cases, it may be possible to redesign the function to avoid the side effect altogether. This can make the function easier to understand and test, and can help reduce the risk of bugs.

It is important to keep in mind that side effects can make a function more difficult to understand and test, and can increase the risk of bugs. It is generally a good idea to minimize side effects in your code as much as possible. However, in some cases it may be necessary or desirable to use side effects in a function. It is important to consider the trade-offs and to use them appropriately.

Use KISS principle

The acronym "KISS" stands for "Keep It Simple, Stupid" or "Keep It Short and Simple." The KISS principle advises developers to keep their designs simple and straightforward, rather than unnecessarily complex.

Use DRY principle

The acronym "DRY" stands for "Don't Repeat Yourself." The DRY principle advises developers to avoid repeating the same code or information multiple times, and instead to aim for a single, unified representation.

Use YAGNI principle

The acronym "YAGNI" stands for "You Ain't Gonna Need It." The YAGNI principle advises developers to focus on building only the features and functionality that are needed to meet the current requirements of a project, rather than adding unnecessary or speculative features.

Use SOLID principles

The SOLID principles are a set of design guidelines that were developed by Robert C. Martin to help developers design object-oriented software that is more maintainable, scalable, and flexible. That being said, these principles can be a useful guide for designing software in any paradigm.

The acronym "SOLID" stands for the following five principles:

Single Responsibility Principle (SRP): This principle states that a class or module should have only one reason to change, and that it should have a single, well-defined responsibility.

// 🚫 BAD: This class violates the SRP because it has two responsibilities:
// 1. Handling authentication
// 2. Fetching data from a database
class AuthenticatedDataFetcher {
  async fetchData(username, password) {
    if (this.authenticate(username, password)) {
      return this.fetchDataFromDB();
    }
  }
 
  authenticate(username, password) {
    // ...
  }
 
  fetchDataFromDB() {
    // ...
  }
}
 
// ✅ GOOD: These two classes have a single responsibility each:
// 1. Handling authentication
// 2. Fetching data from a database
class Authenticator {
  authenticate(username, password) {
    // ...
  }
}
 
class DataFetcher {
  async fetchData(username, password) {
    if (Authenticator.authenticate(username, password)) {
      return this.fetchDataFromDB();
    }
  }
 
  fetchDataFromDB() {
    // ...
  }
}

Prefer composition over inheritance

Composition is a way of combining simple units of code to create more complex behavior. It involves creating objects that delegate certain tasks to other objects, rather than trying to do everything themselves. Composition can be a more flexible and scalable approach to code reuse than inheritance, as it allows you to mix and match behaviors at runtime and to easily change the implementation of a behavior by swapping out the composed object.

Inheritance, on the other hand, involves creating a new class that extends an existing class, inheriting its behavior and implementation. This can be inflexible, as it ties the subclass to the implementation of the superclass and can make it difficult to change the implementation of a behavior without changing the subclass.

class Color {
  constructor(color) {
    this.color = color;
  }
}
 
class Circle {
  constructor(color, radius) {
    this.color = new Color(color);
    this.radius = radius;
  }
 
  area() {
    return Math.PI * this.radius * this.radius;
  }
}
 
const circle = new Circle("red", 5);
console.log(circle.area()); // 78.53981633974483
Last updated on