Skip to content

Instantly share code, notes, and snippets.

@danieluhl
Created September 8, 2021 00:11
Show Gist options
  • Save danieluhl/ebf16890c1c7d457bd272ea05cfe0255 to your computer and use it in GitHub Desktop.
Save danieluhl/ebf16890c1c7d457bd272ea05cfe0255 to your computer and use it in GitHub Desktop.
A Philosophy of Software Design Red Flags

from the book "A Philosophy of Software Design"

Why

High complexity code causes change amplification, high cognitive load, and more unknown unknowns, when working with a codebase. These are costly and plunge engineers into a kind of sadistic coding hell.

Complexity is caused by dependencies and obscurity. Code is obscure when information required to work with the code is not provided.

Complexity is most often incremental, it sneaks up on the best of us.

Working Code Isn't Enough

Tactical programming is getting something working often leaving a wake of destructive complexity for others (maintainers (heroes!)) to clean up. Companies often promote tactical programmers to the detriment of long-term code health and employee growth.

Strategic coding is thinking through the problem and system design first.

When thinking long-term, it always pays to have some upfront strategy to avoid long-term maintenance issues.

Modules Should be Deep

A shallow module has a complicated interface relative to the functionality it provides. They have a high cost to learning and using their interfaces. Beware small modules, they tend to be shallow.

Information Hiding

Information leakage occurs when the same knowledge is used in multiple places behind two APIs. For example two different classes that both understand the format of a particular format type would both need to be updated if the format is updated.

Temporal Decomposition (a type of information leakage) is when execution order is reflected in code structure: operations that happen at different times are in different methods or classes but use the same underlying knowledge (e.g. file format). Usually combining these methods or modules into one method or class eliminates the information leakage.

Overexposure is when users of an API are forced to learn about rarely used features. This increases the cognitive load. For example, having good defaults can eliminate overexposure.

General-purpose Modules are Deeper

The API of your modules should be "somewhat general-purpose". The underlying implementation should be just what your users need today. This aligns well with making modules deep.

General purpose APIs tend to lead to better information hiding.

Different Layer, Different Abstraction

Pass-through methods indicate that there is not clear division of responsibility between classes.

  • Expose the lower level class directly to the calling class (cut out the middle man)
  • Redistribute functionality between classes
  • Merge the classes

Decorators are a code smell because they often create pass-through methods. Before using decorators:

  • Add functionality directly to the underlying class
  • Add the functionality to an existing decorator (rather than make a new one)
  • Implement it as a stand-alone class instead

Interfaces and variable should also never be "pass-through".

Pull Complexity Downwards

Configuration parameters move complexity upwards instead of down.

Ideally each module solves a problem completely, configuration parameters mean an incomplete solution.

  • Will users be able to determine a better value than we can?
  • Can we provide reasonable defaults automatically?

Better Together or Better Apart?

Dividing modules results in:

  • Increase cognitive load keeping track of all the modules, more interfaces
  • Additional code to manage the components
  • More separation between parts of the system (harder to see what the system is doing all at once)
  • Duplication of code

Bringing things together can be beneficial if they are closely related:

  • They share information; e.g. depend on the same syntax or format of document
  • They are used together most of the time
  • They overlap conceptually
  • It's hard to understand one without the other

Bring things together if:

  • It will simplify the interface
  • It will eliminate duplication

Do separate general-purpose and special-purpose code.

Repetition if you see code repeated over and over you need a better abstraction.

Each method should do one thing and do it completely.

Conjoined Methods It should be possible to understand each method independently. Similarly, if any piece of code is separated such that to be able to understand one you must look at the other, this is a big red flag.

Define Errors out of Existence

Minimize the number of places where exceptions must be handled. In many cases operations can be modified so that the normal behavior handles all situations and there is no exceptional condition to report.

Exception Masking is when an exception is handled at the lowest possible level of the program so higher levels need not worry. This is common in distributed systems.

Exception Aggregation is when many exceptions are handled by one common piece of code. e.g. wrap the dispatcher rather than each individual implementation.

Excuses to not Write Docs

Good code is self-documenting no.

Optimizing the code to be easy to read is sometimes good, like good variable names, but other times bad, like breaking all methods into small pieces.

I don't have time to write comments Yes you do.

Good comments make a big difference in the maintainability of software, which pays for the initial investment quickly. Writing good comments shoudn't add more then 10% to your development time.

Comments get out of date and become misleading Then update them.

Large docs updates are only necessary when there are large code updates, which should be rare.

Comments I have seen are worthless; why bother Write good comments, not bad ones. This is not difficult once you know how.

Naming Things

Bad names cause bugs.

Vague Name: name is broad enough to refer to many different things, doesn't convey enough information and is likely to be misused.

Hard to Pick Name: If it's really difficult to pick a name that creates a clear image, it's a hint that the underlying object may not have a clean design.

Use consistent names. Use industry standards when possible.

Modifying Existing Code

If you're not making the design better, you're probably making it worse.

When you finish a change, the system should have the design you would have picked if you designed it from the start.

Consistency

  • names
  • coding style
  • interfaces
  • design patterns
  • invariants: a property of a variable or structure that is always true

Code Should be Obvious

Making code more obvious:

  • Good naming
  • Consistency
  • Judicious use of white space
  • Good comments

What makes code less obvious:

  • Event-driven programming (minimize this and document well)
  • Generic containers
  • Different types for declaration and allocation
  • Code that violates reader expectations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment