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 ifs.), 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 moreallowedargument values still keeping readability; -
Extendable by 1) extracting
allowedto the object property (e.g.this.allowed) or 2) application-wide constant or enum; -
Further extendable - the baby version of Strategy pattern 3 - as logic in
allowedArgValuesitems grows, introduce on-demand dedicated strategy methods, then classes;
Implied SWE Principles
- Polymorphism:
givenconveniently accepts 2 types,allowed3 types (arrays and enums - objects are objects in JS). - SRP: encapsulate the decision on argument transformation in independent
allowedArgValueselements; - SoC: encapsulate the final argument value retrieved in
.findone-liner function; - Open/Closed Principle: easily add more types for
allowedargument values with no need for other modifications in the method; - Delegation, SRP, encapsulation:
_givenshelper, 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 Conditionals
- Single
ifis acceptable on the single early return; - Two have to be written as ternary;
- 3+
ifs: be the above solution at start and following towards (not necessarily to) Strategy pattern; switch/case: God forbid :)
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". ↩