Skip to content

Instantly share code, notes, and snippets.

@ProLoser
Last active January 17, 2018 00:45
Show Gist options
  • Save ProLoser/e63029e026480004d242699aa02da66c to your computer and use it in GitHub Desktop.
Save ProLoser/e63029e026480004d242699aa02da66c to your computer and use it in GitHub Desktop.
Code Complexity

Code Complexity Anti-Patterns

This document is designed to illustrate different examples of code complexity I've encountered and potential solutions to combat it. Code complexity can keep changing through the lifecycle of code, but one of my primary goals I try to keep in mind above all else is refactorability. No matter what, code will eventually become dated and need refactoring. Documentation, organization, cleanliness, unit tests are all aspects that help contribute to refactorable code. While quick prototypes and one-time use codebases will typically not be as concerned with code quality and complexity, long-lasting applications and libraries should be mindful of some of these anti patterns and work to reduce them when possible.

All of these are my own opinion and should not be treated as rules, just as suggestions.

In alphabetical order:

Class Abuse

Making everything classes when classes are not necessary. Poltergeist, or Objects whose sole purpose is to pass information to another object, is an example of Class Abuse

Codesmells: Classes that don't have any methods, usually with all of the logic in the constructor.

Copy-Pasta

The well-known pattern of copy-pasting and slightly tweaking logic instead of refactoring the original logic to be more versatile. Results in a bloated codebase and bugs.

Deep Stack Traces

Having a large amount of nested function calls isn't in-of-itself a problem, but many anti-patterns tend to be codesmelled by really deep call chains. I frequently refer to reducing the depth of a callstack as flattening a function.

DSL Abuse

Related: Over-Specificity

The acronym DSL (Domain Specific Language) is actually an example of DSL abuse. When acronyms and terms are heavily used throughout a codebase without any explanation in the code or documentation. Instead of using more generic terms.

Symptoms:

  • Slow onboarding of new team members
  • Code is not self-documenting

Codesmells: Heavy use of acronyms and obscure terms in functions, variables and documentation

Examples:

Solutions: Avoid use of acronyms in code.

Error Hiding

Error hiding comes up a lot when bad data is ignored by functions instead of surfacing valuable error information or sensible defaults. This can lead to bugs cropping up when most functions in a system continue quietly chugging along when the data they are receiving is malformed or missing. Creating key failure points in a system that surface some sort of error or definitive notification is imperitive to avoid bugs like blank screens and broken application states as well as detecting unforseen data states.

If there are concerns with application failure due to unhandled errors, build global error handling solutions so that production applications can quietly report errors and surface useful alternatives or messages to the user. In development, these global handlers can be disabled to help developers debug bad data.

Functional Abuse

The opposite of Class Abuse, this is when functional programming is preferred when a class may make more sense. This is can occur when multiple functions act upon the several bodies of data, but a contract is not enforced.

Codesmells:

  • Multiple distinct functions with a large number of relatively identical arguments.
  • Multiple functions changing the data in-place.
  • Multiple functions changing (relatively) global data.

God Objects

God Objects or Classes are monolithic bodies of logic that try to do or contain too much.

Codesmells: Unusually massive files / classes / functions / objects

One-Off Functions

Related: Deep Stack Traces, Overly Specific functions / variables, Over-Obfuscation

These are typically 1 or 2 line functions that are only used in one place in the entire codebase. This tends to occur when a line of logic is particularly complex and the impression of simplification is given by relocating the logic to a smaller function that is then called by the original function.

The problem is these functions are frequently completely useless in any other context, either not versatile, or just not necessary anywhere else in a codebase. This can obfuscate complexity

Codesmells: Tiny, specific functions used in only one location

Examples:

function doSomething(text) {
  if (checkFirstCharacterForDigits(text)) {
    // ...
  }
}
function checkFirstCharacterForDigits(text) {
  return /^\d/.test(text);
}

Solutions: Usually the best solution is to simply hoist the logic to the caller function. I also refer to this as flattening the function.

function doSomething(text) {
  // Check first character for digits
  if (/^\d/.test(text)) {
    // ...
  }
}

Over-Obfuscation

This is a general term for logic that gets hidden away by functions and classes without any real added benefit. Typically leads to more complicated debugging and onboarding of new developers. See other anti-patterns for specific examples.

Over-Specificity

Related: Copy-Pasta

This means that developers will be less inclined to reuse the logic for slightly different purposes even though the internal logic is the same. Junior developers will frequently copy-paste this logic, adjusting the names for different variations rather than refactoring the logic.

Codesmells: Verbose or extremely specific function/variable names.

Pass-Thru Data

This is when a function or class or other body of logic receives variables or data that it doesn't use directly but is used by a passing to a child / callee function.

Examples:

function first(a, b, c) {
  // `outer` logic doesn't use `b`
  save(a, c);

  second(b);
}

function second(b) {
  // `inner` logic uses `b`
  save(b);
}

Solutions:

function container() {
  
}
function first(a, c) {
  save(a, c);
}

function second(b) {
  // `inner` logic uses `b`
  save(b);
}

Tailing Abuse

A tail call is when you call (and usually return) another function at the end of a function. While normally this can be optimized by the compiler/runtime, using tail calls to trigger unrelated logic or to chain additional logic adds depth This is when a function or class or other body of logic receives variables or data that it doesn't use directly but is used by a passing to a child / callee function.

Examples:

function first(data) {
  // ...
  doSomething(data)
  // ...
  second(data);
}

function second(data) {
  // ...
  doSomethingElse(data);
  // ...
}

Solutions:

// aka: dispatcher, container
function controller(data) {
  /**
   * Hoisting all chained calls to one function makes the logic path clear without having to dive deeper and deeper into the code. We can debug and refactor calls to functions more easily without breaking the chain.
   */
  first(data);
  second(data);
}
function first(data) {
  // ...
  doSomething(data);
  // ...
}

function second(data) {
  // ...
  doSomethingElse(data);
  // ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment