Often during the course of my day I come across code which evaluates the same conditional comparisons in multiple contexts. Understandably, this is rather typical of most software systems, and while it may only introduce a negligible amount of technical dept (in the form of redundancy) for smaller systems, that dept can grow considerably in more complex, large scale applications. From a design perspective, this issue is applicable to nearly every language.
For example, consider a simple Compass
class which defines just one public property, “direction” and, four constants representing each cardinal direction: North, East, South and West, respectively. In JavaScript, this could be defined simply as follows:
1 2 3 4 5 6 7 8 9 | var Compass = function(direction){ this.direction = direction; } Compass.NORTH = "North"; Compass.EAST = "East"; Compass.SOUTH = "South"; Compass.WEST = "West"; |
Technically, there is nothing problematic with the above class signature; the defined constants certainly provide a much better design than conditional comparisons against literal strings throughout implementation code. That being said, this design does lead to redundancy as every instance of Compass
which needs to evaluate the state of direction
requires conditional comparisons.
For example, to test for Compass.North
, typically, client code must be implemented as follows:
1 2 3 4 5 | if (compass.direction === Compass.NORTH) { //... } |
Likewise, simular comparisons would need to be implemented for each cardinal direction. And, while this may seem trivial for a class as simple as the Compass
example, it does become a maintenance issue for more complex implementations.
With this in mind, we can simplify client code by defining each state as a specific method of Compass
. In doing so, we afford our code the benefit of exercising (unit testing) Compass
exclusively. This alone improves maintainability while also simplifying client code which depends on Compass
. As such, Compass
could be refactored to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var Compass = function(direction){ this.direction = direction; } Compass.prototype = { isNorth: function() { return this.direction === Compass.NORTH; }, isWest: function() { return this.direction === Compass.WEST; }, isSouth: function() { return this.direction === Compass.SOUTH; }, isEast: function() { return this.direction === Compass.EAST; } } |
Based on the above implementation of Compass
, the previous conditional comparison can be refactored as follows:
1 2 3 4 5 | if (compass.isNorth()) { //... } |
Comparator API
To simplify implementing conditional comparisons, I have provided a simple Comparator
API that defines a single static method: Comparator.each
, which allows for augmenting existing objects with comparison methods. Comparator.each
can be invoked with three arguments as follows:
type | |
The Class to which the comparison methods are to be added. | |
property | |
The property against which the comparisons are to be made. If the property has not been defined it, too, will be added. | |
values | |
An Array of constants where each value will be used to create a new comparison method (prefixed with “is”). If the constants specified are Strings, typically an Array containing each constant should suffice. For example, passing [Foo.BAR] where BAR equals “Bar” would result in an isBar() method being created. To specify custom comparison method names, an Object of name/value pairs can be used where each name defines the name of the method added and the value is the constant evaluated by the method. This is useful for constants which are not strings. For example, {isIOS421: DeviceVersion.IOS_4_2_1} where IOS_4_2_1 equals 4.2.1 would result in an isIOS421() method being created. |
Taking the Compass
example, the previous comparison methods could be augmented without the need to explicitly define them via Comparator.each
:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var Compass = function(){ this.direction = direction; } Compass.NORTH = "North"; Compass.EAST = "East"; Compass.SOUTH = "South"; Compass.WEST = "West"; Comparator.each( Compass, "direction", [Compass.NORTH, Compass.WEST, Compass.SOUTH, Compass.EAST] ); |
The above results in the comparison methods isNorth
, isEast
, isSouth
and isWest
being added to the Compass
type.