DEV Community

Cover image for Don't make counters like that. πŸ›‘
Ivan Zaldivar
Ivan Zaldivar

Posted on

Don't make counters like that. πŸ›‘

Introduction

In this post, you will learn how to create innovative, efficient, and simply impressive counters. πŸ’― Say goodbye to outdated ways of counting. 🌟

No more mundane loops or tedious iterations: we will show you how to leverage powerful concepts such as Closures, functional programming, and OOP design. πŸ”₯πŸ’‘ Whether you're a fan of functional paradigms or a defender of object-oriented programming, we have you covered! πŸ’ͺπŸ”§

Practical example

Before we begin, let's create a simple example that will allow us to illustrate more effectively.

Let's create a function called "semaphore". This function will simulate the behavior of a traffic light. Every time this function is executed, it will change color. When the maximum number of available colors is reached, its value will be reset.

let counter = 0

const semaphore = () => {
  let lights = ['green', 'yellow', 'red']

  // Get counter value.
  const value = counter

  const color = lights.at(value)

  // Increment value
  counter++

  // Reset counter
  if (value >= lights.length - 1) counter = 0

  return color
}

(() => {
  setInterval(() => {
    const color = semaphore()
    console.log(color)
  }, 3000)
})()
Enter fullscreen mode Exit fullscreen mode

Despite being a rather simple example, we can encounter the following issues. Imagine in a more extensive and complex code.

  • Direct mutability of the counter: The counter counter is declared using let and is directly modified using the increment operator (counter++). This introduces direct mutability in the code, which can be problematic when the code becomes more complex, as any part of the code can access and modify the counter without restrictions.
  • Global variable: The variable counter is declared outside the scope of the semaphore function, making it a global variable. This can lead to undesired coupling and make maintenance and understanding of the code in a broader context more difficult. Additionally, the use of global variables can result in naming conflicts and hinder code reuse.
  • Lack of encapsulation: The counter counter is not encapsulated within a structure or object that provides a controlled interface for its manipulation. This makes the internal state of the counter accessible from any part of the code, which can lead to unexpected results and make tracking changes to the counter difficult.
  • Implicit conditional reset: The counter reset is done by checking a condition within an if block (if (value >= lights.length - 1) counter = 0), which can be confusing and hinder code comprehension.
  • Lack of abstraction and reusability: The counter counter is directly handled within the context of the semaphore function. This limits its reusability in other parts of the code and hampers extensibility if additional functionality needs to be added to the counter in the future.

Solution 1: Create counter using Closure

If you're a developer who prefers more than one functional paradigm, you can choose to use a closure to create your counter.

/**
 * Creates a counter object with initial value.
 * @param {number} [initialValue=0] - The initial value of the counter. Defaults to 0 if not provided.
 * @returns {Object} - Counter object with methods.
 */
export const createCounter = (initialValue = 0) => {
  let counter = initialValue;

  /**
   * Retrieves the current value of the counter.
   * @returns {number} - The current value of the counter.
   */
  const getValue = () => counter;

  /**
   * Increments the counter by the specified value.
   * @param {number} [value=1] - The value to increment the counter by. Defaults to 1 if not provided.
   * @returns {void}
   */
  const increment = (value = 1) => {
    counter += value;
  };

  /**
   * Decrements the counter by the specified value.
   * @param {number} [value=1] - The value to decrement the counter by. Defaults to 1 if not provided.
   * @returns {void}
   */
  const decrement = (value = 1) => {
    counter -= value;
  };

  /**
   * Checks if the counter is zero.
   * @returns {boolean} - True if the counter is zero, false otherwise.
   */
  const isZero = () => !counter;

  /**
   * Resets the counter to zero.
   * @returns {void}
   */
  const reset = () => {
    counter = 0;
  };

  return {
    getValue,
    increment,
    decrement,
    isZero,
    reset
  };
};
Enter fullscreen mode Exit fullscreen mode

Implementation

This option is INFINITELY better than the previous one because it utilizes encapsulation and provides a controlled interface to interact with the counter. This improves the abstraction, reusability, control, security, maintainability, and extensibility of the code compared to the first example.

// Create counter.
const counter = createCounter()

const semaphore = () => {
  let lights = ['green', 'yellow', 'red']

  // Get counter value.
  const value = counter.getValue()

  const color = lights.at(value)

  // Increment counter.
  counter.increment()

  // Reset counter.
  if (value >= lights.length - 1) counter.reset()

  return color
}

(() => {
  setInterval(() => {
    const color = semaphore()
    console.log(color)
  }, 3000)
})()
Enter fullscreen mode Exit fullscreen mode

Look, with this change we have greatly improved our code, not only is it more explicit about what we do, but it also offers greater control and security.

Solution 2: Create counter using Class

If you're a developer who prefers more than one OOP paradigm, you can choose to use the implementation of a class to create your counter.

/**
 * Counter class that maintains a count value and provides methods for manipulation.
 */
export class Counter {
  private _value = 0;
  /**
   * Creates a counter object with an initial value.
   * @param {number} [initialValue=0] - The initial value of the counter. Defaults to 0 if not provided.
   */
  constructor(initialValue = 0) {
    this._value = initialValue;
  }

  /**
   * Retrieves the current value of the counter.
   * @returns {number} - The current value of the counter.
   */
  get value(): number {
    return this._value;
  }

  /**
   * Increments the counter by the specified value.
   * @param {number} [value=1] - The value to increment the counter by. Defaults to 1 if not provided.
   */
  increment(value: number = 1): void {
    this._value += value;
  }

  /**
   * Decrements the counter by the specified value.
   * @param {number} [value=1] - The value to decrement the counter by. Defaults to 1 if not provided.
   */
  decrement(value: number = 1): void {
    this._value -= value;
  }

  /**
   * Checks if the counter is zero.
   * @returns {boolean} - True if the counter is zero, false otherwise.
   */
  get isZero(): boolean {
    return !this._value;
  }

  /**
   * Resets the counter to zero.
   */
  reset(): void {
    this._value = 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

It is important to stay away from outdated counting methods and adopt innovative approaches such as the use of Closures or Classes. By adopting these techniques, we can create more efficient, maintainable, and extensible counters that adhere to best practices in functional programming or object-oriented programming paradigms.

Follow me ❀️

Top comments (0)