Software Design Philosophy Overview

Software Design Principles

In general, a principle is a fundamental idea that serves as the basis of belief, behavior, or reasoning. A software principle is a high-level guideline that serves as the foundation for good software coding behaviors. Some of them can even be worded in commandment form: “Thou shalt not repeat thyself.”

Software design principles can be thought of general best-practices for approaching the act of actually writing code, and can be applied broadly to virtually any coding scenario. Let’s take a look at the term SOLID, which is an acronym for five distinct principles.

The Single Responsibility Principle (SRP) states that a software module should have exactly one responsibility. That is to say, any piece of a software system should do one thing, and only one thing. This helps to increase software cohesion by ensuring that unrelated functionality isn’t jumbled up together in an unintelligible mess (such as a God Object, which is an anti-pattern). It can also reduce the size of a software module simply by limiting its available scope. After all, the less a class or method does, the shorter it will be, and the easier it will be to understand it.

The Open-Closed Principle (OCP) states that a software module should be open for extension, but closed for modification. Some over-thinkers might interpret this to mean that, once shipped, code should be considered immutable and one should only ever add functionality (such as wrapper or extension classes) instead of doing things like fixing bugs. But that’s not the case: developer should always fix their bugs. The OCP is actually referring to how inheritance works within object-oriented software. Specifically, it says that types should be easily extendable (such as by adding new properties or fields through a subclass) without having to modify the original code. One way of achieving this is to use polymorphic types that derive from some abstract base class: the base class has its own immutable core implementation, and a required interface that derived types must implement. In simplistic terms, the OCP tends frowns upon sealed (C#) or final (Java) classes, since that precludes extension.

The Liskov Substitution Principle (LSP) is probably the least well-understood of the SOLID design principles. In textbook terms, it states that any reference to an object within a software system should be replaceable with an instance of a sub-type of that object without affecting the correctness of the software system. Put much more concisely: derived types should not violate the contracts of their base types. If a base type doesn’t require methods to be called in any order, then a derived type shouldn’t (this is actually called Temporal Coupling, an anti-pattern to be avoided). If a method on a base type doesn’t throw a given exception, then overrides of that method in derived types should not that exception either. The Java programming language bakes this in at the compiler level with exceptions: methods must declare the exceptions they throw, and sub-types cannot throw exceptions not listed by the base type because they would be unexpected and uncaught by callers. The LSP also gives rise to some more complicated concepts in coding such as covariance and contravariance. C# bakes these into the language through the use of the “out” and “in” keywords (respectively) in generic interface declarations.

The poorly-named Interface Segregation Principle (ISP) is almost an extension of the SRP, and states that interfaces should be limited in scope that serve a cohesive purpose. In most object-oriented programming languages, concrete types are free to implement multiple interfaces. Owing to that, it should be preferred to have a large number of small interfaces than to have a small number of large interfaces, even if a single given type implements several interfaces. As with the SRP, this helps to drive up software cohesion by minimizing the potential for coupling unrelated concepts. It also eases the burden of implementation by ensuring that concrete types don’t have to consider more functionality than they intend to cover.

The Dependency Inversion Principle (DIP) states that software should not rely on concrete implementations, but should instead rely on abstractions. Please note that this has nothing to do with Dependency Injection, which is a software design pattern. DIP states that abstractions should not rely on details, but the other way around: details should rely on abstractions. For example, in an application, the business logic layer should not rely on the details of the repository layer. Rather, the business logic layer should declare the interfaces and abstractions it requires for retrieving data, and a separate repository layer should implement those interfaces and abstractions for a particular database. Note that the repository layer does not declare its own interface or abstraction: the business logic does that. This way, the business logic can be completely decoupled from the specific implementation of the repository layer. It need not know anything about the underlying technology, concrete types, project(s), solution, or even source code repository where the repository implementation exists. On the other hand, the repository layer needs to know about the business logic layer’s specific needs in order to properly meet those needs. Specific implementations can then be swapped out at will without needing to refactor the business logic. Rather than the consumer knowing details about the provider, the provider knows details about the consumer in order to properly provide. In this way, the dependency has been inverted.

The SOLID design principles here are general best-practices guidelines for how to write code. But this list is hardly exhaustive. There are plenty of other principles: General Responsibility Assignment Software Principles (GRASP) which aims to decouple code and drive up cohesion; Don’t Repeat Yourself (DRY) which aims to reduce copy-n-paste errors, as well as the number of places a bug has to be fixed; Keep It Simple, Stupid (KISS) which aims to eliminate the kind of grotesque implementations that arise from overthinking things; and many others.

Principles such as these can be applied daily to any and all software a developer may write. However, principles are not about specific implementation details. That’s what patterns are for.