Wednesday, 26 June 2019

Software Design

This a collection of both online references and some my thoughts related to Software Design and Development in general.

Main Principles

Code should be correct, clear and efficient.
Prefer simple. Avoid clever.
(taken from

Make MVP working (correctly) first. Release it and analyze feedback. Revenue, not the beauty of the code should drive development...but code should be well-designed if TDD is followed. Refactor evolutionary, not revolutionary.

Software design
SOLID Principles

12 Factor Applications 
Twelve-Factor Apps In node.js
Twelve-Factor App methodology

TDD - Test-Driven Development

I feel comfortable when replacing one implementation of the function with another only if that function is covered by unit tests.

POD - Performance-Oriented Development

Software Product Architecture


Break up monolithic application into plugin-based one. Benefits:
  • update single plugin instead of entire application
  • bug in single plugin does not affect other plugins or entire application
  • some plugins can be free, some can be sold

Metric for Good Source Code

  • how easy (or difficult) is to:
    • navigate the code
    • isolate the code
  • naming (of packages, namespaces, functions, classes, structs, variables etc...)
  • TBC...

Command-line Arguments

  • To indicate optional arguments, Square brackets are commonly used, and can also be used to group parameters that must be specified together.
  • To indicate required arguments, Angled brackets are commonly used, following the same grouping conventions as square brackets.
  • Exclusive parameters can be indicated by separating them with vertical bars within groups.

Argument passing strategy - environment variables vs. command line

Logical Expressions

Use Boolean Algebra laws to simplify complex conditions (logical expressions).

Global Variables

They should be avoided unless they are static/singletons that represent an object with cross-cutting concern functionality.

Global Variables Are Bad


Functions should be simple, short and follow SRP principle. E.g. if function has to create a file at some path, don't make it also creating that path (if path does not exist). Create another function which is responsible ONLY for creating paths instead.

If function has to perform a task depending on the value of its argument, move that argument check out of the function - function should do only that task, not to verify whether that task should be performed or not.


func processAttachment(attachment attachment, ...) error {
  if attachment.MimeType != "application/x-msdownload" {
return nil
    // do here something with attachment

should be:

if attachment.MimeType == "application/x-msdownload" {
  err := processAttachment(attachment)

This is more explicit and function is responsible for one thing only.

Don't make library/package functions asynchronous by default - allow users to choose how they want to consume them - synchronously or asynchronously. They can always create async wrapper around them.

The same stands for functions in Go. We could make them accept sync.WaitGroup argument so they can be awaited...but we should make function only do its main job as fiddling with wait group pollutes function's main functionality and thus break SRP.

func foo(arg1 T1, arg2 T2, ...wg *sync.WaitGroup) {
   defer wg.Done()

In the same way, don't add logging to library/package functions. Return errors/throw exceptions with error messages/codes instead. User of the library should decide what they want to see in the log output.

If function has multiple parameters and e.g. one parameter is used only in one part of the function, check if this part of the function is doing a task (or...has responsibility for one "thing") that could be extracted into a separate function.

Indentation & Single Point of Return

There are two schools here. The one which recommends that each function should have single point of return and one that allows multiple points of return.

Single point of return:
  • if function is long this increases chances of having multiple levels of nested conditions
  • returned value (error) is assigned at multiple places and at multiple levels
  • it's difficult to track positive execution path
Multiple points of return:
  • prevents deep levels of indentation (such functions usually have only two)
  • it is easy to track which expression would make function to return which error
  • we can use indentation here to visually create positive and error paths: positive path of execution are expressions in the 1st indentation level. Handling errors is in the 2nd (indented) level of indentation (see Code: Align the happy path to the left edge)
Here are some more Tips for a good line of sight from Mat Ryer:
  • Align the happy path to the left; you should quickly be able to scan down one column to see the expected execution flow
  • Don’t hide happy path logic inside a nest of indented braces
  • Exit early from your function
  • Avoid else returns; consider flipping the if statement
  • Put the happy return statement as the very last line
  • Extract functions and methods to keep bodies small and readable
  • If you need big indented bodies, consider giving them their own function

How small function should be?

How small should functions be?
Small Functions considered Harmful
What should be the maximum length of a function?
How can wrapping an expression as a function be Clean Code?

Terminology: Parameters VS Arguments

Parameter is the variable which is part of the method's signature (method declaration).
An argument is an expression used when calling the method.

From MSDN: "...the procedure defines a parameter, and the calling code passes an argument to that parameter. You can think of the parameter as a parking space and the argument as an automobile." 

“Parameter” vs “Argument”

Arguments Validation

Arguments should be validated if their values are coming from the wild outside world and this happens in the public API. Contract validation frameworks can be used.

In private/internal functions we can use assertions (in C++/C#) or no validation at all and allow application to crash.

Structs & Classes

Prefer composition over inheritance.

For data members use their natural types, which are best describing them, rather than types used in their presentation. E.g.


type Directory struct {
   Created      time.Time
   LastModified time.Time

instead of

type Directory struct {
   Created      string
   LastModified string

It might be necessary at some point to perform some manipulation with these values e.g. comparisons, period calculations etc...for which an API is provided on Time type. Otherwise we'd first need to convert strings to Time, do manipulation and then convert result back to string.

Perform conversion to string at the point when time values have to be displayed in UI.


Fluent design of the API can make code human readable even more.


Don't use logging as a substitute for proper debugging. Logging is poor man's debugging. Learn how to use debugging and profiling tools relevant for your development stack and IDE.

Think carefully what will go into log. If there is no error, don't make log message like this:

Created symlink ./.../myapp--setup.exe --> myapp-setup.exe. Error: <nil>

When you later analyze log file and look for word "error", you'll get tons of false positives.

Working with Data

Data Access Object (DAO)


The older I am the less I like having documentation about the software I write anywhere else but in the source code itself. This reduces information redundancy, duplication and situations when documentation is not in sync with the implementation. Having brief but comprehensive comments and tools (example1) which can extract desired information from them should do the job. Having high unit test coverage (and BDD-style tests if you are kind to non-tech members of the team) should also help as reading test names should be as informative and as easy as reading a requirement specification.

Some Common Patterns and Anti-Patterns

MVC (Model - View - Conroller)

  • Model
    • processes data
  • View
    • shows the results
    • many dynamic languages generate data for views by writing code in static HTML files
  • Controller
    • handles user requests

Producer - Consumer

What is the benefit of writing to a temp location, And then copying it to the intended destination?


Resource Management

Resources (data, files, directories, handles etc...) should be managed in a secure manner. Security is based on three (CIA) principles:

  • set of rules that limits access to information

  • the assurance that the information is trustworthy and accurate
  • involves maintaining the consistency, accuracy, and trustworthiness

  • guarantee of reliable access to the information by authorized people

Unit Testing

Be explicit when using data structures for representing data

Array members should mean the same thing. If an element has a special meaning, move it our from array and make it a member field of some other data structure. Example:

const (
copyFuncName string = "copy"

// Function parses CLI arguments and returns a string slice:
//    first element = function name
//    subsequent elements = function arguments
func parseArgs() ([]string, error) {

...and later we have:

switch funcDetails[0] {
case copyFuncName:
err = copy(funcDetails[1], funcDetails[2])

The red flag here was:

switch funcDetails[0]


copy(funcDetails[1], funcDetails[2])

When you read it, you are wondering "What is the meaning of that funcDetails[i], what value does it contain?". This should be explicit, something like:

switch funcDetails.Name:

So, we can transform the array into a struct with two fields: Name (string) and Args (array).

type funcInfo struct {
name string
args []string

Our switch statement now requires less mental effort to read and understand:

switch {
case copyFuncName:
err = copy(funcInfo.args[0], funcInfo.args[1])


It is often useful to add some meta-data to data or services which are consumed by external clients. They help client management. 


If is easier to manage clients if data format or services (e.g. public API) they are consuming are versioned. If we bump version, clients can detect that and request new data or client software update.

No comments: