3 Essentials for Software Design
1. Your Toolbox: Design Patterns
In software design, design patterns are an essential toolbox to ensure your code is broken down into small independent units. This toolbox gets filled one-by-one, a single design pattern at a time. Since the tools can be used in different combinations, quickly the toolbox owner becomes more effective in creating a design that is focused on minimum effort and risk.
This is especially true if you write or support software in a team. First you must ensure the tasks - and thus the code - are broken down into independent units. Common symptoms that arise when this is not properly done: engineers waiting on each other code to complete, functionality duplication, merge conflicts, the work of member "a" breaks the work of member "b" (regression bugs), incoherent interfaces - e.g. getX, doWriteX, deleteById; while much more coherent interface would be: readX-writeX-deleteX, or getX-updateX-deleteX.
The second issue is in supporting (reading) the code.
Ever heard about <see previous article> of my_bad_var that discusses the same as discussed <here>, but has a <another link> reputation? When you are reading, you would like to read a simple all inclusive story, not a story that keeps you sending to different places.
This is all achieved by making sure you cut your software in independent units. To do this, you mainly leverage your toolbox that is filled with design patterns. And to get a toolbox filled with design patterns, you need to learn them one by one.
2. Your Principle: Separate Logic from Object Graph Lookup & Manipulation
The object graph is the graph of your objects, or data structures in general. These objects contain references to other objects thus forming a graph. To find a specific object in this graph, you need to use an entry point and walk zero or more references, called the object lookup. The manipulation is changing the references and/or add or removal of objects into the graph.
A lot of written logic tend to not only implement a specific functionality but also to be embedded into a specific context by doing object graph lookups and manipulations. (Ever seen code that only works when your data is in a database, but won't work if you have it already loaded in memory?) The two different concerns should be separated. This will yield the following benefits: more testable code, more re-usable code, and easier to read - support - code.
3. Your Goal: Realizing the Architecture
When trying to realize the functionality with minimum effort and risk, you often find that the architecture is in your way. When hardwiring the exposed data model to the physical data model, you often can skip a whole set of classes. It also becomes then easy to argue that the architecture is not effective, forgetting that architecture in essence tries to accomplish the opposite - planning for change.
You should be realizing the architecture. In that case it will never be in the way, and it will also guide you to make the right decisions for splitting the code into independent units. Above all, it will ensure that the work you put into designing and developing your system last long.
You can argue that the chance that databases - DBMS-es - change, or the chance that an extra API is needed, or an extra computer language needs to be supported, are slim. But what if? Not all, but some change always happens, you just need to make sure that you are prepared for this. Like cyber security: you are sure you get hacked, but how can you limit the impact. Thus if only a single change would render your system outdated, you failed in software engineering. Never be in the situation where you need to point fingers to product management as they didn't say it was a requirement...
Software design is a specific skill that differs from architecture and implementation. There is a lot of documentation "in the wild" and most of those resources are context specific and highly specialized. In contrast, here we cover three essentials that you need to become an effective software designer: create small independent units that can be implemented in parallel, make sure you separate concerns and allow code re-usability, and finally make sure that you are working based of the architecture versus only functionality.