Multiple Conditionals Clean Way
Make conditionals of any size readable and manageable.
For more about conditionals code smells and refactorings refer to Venables R., Refactoring Workbook (2003), ch. 7, Fowler M.,Refactoring (1999, 2018), ch. 10, Martin R., Clean Code (2008), G28,29,33, Kerievsky J., Refactoring to Patterns (2004), ch. 4, Conditional Complexity.
Problem
Note the context we are talking.
There are always many conditionals asking to be implemented in code. But beyond the simplest cases (noted below) conditionals quickly become unreadable and unmanageable. Here is the starting point demonstrating how to transform these into the effective constructs.
Desired Behavior: Detect if the provided given
argument is among the allowed values;
Inputs: given
: a string or an array of strings to check, allowed
: the value(s) to check against;
Outputs: boolean, true
;
Goal: treat multiple allowed
argument values cleanly (no if
s.), hide conditions complexity;
Solution
From consumer standpoint the solution encapsulates 1 the conditional behavior behind a meaningfully named method. From the implementation details standpoint the conditionals are simplified 2 into a short flat structure.
/**
* Check if a `given` value is allowed.
*
* @param {string | string[]} given The value(s) to check.
* @param {string | string[] | | Record<string, unknown>} allowed The allowed value(s) to compare against.
* Can be a TypeScript enum
*
* @returns {boolean} - Returns true if all `given` are among the allowed values, otherwise false.
*/
public static isAllowed(given: string | string[], allowed: string | string[] | Record<string, unknown>): boolean {
/**
* Normalize `given` to array.
* Could use here and further isString from Lodash if already used in a project.
*/
const givens = Array.isArray(given) ? given : [given];
/**
* Avoiding conditionals to normalize `allowed` argument string, string array or enum to array of strings input.
*/
const allowedArgValues = [
typeof allowed === 'string' ? [allowed] : null,
typeof allowed === 'object' ? Object.values(allowed as Record<string, unknown>) : null
];
// Finally get the `allowed` values.
const _allowed = allowedArgValues.find((value: string[] | null) => { return value; }) as string[];
// Replace conditionals.
return givens.every((given: string) => { return _allowed.includes(given); });
}
Benefits
-
No
if
's, expressive, scalable, readable, straightforward extensible for moreallowed
argument values still keeping readability; -
Extendable by 1) extracting
allowed
to the object property (e.g.this.allowed
) or 2) application-wide constant or enum (e.); -
Further extendable - the baby version of Strategy pattern 3 - as logic in
allowedArgValues
items grows, introduce on-demand dedicated strategy methods, then classes;
Implied SWE Principles
- Polymorphism:
given
conveniently accepts 2 types,allowed
3 types (arrays and enums - objects are objects in JS). - SRP: encapsulate the decision on argument transformation in independent
allowedArgValues
elements; - SoC: encapsulate the final argument value retrieved in
.find
one-liner function; - Open/Closed Principle: easily add more types for
allowed
argument values with no need for other modifications in the method; - Delegation, SRP, encapsulation:
_givens
helper, that can be extended to further prepare for comparisongivens
(e.g. normalize to upper case); - Declarative Approach: the method name, intermediary variables usage and the ir naming, use of
.find
,.every
,.includes
.
Important on if
s
- Single
if
is acceptable on the single early return; - Two have to be written as ternary;
- 3+
if
s: be the above solution at start and following towards (not necessarily to) Strategy pattern;
Footnotes
-
Martin R., Clean Code (2008), p. 301, "G28: Encapsulate Conditionals". ↩
-
Fowler M., Refactoring (2018), p. 239, "Simplifying Conditionals Logic", Kerievsky J., Refactoring to Patterns (2004), p. 84, "Conditional Complexity". ↩
-
Kerievsky J., Refactoring to Patterns (2004), p. 169, "Replace Conditional Logic with Strategy". ↩