[![Read Prev](/assets/imgs/prev.png)](/chapters/ch04.0-logtar-our-logging-library.md)

## Refactoring the code

<!--Learn how to structure your project with separate files for logging, configuration, and utility functions. Improve code readability and efficiency with our expert guide to creating a robust, scalable JavaScript architecture -->

> The code for the entire chapter can be found [at the src/chapter_04.1 directory](/src/chapter_04.1)

In this section, we will explore how to improve the organization and maintenance of our library before introducing more features. Currently, all the code is in one file i.e **`index.js`**. `index.js` also serves as the entry point of our project. We will show how to move the code to multiple files without changing how it works.

### **The Need for Refactoring**

The code has become too large and difficult to manage. This chapter covers the benefits of breaking it into smaller files.

Splitting the code into separate files creates a more organized and manageable codebase. Each part should have a clear responsibility, making it easier to work with and understand. This simplification lays the foundation for future improvements, ensures that the project remains consistent and easy to work with, and allows for the introduction of new features.

Some of the key benefits of organizing/splitting your code into smaller re-usable pieces:

1. **Modularity:** Breaking the code into smaller files helps us manage each component better. This way, the codebase is easier to understand and work with.
2. **Readability:** Smaller files are easier to read and understand. Even people who haven't written any code in your library can quickly understand each file's purpose and contents.
3. **Maintainability:** When the codebase is organized into separate files by functionality, it becomes easier to maintain and update. Changes are limited to specific modules, reducing the risk of unintended consequences.
4. **Testing:** Individual components can be tested separately when code is in separate files. This leads to more thorough testing and fewer interdependent tests. (We'll cover testing in a later chapter of this book.)

### **Creating Separate Files**

Let's work together to split the code in **`index.js`** into separate files for each class and utility. First, create a new directory called `lib`. Inside the `lib` directory, create two folders named `utils` and `config`. Add a file `logtar.js` inside the root of the `lib` directory.

Inside the `utils` directory, create two files named `rolling-options.js` and `log-level.js`. Inside the `config` directory, create two files named `rolling-config.js` and `log-config.js`.

Finally, create a file named `logger.js` at the root of the directory, where the `index.js` and `package.json` files are located.

Your directory structure should now look something like this:

```jsx
lib/
├── logtar.js
├── logger.js
├── utils/
│     ├── rolling-options.js
│     ├── log-level.js
├── config/
│     ├── rolling-config.js
│     ├── log-config.js
index.js (entry point)
package.json
```

#### Explanation

The `logtar.js` file serves as the key file that exports all the necessary functionality to the client.

The `logger.js` file exports our `Logger` class and some related functionality.

The `utils/rolling-options.js` file exports our `RollingSizeOptions` and `RollingTimeOptions` classes.

The `index.js` file only contains a single line of code:

```jsx
module.exports = require("./lib/logtar");
```

The other files export functionality based on their names.

> Note: If you are not working with TypeScript and are using vanilla JavaScript, get into the habit of using `JSDoc` as much as possible. Use it for every function's argument and return type. Be explicit. This may take a bit of time, but it will be convenient in the long run. Using `JSDoc` will make your workflow much smoother as your project grows.

### The `index.js` file

Here are the contents inside the `index.js` file

```jsx
module.exports = require("./lib/logtar");
```

### The `lib/logtar.js` file

```jsx
module.exports = {
  Logger: require("./logger").Logger,
  LogConfig: require("./config/log-config").LogConfig,
  RollingConfig: require("./config/rolling-config").RollingConfig,
  LogLevel: require("./utils/log-level").LogLevel,
  RollingTimeOptions: require("./utils/rolling-options").RollingTimeOptions,
  RollingSizeOptions: require("./utils/rolling-options").RollingSizeOptions,
};
```

### The `lib/logger.js` file

```jsx
const { LogConfig } = require("./config/log-config");
const { LogLevel } = require("./utils/log-level");

class Logger {
  /**
   * @type {LogConfig}
   */
  #config;

  /**
   * @returns {Logger} A new instance of Logger with default config.
   */
  static with_defaults() {
    return new Logger();
  }

  /**
   *
   * @param {LogConfig} log_config
   * @returns {Logger} A new instance of Logger with the given config.
   */
  static with_config(log_config) {
    return new Logger(log_config);
  }

  /**
   * @param {LogConfig} log_config
   */
  constructor(log_config) {
    log_config = log_config || LogConfig.with_defaults();
    LogConfig.assert(log_config);
    this.#config = log_config;
  }

  /**
   * @returns {LogLevel} The current log level.
   */
  get level() {
    return this.#config.level;
  }
}

module.exports = { Logger };
```

### The `lib/config/log-config.js` file

```jsx
const fs = require("node:fs");

const { LogLevel } = require("../utils/log-level");
const { RollingConfig } = require("./rolling-config");

class LogConfig {
  /**
   * @type {LogLevel}
   * @private
   * @description The log level to be used.
   */
  #level = LogLevel.Info;

  /**
   * @type RollingConfig
   * @private
   */
  #rolling_config;

  /**
   * @type {string}
   * @private
   * @description The prefix to be used for the log file name.
   *
   * If the file prefix is `MyFilePrefix_` the log files created will have the name
   * `MyFilePrefix_2021-09-01.log`, `MyFilePrefix_2021-09-02.log` and so on.
   */
  #file_prefix = "Logtar_";

  constructor() {
    this.#rolling_config = RollingConfig.with_defaults();
  }

  /**
   * @returns {LogConfig} A new instance of LogConfig with default values.
   */
  static with_defaults() {
    return new LogConfig();
  }

  /**
   * @param {string} file_path The path to the config file.
   * @returns {LogConfig} A new instance of LogConfig with values from the config file.
   * @throws {Error} If the file_path is not a string.
   */
  static from_file(file_path) {
    const file_contents = fs.readFileSync(file_path);
    return LogConfig.from_json(JSON.parse(file_contents));
  }

  /**
   * @param {Object} json The json object to be parsed into {LogConfig}.
   * @returns {LogConfig} A new instance of LogConfig with values from the json object.
   */
  static from_json(json) {
    let log_config = new LogConfig();
    Object.keys(json).forEach((key) => {
      switch (key) {
        case "level":
          log_config = log_config.with_log_level(json[key]);
          break;
        case "rolling_config":
          log_config = log_config.with_rolling_config(json[key]);
          break;
        case "file_prefix":
          log_config = log_config.with_file_prefix(json[key]);
          break;
      }
    });
    return log_config;
  }

  /**
   * @param {LogConfig} log_config The log config to be validated.
   * @throws {Error} If the log_config is not an instance of LogConfig.
   */
  static assert(log_config) {
    if (arguments.length > 0 && !(log_config instanceof LogConfig)) {
      throw new Error(`log_config must be an instance of LogConfig. Unsupported param ${JSON.stringify(log_config)}`);
    }
  }

  /**
   * @returns {LogLevel} The current log level.
   */
  get level() {
    return this.#level;
  }

  /**
   * @param {LogLevel} log_level The log level to be set.
   * @returns {LogConfig} The current instance of LogConfig.
   * @throws {Error} If the log_level is not an instance of LogLevel.
   */
  with_log_level(log_level) {
    LogLevel.assert(log_level);
    this.#level = log_level;
    return this;
  }

  /**
   * @returns {RollingConfig} The current rolling config.
   */
  get rolling_config() {
    return this.#rolling_config;
  }

  /**
   * @param {RollingConfig} config The rolling config to be set.
   * @returns {LogConfig} The current instance of LogConfig.
   * @throws {Error} If the config is not an instance of RollingConfig.
   */
  with_rolling_config(config) {
    this.#rolling_config = RollingConfig.from_json(config);
    return this;
  }

  /**
   * @returns {String} The current max file size.
   */
  get file_prefix() {
    return this.#file_prefix;
  }

  /**
   * @param {string} file_prefix The file prefix to be set.
   * @returns {LogConfig} The current instance of LogConfig.
   * @throws {Error} If the file_prefix is not a string.
   */
  with_file_prefix(file_prefix) {
    if (typeof file_prefix !== "string") {
      throw new Error(`file_prefix must be a string. Unsupported param ${JSON.stringify(file_prefix)}`);
    }

    this.#file_prefix = file_prefix;
    return this;
  }
}

module.exports = { LogConfig };
```

### The `lib/config/rolling-config.js` file

```jsx
const { RollingTimeOptions, RollingSizeOptions } = require("../utils/rolling-options");

class RollingConfig {
  /**
   * Roll/Create new file every time the current file size exceeds this threshold in `seconds`.
   *
   * @type {RollingTimeOptions}
   * @private
   *
   */
  #time_threshold = RollingTimeOptions.Hourly;

  /**
   * @type {RollingSizeOptions}
   * @private
   */
  #size_threshold = RollingSizeOptions.FiveMB;

  /**
   * @returns {RollingConfig} A new instance of RollingConfig with default values.
   */
  static with_defaults() {
    return new RollingConfig();
  }

  /**
   * @param {number} size_threshold Roll/Create new file every time the current file size exceeds this threshold.
   * @returns {RollingConfig} The current instance of RollingConfig.
   */
  with_size_threshold(size_threshold) {
    RollingSizeOptions.assert(size_threshold);
    this.#size_threshold = size_threshold;
    return this;
  }

  /**
   * @param {time_threshold} time_threshold Roll/Create new file every time the current file size exceeds this threshold.
   * @returns {RollingConfig} The current instance of RollingConfig.
   * @throws {Error} If the time_threshold is not an instance of RollingTimeOptions.
   */
  with_time_threshold(time_threshold) {
    RollingTimeOptions.assert(time_threshold);
    this.#time_threshold = time_threshold;
    return this;
  }

  /**
   * @param {Object} json The json object to be parsed into {RollingConfig}.
   * @returns {RollingConfig} A new instance of RollingConfig with values from the json object.
   * @throws {Error} If the json is not an object.
   */
  static from_json(json) {
    let rolling_config = new RollingConfig();

    Object.keys(json).forEach((key) => {
      switch (key) {
        case "size_threshold":
          rolling_config = rolling_config.with_size_threshold(json[key]);
          break;
        case "time_threshold":
          rolling_config = rolling_config.with_time_threshold(json[key]);
          break;
      }
    });

    return rolling_config;
  }
}

module.exports = { RollingConfig };
```

### The `lib/utils/log-level.js` file

```jsx
class LogLevel {
  static #Debug = 0;
  static #Info = 1;
  static #Warn = 2;
  static #Error = 3;
  static #Critical = 4;

  static get Debug() {
    return this.#Debug;
  }

  static get Info() {
    return this.#Info;
  }

  static get Warn() {
    return this.#Warn;
  }

  static get Error() {
    return this.#Error;
  }

  static get Critical() {
    return this.#Critical;
  }

  static assert(log_level) {
    if (![this.Debug, this.Info, this.Warn, this.Error, this.Critical].includes(log_level)) {
      throw new Error(`log_level must be an instance of LogLevel. Unsupported param ${JSON.stringify(log_level)}`);
    }
  }
}

module.exports = { LogLevel };
```

### The `lib/utils/rolling-options.js` class

```jsx
class RollingSizeOptions {
  static OneKB = 1024;
  static FiveKB = 5 * 1024;
  static TenKB = 10 * 1024;
  static TwentyKB = 20 * 1024;
  static FiftyKB = 50 * 1024;
  static HundredKB = 100 * 1024;

  static HalfMB = 512 * 1024;
  static OneMB = 1024 * 1024;
  static FiveMB = 5 * 1024 * 1024;
  static TenMB = 10 * 1024 * 1024;
  static TwentyMB = 20 * 1024 * 1024;
  static FiftyMB = 50 * 1024 * 1024;
  static HundredMB = 100 * 1024 * 1024;

  static assert(size_threshold) {
    if (typeof size_threshold !== "number" || size_threshold < RollingSizeOptions.OneKB) {
      throw new Error(`size_threshold must be at-least 1 KB. Unsupported param ${JSON.stringify(size_threshold)}`);
    }
  }
}

class RollingTimeOptions {
  static Minutely = 60; // Every 60 seconds
  static Hourly = 60 * this.Minutely;
  static Daily = 24 * this.Hourly;
  static Weekly = 7 * this.Daily;
  static Monthly = 30 * this.Daily;
  static Yearly = 12 * this.Monthly;

  static assert(time_option) {
    if (![this.Minutely, this.Hourly, this.Daily, this.Weekly, this.Monthly, this.Yearly].includes(time_option)) {
      throw new Error(
        `time_option must be an instance of RollingConfig. Unsupported param ${JSON.stringify(time_option)}`
      );
    }
  }
}

module.exports = {
  RollingSizeOptions,
  RollingTimeOptions,
};
```

See how we can still benefit from the strong jsdoc type completion for our classes, even if they exist in different files? This isn't something achievable with regular JavaScript – all credit goes to jsdoc.

> The code for the entire chapter can be found [at the src/chapter_04.1 directory](/src/chapter_04.1)

[![Read Next](/assets/imgs/next.png)](/chapters/ch04.2-writing-logs.md)
