JS Coding Standard

  1. Proper Naming
  2. Naming Styles
  3. Access Control
  4. Constants
  5. Variables
  6. Enums
  7. Classes
  8. Conditions
  9. Parameters
  10. Builder Pattern
  11. Adapter Pattern
  12. Separation of Concern
  13. Convention Over Code
  14. Factory Pattern
  15. Bitwise Operators
  16. Lookup Tables
  17. Mixins
  18. Entity Component System
  19. Provider Pattern
  20. Async vs Sync
  21. Additional Concepts

Proper Naming

No matter what code you write, you should follow proper naming conventions. See my post on this subject for more details.

Naming Styles

Choosing the right naming style in JavaScript is crucial for code clarity and consistency. The most commonly used convention is camel case, where variables and function names begin with a lowercase letter and capitalize the first letter of each subsequent word within the name, such as myVariable or calculateInterest.

It’s important to note that class names and enums in JavaScript traditionally use Pascal case, where each word starts with a capital letter, such as Person or CarModel. This distinction helps differentiate classes, which are often used as blueprints for creating objects, from other elements in the codebase. By adhering to these naming conventions, developers can improve readability and maintainability across their JavaScript projects.

Global constants in JavaScript conventionally use “SCREAMING_SNAKE_CASE”. This means the entire name is in uppercase letters, and words are separated by underscores (_).

When working with JSON, the keys are snake case. This means the entire name is in lowercase letters, and words are separated by underscores (_).

Access Control

This is an important subject that is often overlooked by developers. Access control determines which code is public and which code remains private. The guideline is to only make code public if other parts of the system need to use it directly. By default, everything should be private unless there’s a specific reason to make it public.

If you need to make something public for testing purposes, consider how you can test it without compromising its privacy.

Striving for predictability in code is crucial. This involves managing how we read and write to entities within our system. Therefore, it’s important to conceal the internal workings of your system as much as possible, only exposing what is absolutely necessary.

Considerations

  • Use modules to encapsulate code.
  • Only export what is intended to be used externally.
  • Follow the principle of least privilege, only give access to code or data that is absolutely necessary for a particular function or module. This minimizes the risk of unintended interactions or security vulnerabilities.
  • Ensure that the public API is well-documented, so other developers know exactly what they can use and how to use it. This reduces the likelihood of misuse and makes the code more maintainable.

Constants

This should be the default.
Only change it to a variable (let) when you find yourself wanting to change it.

const GLOBAL_CONSTANT = 20;

function doSomething() {
  const value = "Hello World";
}

Note the case for global constants, it is all upper case and new words are separated using a underscore.

Variables

The keyword “var” is dead and should never be used.

function doSomething(condition) {
  let value = 10;
  
  if (condition === true) {
    value = 20;
  }
}

Enums

JS does not have a Enum type, but you can emulate it using dictionaries and Object.freeze. Object.freeze makes it read only.

const Animals = Object.freeze({
  DOG: "dog",
  CAT: "cat",
  OTHER: "other"
});

When you have defined a ENUM, you must always refer to the ENUM and not to the value. When a value changes, there is only one place to update the value change. This makes the code more robust and maintainable.

Wrong Example:

// !!! DO NOT DO THIS !!!
const animal = "cat";

if (animal === "cat") {
  ...
}

Right Example:

const animal = Animals.CAT;
...

if (animal === Animals.CAT) {
  ...
}

You can also use ENUMs as part of convention over code.
In this example we will give a speak action based on the animal.

const AnimalGreetings = Object.freeze({
  [Animals.DOG]: () => console.log("bark"),
  [Animals.CAT]: () => console.log("purrr"),
  [Animals.OTHER]: () => console.error("not an animal")
});

Benefits of Enums

  1. Readability and Maintainability: Enums make code more readable by replacing magic numbers or strings with meaningful names. This improves code comprehension and maintainability.
  2. Type Safety: Enums enforce type safety by ensuring that only predefined values can be assigned to a variable. This reduces the risk of invalid values being used.
  3. Namespace Management: Enums group related constants together, avoiding naming conflicts and making the codebase cleaner.
  4. Iteration: Enums allow easy iteration over possible values, which is useful for tasks like validation, display in UIs, or creating dropdown lists.
  5. Documentation: Using enums can serve as a form of documentation, providing clear context for what values are possible for a given variable.
  6. Error Reduction: Enums help prevent errors by restricting the values that a variable can take, reducing bugs related to invalid values.
  7. Refactoring: Enums make refactoring easier and safer. Changes to enum names or values are centralized, reducing the chance of missing updates scattered throughout the codebase.

Classes

Example of private members.

class Something {
  #privateField;
  
  #privateMethod() {
    ...
  }
}

Private, is the default way members are defined in classes.
Only make members public when you need to use it as part of the public API of that class.

Fields are never public but can be provided with a public interface using property getters and setters.

class Something {
  #person;
  
  get person() {
    return Object.freeze(this.#person)
  }
  
  set person(newValue) {
    this.#person = assertAllowedValue(newValue)
  }
  
  getMutPerson() {
    return this.#person;
  }
}

In the example above, we return a read-only object. When dealing with simple data types such as strings and numbers, enforcing immutability isn’t necessary. However, for object types, ensuring immutability by default is crucial, which can be achieved using Object.freeze.

In your class API, if mutable versions are required, it should be explicit and requested through a getMut method.

Clarity of intention is paramount. Many bugs stem from unforeseen alterations in intricate systems. Adopting a default read-only approach helps mitigate this issue.

When requesting a mutable version, it is deliberate; the intent is to effect changes as part of the design, not by oversight.

Setting a property using a setter is a deliberate action. It is crucial to validate values before applying them.

In the previous example, assertAllowedValue throws an error if the value is not permissible. Alternatively, you might opt for an if block to handle a silent failure instead.

class Something {
  #person;
  
  set person(newValue) {
    if (assertAllowedValue(newValue)) {
      this.#person = newValue
    }        
}

It’s important to note that silent failures come with their own set of pitfalls. For instance, assuming that an object’s value has changed when it actually doesn’t can lead to unpredictable and potentially unsafe outcomes. This can result in unintended behaviors and make debugging more challenging.

Additionally, it’s crucial to implement proper error handling or validation mechanisms to ensure that unexpected changes are appropriately managed and communicated within your codebase. This helps maintain clarity and predictability in your application’s behavior.

Conditions

Conditions are required in all programming.
There are a couple of rules to consider.

  1. Where possible use convention over code instead of a conditional statement.
  2. Use ternary operators to simplify common if / else statements.
  3. When assigning values instead of if statements consider using boolean expressions where possible. For example
    1. ||=
    2. &&=
    3. ??=

Function / Method parameters

There are two types of parameters:

  1. Required parameters
  2. Optional parameters

We will focus solely on required parameters.
Whenever possible, required parameters should have default values.
Parameters without default values must be validated to ensure they are correct. This includes checking if they have a value, if the value is of the correct type, and if the value falls within the appropriate range.

Consider the following method as an example.

export class Column {
    static create(title,
                  field,
                  dataType=DataType.STRING,
                  isReadOnly=true,
                  width=DEFAULT_WIDTH,
                  align=DEFAULT_ALIGN,
                  sortable=DEFAULT_SORTABLE,
                  sortDirection=DEFAULT_SORT_DIRECTION,
                  groupId=null) {

        return {
            title: assertRequired(title, "Column title is required", "string"),
            field: assertRequired(field, "Column field is required", "string"),
            dataType,
            isReadOnly,
            width,
            align,
            sortable,
            sortDirection,
            groupId
        }
    }
}

Here, I am using a class as a namespace to enhance context and readability. This class functions as a factory to create a dictionary. All parameters are required, except for “groupId,” which defaults to null.

For most parameters, I can provide logical defaults. However, for “title” and “fieldName,” I cannot make assumptions. Therefore, I use assertRequired, a utility function that raises an exception if a parameter is either not defined or not of the correct data type (in this case, a string). Missing parameters in this context are critical failures, and I want the process to break to ensure the developer defines the title and field. Automated testing should catch and report these failures.

An interesting side note is that the builder pattern can also be an excellent alternative for creating the above dictionary.

With the current method, you can get away with providing only the first two parameters because the rest have default values. However, if you want to set the “sortable” parameter, you must also define all the preceding parameters. The builder pattern offers more flexibility in the calling API, allowing you to set only the parameters you need without having to specify all preceding ones.

let column = Column.create("Code", "code");
// or 
let column = Column.create("Code", "code", DataType.string, true, 100, Alignment.left)

Builder Pattern

When using the builder pattern, the declaration in JavaScript is more verbose, but the calling API is much cleaner.

class ColumnBuilder {
    #title;
    #field;
    #dataType = DataType.STRING;
    #isReadOnly = true;
    #width = DEFAULT_WIDTH;
    #align = DEFAULT_ALIGN;
    #sortable = DEFAULT_SORTABLE;
    #sortDirection = DEFAULT_SORT_DIRECTION;
    #groupId = null;

    setTitle(title) {
        this.#title = assertRequired(title, "Column title is required", "string");
        return this;
    }

    setField(field) {
        this.#field = assertRequired(field, "Column field is required", "string");
        return this;
    }

    setDataType(dataType) {
        this.#dataType = dataType;
        return this;
    }

    setIsReadOnly(isReadOnly) {
        this.#isReadOnly = isReadOnly;
        return this;
    }

    setWidth(width) {
        this.#width = width;
        return this;
    }

    setAlign(align) {
        this.#align = align;
        return this;
    }

    setSortable(sortable) {
        this.#sortable = sortable;
        return this;
    }

    setSortDirection(sortDirection) {
        this.#sortDirection = sortDirection;
        return this;
    }

    setGroupId(groupId) {
        this.#groupId = groupId;
        return this;
    }

    build() {
        return {
            title: this.#title,
            field: this.#field,
            dataType: this.#dataType,
            isReadOnly: this.#isReadOnly,
            width: this.#width,
            align: this.#align,
            sortable: this.#sortable,
            sortDirection: this.#sortDirection,
            groupId: this.#groupId
        };
    }
}

The builder pattern is a design pattern that provides a flexible solution to the complex problem of creating objects. It allows the construction of an object step-by-step by separating the construction and representation of the object. This pattern is particularly useful when an object requires multiple parameters, especially when some of these parameters are optional.

Conventions to follow when implementing the builder pattern include ensuring that the builder class encapsulates the construction logic and that each method returns the builder object itself, allowing for method chaining.

The builder pattern is ideal for scenarios where an object has numerous optional parameters, when the creation process involves multiple steps, or when you want to ensure the immutability of the created objects. This pattern enhances code readability, maintainability, and robustness by avoiding the need to pass numerous parameters through a constructor and providing clear, intention-revealing method names for setting each parameter.

// usage example
const column = new ColumnBuilder()
    .setTitle("My Column")
    .setField("myField")
    .setSortable(true)
    .build();

The Adapter Pattern

Before delving into the standard around this pattern, let’s set the stage with a scenario to better grasp the pattern and its associated conventions.

Imagine a “Columns” class designed for a data grid component. Each column within this class can possess various properties such as:

  • Title
  • Field
  • Sort Order
  • Alignment

The Columns class serves as a manager for a collection of “Column” objects. Below are key functionalities pertinent to the adapter pattern:

  1. Creation from Different Sources: Instantiate the “Columns” class from diverse sources, generating a “Column” object for each column and maintaining the collection. These sources encompass:
  • JSON
  • HTML Element
  • Hard-coded values
  1. Transformation of Column Collection: Convert the current set of “Column” objects into:
  • JSON format
  • HTML elements
  • CSS grid column templates

When contemplating the adapter pattern, envision power adapters as an analogy. Just as you might need an adapter to use a two-point plug in a three-point socket, the adapter pattern facilitates seamless compatibility between different interfaces or data formats. In other words, the Adapter Pattern allows incompatible interfaces to work together. This can be useful when you need to integrate components that were not designed to work together, or when you want to use an existing class but its interface does not match the one you need.

In our scenario, we may need to load column data from JSON and utilize it in a CSS context. Our objective includes designing a straightforward interface without necessitating a method for each conversion type (e.g., loadFromJson and saveToJson). Managing multiple conversion methods can become verbose. Thus, we adopt a convention to streamline this process.

Step 1 – Create a enum for the conversion types.

export const ConversionType = Object.freeze({
    CSS: "css",
    JSON: "json",
    HTML: "html"
});

Step 2 – Introduce from and to methods. In our example, we’ll integrate these directly into the Columns class. However, in different contexts, you might opt to create a separate class for handling these conversions. It’s essential to assess each case individually to decide if separation of concerns is necessary. Given that the Columns class focuses on column management, integrating the from and to methods within it aligns logically with its primary responsibilities.

Both methods require a parameter specifying the conversion type, indicated by passing an enum value that defines the intent. The from method additionally needs a parameter indicating the source of data from which columns are loaded. For instance, if the conversion type is JSON, the source parameter would be a JSON object; if HTML, it would be an HTML element that requires processing.

class Columns {
    #collection = [];
  
    to(conversionType) {
        switch (conversionType) {
            case ConversionType.CSS: {
                return toCSS(this.#collection);
            }
            case ConversionType.JSON: {
                return toJSON(this.#collection);
            }
            case ConversionType.HTML: {
                return toHTML(this.#collection);
            }
            default:
                return null;
        }
    }  
    
    static from(conversionType, source) {
        const result = new Columns();

        switch (conversionType) {
            case ConversionType.JSON: {
                return result.set(fromJSON(source));
            }
            case ConversionType.HTML: {
                return result.set(fromHTML(source));
            }
            default:
                return null;
        }
    }
}

In our scenario, we aimed to integrate the adapter with a factory approach. Essentially, this means loading columns from a source and creating an instance of the Columns class. Subsequently, we can convert the data to the required format as needed. This approach justifies the method being static, enabling us to invoke it without requiring an existing instance of “Columns.”

The standard guideline is as follows:

  • Utilize an enum to specify the conversion types.
  • Employ method names from and to. In essence, use from to load from one type and to to convert to another.
  • Maintain simplicity in the method internals by invoking external functions to handle each condition.
  • Ensure that functions responsible for performing the conversion operations start with either “from” or “to” as needed.

Separation of Concern

The Separation of Concerns (SoC) pattern is indispensable in all development, irrespective of programming language. Its principle is straightforward, yet many struggle to implement it consistently. Simply put, SoC means each component should perform a single task. Clear definition of context is crucial to avoid mixing concerns. For instance, operations on columns should not overlap with those on rows. Column-related data and actions should reside within a dedicated context, as should row-related ones. Other parts of the system may integrate these contexts, but their role is strictly to facilitate integration, not redefine data or actions.

SoC also applies:

  1. Classes: Each class should have a clear responsibility and should encapsulate a single aspect of functionality. This ensures that changes and maintenance are localized and predictable.
  2. Methods: Methods within classes should follow SoC, focusing on performing specific actions related to the class’s responsibility without crossing into other concerns.
  3. Functions: Like methods, standalone functions should ideally perform a single task or operation. This promotes code clarity, reusability, and easier testing.
  4. API Endpoints: Each API endpoint should handle a specific type of request and perform a clearly defined operation. This separation ensures that the API remains cohesive and adheres to its intended purpose.
  5. Data Structures: Data structures, such as arrays, objects, or complex data models, should organize data in a way that aligns with their specific use cases. For example, separating data storage concerns from data processing concerns.
  6. Pipelines: In data processing or software workflows, pipelines should be modularized, with each stage or component responsible for a specific transformation or action on the data. This approach makes pipelines easier to understand, maintain, and scale.
  7. Modules: Modules in modular programming languages or systems should ideally have clear responsibilities and limited interaction with other modules, focusing on specific functionalities or features.
  8. Configuration Files: Configuration files, such as JSON, YAML, or XML files used to set parameters or settings for an application, should separate concerns related to configuration from core application logic.
  9. Middleware: In web applications or APIs, middleware components should handle cross-cutting concerns such as logging, authentication, and validation separately from business logic.
  10. Services: Service layers in applications should focus on specific business operations or integrations, maintaining a separation from both data access and presentation layers.

Mixing concerns complicates code, often leading to bugs. Focused code, aligned with its intended purpose, is easier to test due to predictable inputs and outputs. In contrast, mixing concerns makes code harder to grasp and less predictable.

Benefits of Applying SoC:

  • Modularity: Encourages modular design, where components can be independently developed, tested, and maintained.
  • Clarity: Promotes clear code organization, making it easier to understand and reason about each component’s purpose.
  • Reusability: Facilitates reuse of components across different parts of the application or in different contexts.
  • Scalability: Allows for easier scaling of the application as new features or requirements are added.

By applying SoC across these areas, developers can create more maintainable, understandable, and robust software systems, leading to better overall software quality and developer productivity.

Convention Over Code

Employing a good convention can significantly reduce the amount of code required. By recognizing patterns, you can leverage conventions to save time and effort.

Consider this simplified example:
You have multiple buttons, and clicking each button should update a numeric value.

One approach is creating individual events for each button, where each event handler updates the value. However, this doesn’t scale efficiently; with ten buttons, you’d end up with ten separate events, which isn’t ideal.

A more effective method is to use a single event that all buttons subscribe to. Each button can have a data-value attribute that specifies its numeric value. In the event handler, you fetch this value from the clicked button and update accordingly. This approach drastically reduces redundancy, replacing ten separate events with just one. Plus, as you add more buttons, the code remains unchanged.

The convention here is simple yet powerful: buttons must include a data-value attribute defining their respective values, ensuring consistent and scalable code management.

<button data-value="1">One</button>
...
<button data-value="10">Ten</button>
class ViewModel {
  #value = 0;
  
  async onButtonClick(event) {
    let newValue = event.target.dataset.value;
    
    if (newValue) {
      this.#value = int(newValue);
    }  
  }
}

Conventions offer several advantages over writing explicit code:

  1. Simplicity and Readability: Conventions often use familiar patterns or naming schemes that are easier to understand and maintain. This clarity reduces cognitive load when reading and debugging code.
  2. Consistency: Following conventions ensures that code across a project or within a team adheres to the same standards. This consistency makes it easier for developers to work collaboratively and understand each other’s code.
  3. Scalability: Conventions are typically designed to scale with the project. They promote practices that facilitate adding new features or extending existing ones without necessitating major rewrites or restructuring.
  4. Reduced Code: By relying on conventions, developers can often achieve the desired functionality with less code. This reduction decreases the likelihood of bugs and improves code efficiency.
  5. Maintenance and Updates: Conventions make it easier to update or refactor code because changes can be applied consistently across the project. This saves time and minimizes the risk of introducing new bugs.
  6. Tooling and Automation: Many development tools and frameworks are built around conventions, providing built-in support and automation. This integration can streamline development processes and improve productivity.

Overall, conventions promote good coding practices, enhance code quality, and contribute to a more efficient and manageable development process.

Factory Pattern

The factory pattern functions like a production line in a car factory, manufacturing necessary objects consistently. As seen previously, we’ve discussed two instances: the Columns.create namespace example and the builder pattern.

The primary goal of the factory pattern is to abstract away complexities involved in object creation. In the aforementioned examples, creating a column object involves more than just assembling a dictionary; it includes tasks like validation, applying default values, enforcing business rules, and more. By encapsulating these processes, the factory pattern shields the caller from these intricacies, thereby simplifying the codebase.

Here are the core benefits of using the factory pattern:

  1. Encapsulation: The factory pattern encapsulates the object creation process, hiding the complexity of object instantiation from the client code.
  2. Centralized Control: It provides centralized control over object creation, allowing you to manage how objects are created and configured in one place.
  3. Consistency: Ensures consistent object creation throughout the application, reducing the chances of errors and ensuring adherence to coding standards.
  4. Flexibility: Facilitates easy changes to the object creation process, such as switching between different types of objects or adjusting configuration settings, without modifying client code.
  5. Code Reusability: Promotes code reuse by centralizing object creation logic, which can be utilized across different parts of the application.
  6. Separation of Concerns: Supports separation of concerns by separating the responsibility of object creation from other functionalities, improving code organization and maintainability.
  7. Enhanced Testability: Simplifies unit testing by allowing you to mock or substitute the factory when testing components that depend on the created objects.
  8. Scalability: Facilitates scalability by providing a structured approach to object creation, accommodating changes and additions to object types or configurations over time.

These benefits make the factory pattern a valuable tool for managing object creation complexities and promoting modular, maintainable software design.

Bitwise Operators

Bitwise operators can seem daunting at first, but they’re extremely practical. We’ll use enums and bitwise operators instead of an array to define options.

Step 1 – declare the enum

const UpdateOptions = Object.freeze({
  CSS: 1,
  DATA: 2,
  LAYOUT: 4
})

Note that the values of the enum must be numeric, following a bit sequence that doubles each time: starting at 1, then 2, 4, 8, 16, and so on.

Step 2 – define the option combination you need

Example – combine options example

const updateOption = UpdateOptions.CSS | UpdateOptions.LAYOUT

Example – add to option

let updateOption = UpdateOptions.CSS;
updateOptions |= UpdateOptions.DATA;

Example – remove option

let updateOptions = UpdateOptions.CSS | UpdateOptions.LAYOUT;
updateOptions &= ~UpdateOptions.LAYOUT;

Example – toggle option on and off

let updateOptions = UpdateOptions.CSS | UpdateOptions.DATA;
updateOptions ^= UpdateOptions.DATA;

This example shows that we want to update the CSS and LAYOUT but not the DATA. You can combine these options in any way you like. As you can see, the code remains clean and highly readable. This is a major benefit of using bitwise operators. Additionally, the performance is exceptional.

Step 3 – check the option and execute intent as required.

Example – check single value in option

if (updateOption & UpdateOptions.LAYOUT) {
  ...
}

Example – check for multiple values in option

if (updateOption & (UpdateOptions.LAYOUT | UpdateOptions.DATA)) {
  ...
}

Here are some benefits of using bitwise operators:

  1. Efficiency: Bitwise operations are generally faster than arithmetic operations because they are directly supported by the processor.
  2. Memory Optimization: Bitwise operators can pack multiple boolean flags into a single integer, reducing memory usage.
  3. Clean Code: Using bitwise operators with enums can result in cleaner, more readable code when managing multiple flags or options.
  4. Fine-grained Control: Bitwise operators allow precise control over individual bits, enabling operations like setting, clearing, flipping, and testing specific bits.
  5. Performance: Operations on bits are computationally inexpensive, making bitwise operators a performance-efficient choice for low-level programming tasks.
  6. Compact Representation: Bitwise operations can compactly represent and manipulate data structures, such as bitfields or bitmaps, useful in scenarios like permissions handling or feature toggles.
  7. Flexibility: Bitwise operators enable flexible combinations of flags, allowing any combination of options to be represented and manipulated easily.
  8. Portability: Bitwise operations are universally supported across all programming languages and hardware architectures, ensuring consistent behavior.
  9. Algorithm Implementation: Many algorithms, particularly those in cryptography, graphics, and compression, rely heavily on bitwise operations for their efficiency and effectiveness.

Lookup Tables

Lookup tables in JavaScript offer several benefits, particularly in terms of performance and code maintainability. Here are some key advantages:

  1. Speed and Efficiency:
    • Faster Access: Lookup tables provide O(1) time complexity for retrieval operations. This means that accessing a value in a lookup table is extremely fast, regardless of the size of the table.
    • Reduced Computation: Instead of performing complex calculations or running through conditional logic, a lookup table allows you to retrieve precomputed results directly, saving time and processing power.
  2. Simplified Code:
    • Cleaner and More Readable Code: Using lookup tables can make your code more readable and easier to maintain. Instead of long chains of if-else statements or switch cases, a lookup table can handle many conditions in a concise way.
    • Ease of Maintenance: Updating a lookup table is often simpler than modifying multiple conditional statements. You can add, remove, or change mappings in one centralized location.
  3. Consistency:
    • Avoiding Repetition: By centralizing values in a lookup table, you ensure consistency across your codebase. This minimizes the risk of errors due to duplicated values or logic scattered throughout the code.
  4. Flexibility:
    • Dynamic Lookups: Lookup tables can be modified at runtime, allowing for dynamic changes in behavior without altering the underlying code logic.
    • Configuration-Driven Logic: They enable you to externalize configuration data, making it easier to adapt your application to different environments or requirements by simply changing the table entries.
  5. Memory Efficiency:
    • Space Efficiency: While lookup tables consume memory, the trade-off is often worthwhile if the alternative involves significantly more computation or code complexity.

Example Use Case

Consider a scenario where you need to convert a day number (0-6) to a day name:

// Code we are trying to avoid
function getDayName(dayNumber) {
    switch (dayNumber) {
        case 0: return 'Sunday';
        case 1: return 'Monday';
        case 2: return 'Tuesday';
        case 3: return 'Wednesday';
        case 4: return 'Thursday';
        case 5: return 'Friday';
        case 6: return 'Saturday';
        default: return 'Invalid day';
    }
}

// With a lookup table
const dayNames = 
['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

function getDayName(dayNumber) {
    return dayNames[dayNumber] || 'Invalid day';
}

In the example above, using a lookup table (dayNames array) simplifies the function and makes it easier to maintain.

Convention based lookup table

In this example we are going to read an attribute from a HTML Element and based on the value call a function.

const actionsTable = {
  "save": "saveRecord",
  "save-as": "saveRecordAs",
  "cancel": "revertChanges"
}

function onClick(event) {
  // read the value from DOM and make sure it is a valid value.
  const action = assertValidAction(event.target.dataset.action);
  
  // look up the function name we need to call
  const functionName = actionsTable[action]
  
  // perform the function
  this[functionName].call(this);
}

This scales very well because new actions does not require many changes to our “onClick” method. If I have three or ten actions, “onClick” stays the same.

In summary, lookup tables are a powerful tool for improving performance, simplifying code, ensuring consistency, and providing flexibility. They are particularly useful when dealing with frequent, repetitive tasks that involve mapping one set of values to another.

Mixins

Mixins are an older JavaScript pattern that involves adding properties or functions to an object, providing a more flexible alternative to class inheritance. With class inheritance, you often inherit code that you don’t need, leading to bloated and less maintainable objects. In contrast, mixins extend an object just in time with the specific features you need. JavaScript’s dynamic nature allows for this flexibility, making it a straightforward and powerful technique for composing behaviors and functionalities.

The mixin pattern is a powerful tool in JavaScript for creating flexible, reusable code. By composing behaviors from different sources, you can keep your codebase modular and maintainable. Whether you’re building small applications or large-scale projects, understanding and using mixins can greatly enhance your coding experience.

// method that will be added during mixin
function walk(x, y) {
    this.position.x = x;
    this.position.y = y;
}

// mixin add function
addWalkMixin(target) {
    target.position = {x: 0, y: 0};
    target.walk = walk;
}

// mixin cleanup function
removeWalkMixin(target) {
    delete target.position;
    delete target.walk;
}

// usage example
const myPerson = {};
addWalkMixin(myPerson);
myPerson.walk(10, 20);

Benefits of the Mixin Pattern

  1. Code Reusability: Mixins allow you to reuse methods and properties across different objects or classes, reducing duplication and enhancing maintainability.
  2. Flexible Composition: Mixins promote composition over inheritance, enabling you to combine functionalities from multiple sources without the limitations of single inheritance.
  3. Separation of Concerns: By isolating different functionalities into separate mixins, you keep your code modular and easier to manage.
  4. Dynamic Behavior: Mixins can be applied at runtime, allowing you to extend objects with new behaviors dynamically, which is particularly useful in dynamic applications.
  5. Avoid Inheritance Hierarchy Issues: Mixins help avoid the pitfalls of deep inheritance hierarchies, such as the diamond problem and tightly coupled classes.

Downfalls of the Mixin Pattern

  1. Namespace Collisions: Since mixins add methods and properties directly to objects, there is a risk of name collisions if different mixins define methods or properties with the same name.
  2. Lack of Clarity: Applying multiple mixins to an object can make the source of certain behaviors unclear, making the code harder to understand and maintain.
  3. Potential for Conflicts: When combining mixins, conflicts can arise if multiple mixins define methods or properties that interact in unintended ways.
  4. Overhead of Manual Application: Unlike inheritance, where the parent class’s methods and properties are automatically available to the subclass, mixins require explicit application, adding some overhead and potential for human error.
  5. Encapsulation Issues: Mixins can break encapsulation by exposing internal details of an object, leading to less robust and harder-to-maintain code.
  6. Dependency Management: Managing dependencies between different mixins can become complex, especially as the number of mixins and their interdependencies grow.

The mixin pattern is a powerful tool in JavaScript, offering flexibility and reusability that traditional inheritance can lack. However, it also introduces potential complexities and risks that need to be managed carefully. By understanding both the benefits and downfalls, you can make informed decisions about when and how to use mixins in your projects.

Entity Component System (ECS)

ECS stands for Entity, Component, System. Rather than delving into a comprehensive book on ECS, let’s explore this powerful and flexible pattern from a straightforward perspective.

Usage of ECS

Initially designed as a data-driven pattern for managing numerous static and dynamic objects in games, ECS has proven its versatility. Beyond gaming, ECS can be highly beneficial for UI development and potentially for various server-side applications.

Core Concepts of ECS

  1. Entity: A simple object identified by a unique ID.
  2. Component: A data structure serving as an attribute of an entity.
  3. System: Functions that perform actions on entities, typically based on their components.
// create simple entity
const entity1 = EntityBuilder.build();

// entity1 = {
//   id: 1
// }

// create a entity with components
const entity2 = EntityBuilder
  .addComponent("walk")
  .addComponent("talk")
  .build()
  
// entity2 = {
//   id: 2,
//   walk: {
//     isWalking: false,
//     x: 0,
//     y: 0
//   }, 
//   talk: {
//     isTalking: false,
//     saying: ""
//   }
// }
  
// system for walking entities
function walk(x, y) {
  const entities = EntityQueryManager.byComponents(["walk"])
  for (let entity of entities) {
    walkEntity(entity, x, y);
  }
  
  updateRender(entities);
}

function walkEntity(entity, x, y) {
    entity.walk.isWalking = true;
    entity.walk.x += x;
    entity.walk.y += y;
}

ECS in JavaScript

JavaScript’s dynamic nature minimizes the need for extensive boilerplate code to implement ECS. Here’s how it can be structured:

  • Entity: Essentially a dictionary (or object) with an id property.
  • Components: Attached to entities to extend their data.

For example, consider several objects on a screen. Some are movable, while others are not. All are entities, but a movable object has a “movement” component, which is a dictionary containing properties related to movement, such as whether it is moving and its speed.

In many ways, this is akin to the mixin pattern, with a crucial distinction: ECS inflates only data structures, not functions. Systems are the functions that act on entities, driven entirely by data. This data-driven composition makes ECS lightweight and memory-efficient.

Query Manager

A critical part of the ECS pattern is the query manager. It tracks entities and their components, enabling efficient queries based on component composition. For instance:

  • Get all objects with the walk component.
  • Get all objects with both walk and talk components.
  • Get all objects with walk and talk components within a radius of 10 units from a given point.

This querying capability allows the use of spatial data structures to limit results and enhance performance.

Benefits of ECS

  1. Modularity and Reusability: ECS promotes a highly modular architecture. Components are reusable and can be combined in various ways to create complex behaviors without duplicating code.
  2. Separation of Concerns: ECS separates data (components) from behavior (systems), which helps in maintaining clean and manageable code.
  3. Scalability: ECS can handle a large number of entities efficiently. The separation of data and behavior allows for easy parallelization and optimization.
  4. Flexibility: Entities can change dynamically by adding or removing components, allowing for flexible and dynamic behavior changes at runtime.
  5. Memory Efficiency: By only inflating data structures and not functions, ECS can be more memory-efficient compared to other patterns.
  6. Ease of Maintenance: With clear separation of responsibilities, ECS systems are easier to test, debug, and maintain.
  7. Performance Optimization: ECS allows for targeted updates and optimizations. Systems can be designed to only act on entities with specific components, reducing unnecessary computations.

Downsides of ECS

  1. Complexity for Simple Projects: For small projects or simple applications, the overhead of setting up and managing ECS may not be justified.
  2. Learning Curve: Developers unfamiliar with ECS may find the pattern challenging to grasp initially, especially if they are accustomed to traditional object-oriented programming.
  3. Overhead of Component Management: Managing components and systems can introduce overhead, particularly if the implementation is not well-optimized.
  4. Potential for Fragmentation: With data spread across multiple components and systems, it can be challenging to get a holistic view of an entity’s state.
  5. Initial Setup Time: Setting up an ECS framework requires careful planning and design, which can be time-consuming.
  6. Performance Trade-offs: While ECS can be optimized for performance, poor design or implementation can lead to performance bottlenecks. For example, inefficient querying or poor memory layout can negate ECS benefits.
  7. Debugging Complexity: Debugging ECS-based systems can be more complex due to the decoupled nature of entities, components, and systems. Tracing the flow of data and behavior can be more challenging.

Conclusion

ECS can be simple to start with, yet it offers the flexibility to handle complex scenarios as needed. Its lightweight nature and efficiency make it a valuable pattern in various development contexts.
Implementing other patters such as lookup tables and builder pattern will simplify the framework around ECS.

Provider Pattern

The Provider pattern is a design approach that emphasizes the separation of concerns by delegating specific tasks to specialized components, known as providers. This pattern is particularly useful in complex systems where different scenarios require distinct handling or processing. By organizing the code into providers, each dedicated to a particular responsibility, you can achieve a more modular and maintainable architecture.

Core Concept

The central idea behind the Provider pattern is to abstract the logic for different scenarios into discrete, self-contained units. Each provider is designed to handle a specific aspect of the functionality, making the system more organized and easier to manage. For example, consider a system that needs to support multiple types of data parsing or processing. Instead of embedding all the logic within a single monolithic component, you would create separate providers, each responsible for a particular type of data or processing task. This approach not only simplifies the code but also makes it more adaptable to changes and extensions.

Example: Binding Engine

To illustrate the Provider pattern in action, let’s consider a binding engine, which is a common component in UI frameworks that manages the data binding between the user interface and the underlying data model.

In a binding engine, as the UI is parsed, the engine may encounter different binding scenarios that require generating specific supporting code. Rather than handling all these scenarios directly within the binding engine, the system can use providers to manage the complexity. Here’s how it works:

  1. Parsing the Expression: As the UI is parsed, the binding engine encounters a binding expression (e.g., data binding syntax within a UI template).
  2. Identifying the Provider: The binding engine examines the binding expression and determines which provider is responsible for handling that specific scenario. For instance, one provider can handle updates to attributes and another, updates to styles.
  3. Delegating the Task: Once the appropriate provider is identified, the binding engine delegates the task to that provider. The provider then generates the necessary code or performs the required processing to support the binding scenario.
  4. Output: The binding engine integrates the output from the provider back into the overall process, ensuring that the UI behaves as expected.

This modular approach allows the binding engine to support a wide range of binding scenarios without becoming overly complex. It also makes it easier to introduce new types of bindings in the future, as you can simply add new providers without modifying the core binding engine.

Versatility of the Provider Pattern

The Provider pattern is not limited to binding engines; it can be applied in any scenario where tasks need to be delegated to specialized components based on specific requirements. For instance:

  • Authentication Systems: In an authentication system, different providers could handle different authentication methods (e.g., password-based, OAuth, biometric).
  • Payment Gateways: An e-commerce platform could use providers to manage different payment methods, with each provider responsible for interacting with a specific payment gateway.
  • Data Import/Export: In a data processing system, providers could be used to handle different data formats, with each provider responsible for parsing and converting a specific format.

Benefits

  • Modularity: By encapsulating functionality within providers, the system becomes more modular, making it easier to manage and extend.
  • Maintainability: Providers reduce the complexity of the main logic by offloading specific tasks to specialized components, leading to cleaner and more maintainable code.
  • Flexibility: New providers can be introduced to handle new scenarios without disrupting existing functionality, providing a flexible foundation for growth.

The Provider pattern is a powerful tool for designing systems that need to handle a variety of specialized tasks. By delegating responsibilities to specialized providers, you can create a more organized, maintainable, and scalable architecture. Whether in a binding engine, an authentication system, or any other complex scenario, the Provider pattern helps manage complexity by ensuring that each task is handled by the most appropriate component.

Async vs Sync

Question: When should I create a async function.

Functions in most programming environments are synchronous by default, meaning that the thread executing the function will wait for it to complete before proceeding with the rest of the code. This can be problematic in scenarios where the function takes a significant amount of time to execute, as it can cause the application to become unresponsive, particularly if the function is running on the UI thread.

Asynchronous functions offer a solution to this issue by allowing the code execution to continue while waiting for the function to complete. However, it’s important to use asynchronous functions correctly, as misuse can still lead to blocking the UI thread. Generally, asynchronous functions are used in scenarios where you either cannot or do not want to wait for the function to complete immediately. Common examples include making server requests or handling input/output operations.

One of the key advantages of asynchronous functions is their ability to perform multiple operations concurrently. Although this concurrency occurs within a single thread (since JavaScript and many other environments use single-threaded execution), it can still improve efficiency and responsiveness. For instance, while waiting for a server response, the application can continue executing other tasks.

However, asynchronous operations come with their own overhead, as additional infrastructure is required to manage the waiting process. This overhead can impact performance, especially in scenarios where frame-by-frame execution is critical, such as in a real-time canvas renderer. In such cases, using synchronous functions may be preferable to minimize performance and memory overhead.

To determine whether a function should be synchronous or asynchronous, consider the following checklist:

  1. Execution Time: If the function takes a long time to complete and may cause the UI to freeze, consider using asynchronous functions.
  2. Concurrency Needs: If you need to perform multiple tasks concurrently and improve responsiveness, asynchronous functions are beneficial.
  3. Performance Impact: Evaluate the overhead of managing asynchronous operations, especially in performance-critical scenarios like real-time rendering.
  4. User Experience: Consider whether the responsiveness of the application would be negatively affected by waiting for the function to complete.

By carefully evaluating these factors, you can make informed decisions about when to use synchronous versus asynchronous functions in your code.

Additional

  1. JS Input mapping simplified