Skip to content

Latest commit

 

History

History
453 lines (369 loc) · 14.7 KB

ch04.1-refactoring-the-code.md

File metadata and controls

453 lines (369 loc) · 14.7 KB

Read Prev

Refatorando o código

O código de todo capítulo pode ser encontrado no diretório code/chapter_04.1

Nessa seção, vamos explorar como melhorar a organização e manutenção da nossa biblioteca antes de introduzir mais recursos. Atualmente, todo código se encontra em um único arquivo index.js. O index.js também serve como um ponto de entrada do nosso projeto. Vamos mostrar como mover o código para múltiplos arquivos sem alterar o funcionamento.

A necessidade de refatorar

O código se tornou muito grande e difícil de lidar. Esse capítulo vai cobrir os benefícios de quebrar o código em arquivos menores.

Dividir o código em arquivos separados cria uma base de código mais organizada e gerenciável. Cada parte deve ter uma responsabilidade clara, tornando mais fácil o trabalho e entendimento. Essa simplificação estabelece as bases para melhorias futuras, garante que o projeto continuará consistente e fácil de se trabalhar, além de permitir a introdução de novos recursos.

Alguns dos benefícios chave em organizar/dividir o seu código em pedaços menores e reutilizáveis:

  1. Modularidade: Quebrar o código em arquivos menores nos ajuda a gerenciar melhor cada componente. Dessa forma, a base de código se torna mais fácil de trabalhar e entender.
  2. Legibilidade: Arquivos menores são máis fáceis de ler e entender. Até mesmo pessoas que nunca escreveram código na sua biblioteca podem rapidamente entender o propósito e conteúdo de cada arquivo.
  3. Manutenibilidade: Quando a base de código está organizada em arquivos separados por funcionalidade, ela se torna mais fácil de manter e atualizar. Mudanças ficam limitadas a módulos específicos, reduzindo o risco de consequências intencionais.
  4. Testes: Componentes individuais podem ser testados separadamente quando o código está em arquivos separados. Isso leva a testes mais minunciosos e menos interdependentes. (Vamos falar sobre testes mais adiante neste livro.)

Criando arquivos separados

Vamos trabalhar juntos para dividir o código do index.js em arquivos separados para cada classe e utilidade. Primeiramente, crie um novo diretório chamado lib. Dentro do diretório lib, crie duas pastas chamadas utils e config. Adicione um arquivo logtar.js dentro da raiz do diretório lib.

Dentro do diretório utils, crie dois arquivos chamados rolling-options.js e log-level.js. Dentro do diretório config, crie dois arquivos chamados rolling-config.js e log-config.js.

Finalmente, crie um arquivo chamado logger.js na raiz do diretório, onde o index.js e o package.json estão localizados.

A estrutura dos seus diretórios deve ficar desse jeito:

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

Explicação

O arquivo logtar.js serve como o arquivo-chave que exporta todas as funcionalidades necessárias para o cliente.

O arquivo logger.js exporta a nossa classe Logger e algumas funcionalidades relacionadas.

O arquivo utils/rolling-options.js exporta nossas classes RollingSizeOptions e RollingTimeOptions.

O arquivo index.js contém apenas uma única linha de código:

module.exports = require('./lib/logtar');

Os outros arquivos exportam funcionalidades baseando-se nos seus nomes.

Nota: Se você não está trabalhando com TypeScript e está usando JavaScript puro, adquira o hábito de usar JSDoc o máximo que puder. Use para todos os argumentos de funções e tipos de retorno. Seja explícito. Isso pode te tomar muito tempo, mas será conveniente a longo prazo. Usar JSDoc vai tornar seu fluxo de trabalho muito mais fluido conforme o projeto for crescendo.

O index.js

Aqui está o conteúdo dentro do arquivo index.js

module.exports = require('./lib/logtar')

O arquivo lib/logtar.js

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,
};

O arquivo lib/logger.js

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

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

    /**
     * @returns {Logger} Uma nova instância do Logger com a configuração padrão.
     */
    static with_defaults() {
        return new Logger();
    }

    /**
     * 
     * @param {LogConfig} log_config 
     * @returns {Logger} Uma nova instância do Logger com a configuração fornecida.
     */
    static with_config(log_config) {
        return new Logger(log_config);
    }

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

    /**
     * @returns {LogLevel} Log level atual.
     */
    get level() {
        return this.#config.level;
    }
}

module.exports = { Logger };

O arquivo lib/config/log-config.js

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

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

class LogConfig {
    /**
     * @type {LogLevel}
     * @private
     * @description Log level a ser utilizado.
     */
    #level = LogLevel.Info;

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

    /**
     * @type {string}
     * @private
     * @description Prefixo a ser utilizado no nome do arquivo de log.
     *
     * Se o prefixo de arquivo for `MyFilePrefix_` os arquivos de log criados terão os nomes
     * `MyFilePrefix_2021-09-01.log`, `MyFilePrefix_2021-09-02.log` e assim por diante.
     */
    #file_prefix = "Logtar_";

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

    /**
     * @returns {LogConfig} Uma nova instância de LogConfig com os valores padrão.
     */
    static with_defaults() {
        return new LogConfig();
    }

    /**
     * @param {string} file_path Path para o arquivo de configuração.
     * @returns {LogConfig} Uma nova instância de LogConfig com os valores do arquivo de configuração.
     * @throws {Error} Se o file_path não for uma string.
     */
    static from_file(file_path) {
        const file_contents = fs.readFileSync(file_path);
        return LogConfig.from_json(JSON.parse(file_contents));
    }

    /**
     * @param {Object} json O objeto json a ser parseado em {LogConfig}.
     * @returns {LogConfig} Uma nova instância de LogConfig com valores do objeto json.
     */
    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 Log config a ser validada.
     * @throws {Error} Se o log_config não for uma instância de 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} Log level atual.
     */
    get level() {
        return this.#level;
    }

    /**
     * @param {LogLevel} log_level Log level a ser definido.
     * @returns {LogConfig} Instância atual de LogConfig.
     * @throws {Error} Se o log_level não for uma instância de LogLevel.
     */
    with_log_level(log_level) {
        LogLevel.assert(log_level);
        this.#level = log_level;
        return this;
    }

    /**
     * @returns {RollingConfig} Configuração de rotação atual.
     */
    get rolling_config() {
        return this.#rolling_config;
    }

    /**
     * @param {RollingConfig} config Configuração de rotação a ser definida.
     * @returns {LogConfig} Instância atual de LogConfig.
     * @throws {Error} Se o config não for uma instância de RollingConfig.
     */
    with_rolling_config(config) {
        this.#rolling_config = RollingConfig.from_json(config);
        return this;
    }

    /**
     * @returns {String} Tamanho máximo de arquivo atual.
     */
    get file_prefix() {
        return this.#file_prefix;
    }

    /**
     * @param {string} file_prefix O prefixo de arquivo a ser definido.
     * @returns {LogConfig} Instância atual de LogConfig.
     * @throws {Error} Se o file_prefix não for uma 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 };

O arquivo lib/config/rolling-config.js

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

class RollingConfig {
    /**
     * Rotaciona/Cria um novo arquivo toda vez que o tamanho do arquivo atual excede o limite em `segundos`.
     *
     * @type {RollingTimeOptions}
     * @private
     *
     */
    #time_threshold = RollingTimeOptions.Hourly;

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

    /**
     * @returns {RollingConfig} Uma nova instância de RollingConfig como os valores padrão.
     */
    static with_defaults() {
        return new RollingConfig();
    }

    /**
     * @param {number} size_threshold Rotaciona/Cria um novo arquivo toda vez que o tamanho do arquivo atual excede o limite.
     * @returns {RollingConfig} Instância atual de RollingConfig.
     */
    with_size_threshold(size_threshold) {
        RollingSizeOptions.assert(size_threshold);
        this.#size_threshold = size_threshold;
        return this;
    }

    /**
     * @param {time_threshold} time_threshold Rotaciona/Cria um novo arquivo toda vez que o tamanho do arquivo atual excede o limite.
     * @returns {RollingConfig} Instância atual de RollingConfig.
     * @throws {Error} Se o time_threshold não for uma instância de RollingTimeOptions.
     */
    with_time_threshold(time_threshold) {
        RollingTimeOptions.assert(time_threshold);
        this.#time_threshold = time_threshold;
        return this;
    }

    /**
     * @param {Object} json O objeto json a ser parseado em {RollingConfig}.
     * @returns {RollingConfig} Uma nova instância de RollingConfig com os valores do objeto json.
     * @throws {Error} Se o json não for um objeto.
     */
    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 };

O arquivo lib/utils/log-level.js

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 };

A classe lib/utils/rolling-options.js

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; // A cada 60 segundos
    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,
};

Viu como ainda podemos nos beneficiar do forte preenchimento de tipos do jsdoc para nossas classes, mesmo que elas existam em arquivos diferentes? Isso não é possível utilizando JavaScript regular - todos os créditos vão para o jsdoc.

O código de todo capítulo pode ser encontrado no diretório code/chapter_04.1

Read Next