OO vs FP console log example

A common feature implemented in object-oriented and functional styles.

Here is an example showing how we can accomplish a lot using functional traits available in the JavaScript language. I first will show the problem, then will show a typical object-oriented solution, then will remove all the extra code using the functional approach.

Example

Let us start with a simple program: add two numbers and print the result

1
2
3
4
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5

Everything is working fine, and we decide to add logging to the add function. Maybe we want to run this function on the server, or help debugging in case there is an unexpected result. We can add console.log statement to the function itself

1
2
3
4
5
6
7
function add(a, b) {
console.log('a', a, 'b', b);
return a + b;
}
console.log(add(2, 3));
// a 2 b 3
// 5

This is fine, but it pollutes the standard output stream with lots of noise. We really only want to turn the argument logging inside the add function sometimes, leaving it off by default. We could pass a flag into the function

1
2
3
4
5
6
7
function add(a, b, verbose) {
if (verbose) {
console.log('a', a, 'b', b);
}
return a + b;
}
console.log(add(2, 3)); // 5

This is a bad solution

  • Extra if statement increases the complexity of the function
  • Passing extra parameters to the function makes the function's signature a mess
  • add function mixes addition and logging aspects

Object-oriented solution

Let us move the logging logic (verbosity) into a singleton object Log. Any function that desires to print, can just log the message via this singleton. The Log object can then decide if the logging is below the threshold and silently ignore it. Otherwise it will print it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Log = {
debugMode: false,
log: function (verbose) {
// print every argument except the 'verbose' flag
var args = Array.prototype.slice.call(arguments, 1);
if (verbose) {
if (this.debugMode) {
console.log.apply(console, args);
}
} else {
console.log.apply(console, args);
}
}
};
Log.debugMode = true;
function add(a, b, verbose) {
Log.log(true, 'a', a, 'b', b);
return a + b;
}
Log.log(false, add(2, 3));
// a 2 b 3
// 5

There is nothing wrong with this approach. But I personally avoid it for several reasons.

  • It is verbose
  • The internal state (debugMode variable) is public and can change at any moment. For example, a rogue function can change it before printing and forget to restore it

    1
    2
    3
    4
    5
    6
    7
    8
    function add(a, b) {
    Log.debugMode = true;
    Log.log(true, 'a', a, 'b', b);
    return a + b;
    }
    Log.debugMode = false;
    add(2, 3);
    // debug messages are printed from now on!
  • When passing Log.log method around we must be careful to bind it to the Log instance.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var Log = {
    ...
    debug: function () {
    var args = Array.prototype.slice.call(arguments, 0);
    args.shift(true);
    this.log.apply(this, args);
    }
    };
    [1, 2, 3].forEach(Log.debug);
    // error - `this` inside Log.debug does not point to Log
  • In order to log something you need to read Log object's documentation to see how the state needs to be setup before the log() call.

Functional approach

Let us make a single function with first arguments being debug mode and message verbosity. The rest of the arguments are values we want to print to the console. This function thus has all the information to decide how to behave.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function log(debugMode, verbose/*, and print arguments */) {
if (verbose && debugMode || !verbose) {
var args = Array.prototype.slice.call(arguments, 2);
console.log.apply(console, args);
}
}
log(false, false, 'this is normal message in prod mode');
log(true, false, 'this is normal message in debug mode');
log(false, true, 'this is verbose message in prod mode');
log(true, true, 'this is verbose message in debug mode');
// output
/*
this is normal message in prod mode
this is normal message in debug mode
this is verbose message in debug mode
*/

Now we can create the print function to be used in the program by partially applying log function. This makes the debugMode state variable essentially private.

1
2
3
4
5
6
7
8
var print = log.bind(null, true);
function add(a, b, verbose) {
print(true, 'a', a, 'b', b);
return a + b;
}
print(false, add(2, 3));
// a 2 b 3
// 5

We can easily use print function as iterator callbacks, even binding verbose argument.

1
2
3
4
[1, 2, 3].forEach(print.bind(null, false));
// 1 ...
// 2 ...
// 3 ...

Not only the debugMode inside the print function is private to the function, the print reference itself is immutable. If we want to switch the debugMode from true to false we need to create a new print reference

1
2
3
4
5
6
7
8
9
10
11
var print = log.bind(null, true);
function add(a, b, verbose) {
print(true, 'a', a, 'b', b);
return a + b;
}
print(false, add(2, 3));
print = log.bind(null, false);
print(false, add(2, 4));
// a 2 b 3
// 5
// 6

This feature is not as important in this example, but data immutability makes reasoning about the data flow easier, see immutable example.

Because we fix the internal state of the log function left to right using built-in Function.prototype.bind method, we need to pay careful attention to the argument order when designing the log function. I advise to put the arguments least likely to change first. Otherwise, you would need to use 3rd party library to perform selective partial application.

We can make the logging function robust by defending it against invalid inputs. For example, using check-more-types/defend method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var check = require('check-more-types');
function _log(debugMode, verbose) {
if (verbose && debugMode || !verbose) {
var args = Array.prototype.slice.call(arguments, 2);
console.log.apply(console, args);
}
}
var log = check.defend(_log,
check.bool, 'debug mode should be boolean flag',
check.bool, 'message verbosity should be boolean flag');
log(false, false, 'valid flags');
// "valid flags"
log(false, 'verbose', 'string instead of boolean');
// Error: Argument 2: verbose does not pass predicate: message verbosity should be boolean flag

Related: