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.
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:
- 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.
- 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.
- 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.
- 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.)
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
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. UsarJSDoc
vai tornar seu fluxo de trabalho muito mais fluido conforme o projeto for crescendo.
Aqui está o conteúdo dentro do arquivo index.js
module.exports = require('./lib/logtar')
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,
};
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 };
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 };
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 };
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 };
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