Principals of Software Design

These principals are not rules, but rather things to consider. They always have value, but that value may not exceed the value of other considerations: design choices are always a trade-off. Consider the principals listed here when making such trade-offs.

Decision/Policy Encapsulation

When you make a design decision, the set of things effected by it should be minimal. This applies across various scales, but it more important at larger scales. The idea is that decisions are things that can change, so you should limit the amount of work needed to change them.

At the small scale (say a single file class) its better if fewer parts are effected by a particular decision. Meaning if you make one implementation decision, its best if that's orthogonal from the others. (Ex: when choosing to store some private data in a list or array, its better if most of the stuff using it doesn't care). At a larger scale, its even more important: minimize exposing decisions through abstraction boundaries, especially if there may be many users (AKA: don't expose implementation details).

The thing you decide on should be implemented in exactly one place. For example if you decide to encode some data in a particular way (say format an address as a normalized URL), this decision/policy should be implemented in exactly one place. Thus changing the decision, or bug fixing it or documenting it or testing it requires changing one place.

Also, the few places that do depend on a decision (and this is more important the more such places there are) should be easily fixable if its changed: meaning changing the decision will make them not compile (ideal) or at least fail tests.

If you take a couple of flags into a method, and its possible this set of parameters might change, call it from as few places as possible, and make sure to design the API so changing the set of arguments or their order will cause a compile error. Strongly typed enums are good for this. Lots of int or bool parameters are very bad for this. Structs containing the various settings with good (according to the same rules) constructors or factories are also an option.

Value Types

When possible, follow the rules of "Value Types" and document what they are modeling and what are the Salient Attributes and Methods.

TODO: details

Nice explanation of Value Types:

Nice explanation of how they enable testing and simplify lot of things:

Prefer Immutability

Limit the number of ways a value can be set, ideally to 1 (Which means on construction and this its immutable). Immutability makes things thread safe, and avoids having to copy them. It makes debugging and testing easy as well. If something needs to be mutable, limit what can mutate it, and try to make as many of its components immutable as possible.

This also makes validation much easier (you can validate immutable object on construction, and know they will always be valid)

Use semantic immutability, even it the implementation must be mutable

When possible, make object appear immutable to users of them. For example, if some value is expensive to compute and you want it to be lazily populated, hide the fact that it is lazily populated from users of the object (and be thread safe about it!). This encapsulates the mutability.

Partition mutability

When a object has multiple mutable fields, separate them where possible: ex if you have 2 lazy fields you can often logically think of them as sub objects, then apply the other approaches to each part separately.

When an object must appear stateful / mutable, model it as a state machine

First be sure to apply partitioning as above (it reduces the number of states).

If users of an object must be able to observe changes, make sure the object atomically transitions from one state to the next (and each state is well defined). This can be cleanly implemented by holding a lock during the state transition (which is the only time you mutate), and explicitly validating any invariants you have (at least in debug builds) before releasing the lock and leaving the object in a new state. Document these transitions (what causes them, what they mean, and what the invariants are).

Batch mutator and reader

To enable making many changes to an object (ex appending many items to a list it owns) efficiently, the RAII pattern can be used to get a mutator object that hols a lock protecting the fields it permits mutating. This is often easily implemented as an inner class. It can be combined with a reader writer lock to allow use with batch readers as well of there is a need for them. This is semantically the same as acquiring the lock, then only mutating while holding it, but it adds compiler enforcement.

Open Closed Principal

"software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification" -

Don't allow polymorphism when its not useful

OOP example: Don't make two types share a base type if no one is ever going to have a variable that can be either one of these types and possible other types as well. Consider composition instead in those cases if you want to share implementation logic.

Related: Make everything sealed/final/non-virtual unless you have a reason not to. This prevents people from later violating this for no good reason, and helps enforce the "closed" part of the open closed principal. This also prevents you from breaking code that overrides methods and sub-classes in the future if you decide to forbid that or change your implementations.

Fail Fast

If there is an error that indicates a but in the application, the error should be raised as soon as possible. Absolutely do not try and design code to silently tolerate such errors.

Make API's impossible to use Wrong, or at least intuitive to use correctly:

If it can be used wrong, someone will do it. In that case, if possible, fail fast (compile time is fastest, but not always possible).

Write error safe code

Copyright © 2011-2013 Craig Macomber