3
\$\begingroup\$

Looking for a critique of this simple logger class I'm working on in my transition to TypeScript.

The purpose of the logging class is to just print to a console either info, warn or error messages.

The main things I'm after feedback on are my usage of the TypeScript type system. I.e interfaces/types/whatever else could be done different or better.

interface LoggerConfig {
  prefix?: string;
  prefixColor?: TextColor.White;
  displayConsole?: boolean;
  outputExternal?: boolean;
}

type LogFn = (msg: string) => void;

interface Log {
  config: LoggerConfig;
  log: (msg: string, logColor: TextColor, override?: LoggerConfig) => void;
  info: LogFn;
  warn: LogFn;
  error: LogFn;
}

class Logger implements Log {
  config: LoggerConfig = {
    prefix: 'console :: ',
    prefixColor: TextColor.Green,
    displayConsole: false,
    outputExternal: false,
  };

  constructor(userConfig: LoggerConfig = {}) {
    this.config = {...this.config, ...userConfig};
  }

  log(msg: string, logColor: TextColor, override?: LoggerConfig) {
    const config = override ? {...this.config, ...override} : this.config;
    const {displayConsole, outputExternal, prefix, prefixColor} = config;

    if (displayConsole) {
      showConsole();
    }

    if (outputExternal) {
      external.printToConsole(`${prefix} ${msg}`);
    }

    print(prefixColor + prefix + logColor + ' ' + msg);
  }

  info(msg: string) {
    this.log(msg, TextColor.White);
  }

  warn(msg: string) {
    this.log(msg, TextColor.Yellow, {displayConsole: true});
  }

  error(msg: string) {
    this.log(msg, TextColor.Red, {displayConsole: true, outputExternal: true});
  }
}

const logger = new Logger({prefix: 'testLogger :: '})
\$\endgroup\$
1
  • \$\begingroup\$ It would help to see the definitions of showConsole, external.printToConsole and print. Right now it's difficult to say what the behavior of the logger will be. \$\endgroup\$
    – Gerrit0
    Dec 31, 2018 at 4:06

1 Answer 1

1
\$\begingroup\$

I know this is an old question, but here are some thoughts on how this implementation could become more maintainable and extensible.

Summary

The suggestions made are in order to make the implementation more maintainable and extensible. Some aspects of the actual implementation details have been omitted as the point I'm trying to make is more towards the software design and architecture perspectives.

You can find the implementation of the suggestions on KATA - Logger Feedback


Modelling

Based on the initial problem definition we can model the implementation into the below segragated parts:

Models:

  • LoggerConfig: Model definition of the configuration that a logger can accept.
  • LogTypeDto: Types of logging messages that a logger can output (e.g., INFO, WARNING, ERROR).

Loggers:
We want the logging functionality to be extensible for the future without having to alter any existing implementation, supporting the Open-Closed Principle. In order to achieve this we can define a BaseLogger contract that all the resulting concrete implementations of loggers need to follow, i.e., the ConsoleLogger.

This is translated as follows:

import { LoggerConfig } from "../Models/LoggerConfig";
import { LogType } from "../Models/LogTypeDto";

/// Defines the base contract for all the loggers. Offers base operations to define the logger's functionality
/// and parameterization.
export abstract class BaseLogger {

    /// Base constructor that defines the logger's configuration.
    constructor(private config: LoggerConfig){};

    /// Base contract for logging based on the provided severity.
    public abstract log(severity: LogType, message: string): void;
}
import { BaseLogger } from "../Architecture/BaseLogger";
import { LogType } from "../Models/LogTypeDto";

/// Extension of the BaseLogger to allow the Client application output to the console
/// specific messages based on severity.
export class ConsoleLogger extends BaseLogger {

    public override log(severity: LogType, message: string): void {
        switch (severity) {
            case LogType.INFO:
                console.info(message);
                break;
            case LogType.WARNING:
                console.warn(message);
                break;
            case LogType.ERROR:
                console.error(message);
                break;
            default:
                console.log(message);
                break;
        }
    }
}

By following this approach, we're supporting the OCP, making our application easily extensible and eliminating the risks of ripple effects due to changes on the current functionality.

Factory

In order to make our client code even more decoupled from the specifics of which logger to be used, we can use the Factory Method pattern.

This pattern allows us to completely decouple the client code from the specifics of the object creation.

Base Logger Creator In order to achieve the decoupling and abide by the pattern's specification, we're going to use the BaseLoggerCreator as the contract for all the concrete logger creators. That is:

import { BaseLogger } from "../Architecture/BaseLogger";

/// Base creator which is part of the Factory Method pattern for creating different type of Loggers.
/// To create a specific type 
export abstract class BaseLoggerCreator {
    /// Contract definition for the concrete creators.
    public abstract create(): BaseLogger;

    /// Implements the default logic for creating a base logger, aka will always create a default logger.
    public createDefaultLogger(): BaseLogger {
        let defaultLogger: BaseLogger = this.create();
        return defaultLogger;
    }
}

Note that this base logger can also be responsible for which configuration to be applied on the default logger and can provide functionality that will choose the creation of a specific logger through some configuration.

The concrete creator of a console logger would look like:

import { BaseLogger } from "../Architecture/BaseLogger";
import { ConsoleLogger } from "../Loggers/ConsoleLogger";
import { LoggerConfig } from "../Models/LoggerConfig";
import { BaseLoggerCreator } from "./BaseLoggerCreator";

/// Responsible for constructing a concrete console logger along with the appropriate
/// logger configuration that needs to be applied.
export class ConsoleLoggerCreator extends BaseLoggerCreator {

    public create(): BaseLogger {
        return new ConsoleLogger(new LoggerConfig());
    }
}

Conclusion

Now, our client code is completely decoupled by the specifics of object creation and doesn't depend upon any concrete object.

It would be very easy to accomodate any new requirements with agility, while providing backwards compatibility with minimum effort and minimizing the ripple effect.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.