“… the greatest limitation in writing software is our ability to understand the system we are creating.”
- John Ousterhout, A Philosophy of Software Design, Chapter 1
Design, in software, refers to the organization of our code, including classes, functions, and the relationships between them. No matter how we construct our software, our software has a design. The question is: is that design any good?
Of course, the question of what is a “good” design doesn’t have any obvious answer. In physical systems, such as bridges, home appliances, cars, etc., we often think about design related to “how long the system works before degrading.” That is, the Golden Gate Bridge must be well-designed, because it’s been opened for over 85 years now and still meeting its goal. A car is well-designed if it last 200k miles.
And sure, these things have maintenance costs; the Golden Gate Bridge has had its deck replaced because of salt in the fog corroding the rebar, and I have had to change the oil and do significant transmission repair on my personal car. But ultimately, the goal of this maintainance is to ensure that the produce operates the same as it did
The fundamental difference is that software is, for all intents and purposes, completely malleable in a way that these physical items cannot be. For instance, the main cables of the Golden Gate Bridge cannot be replaced. Additionally, while I can replace parts on my car to maintain it, I cannot dramatically change the car. My car is an economy hatchback that cannot easily fit passengers in the back seat. When I have kids, I will eventually have to replace it if I want to be able to fit them in the back seat as they age.
On the other hand, software is expected to evolve to meet new needs! However, just because it’s easier to retrofit software than my little Ford Fiesta, doesn’t mean it’s easy. Anyone who has tried to fix one bug in their code, only to introduce three more, can attest to the difficulty of changing software.
The difficulty is that as we add features to software, it becomes more complicated. And the more complicated our software is, the harder it is to understand, and therefore the harder it is to maintain. This means changes take more effort measured in developers-hours, which is a stand-in for “cost”, as developer time is the most valuable, and often most expensive, cost of software development. The practical effect of this is that as software ages, it requires more time to implement fewer improvements, as software development grinds down. Entropy is any form is almost certainly unavoidable. However, a good design can dramatically slow the growth of entropy, and reduce the overall effort required to make a software change.
“The measure of design quality is simply the measure of the effort required to meet the needs of the customer. If that effort is low, and stays low throughout the lifetime of the system, the design is good. If that effort grows with each new release, the design is bad. It’s as simple as that”
- Robert Martin - Clean Architecture Chapter 1
Thus, the key to design is to plan for change. That can be anticipating what could change (even if we are not certain exactly what will change - and we never will be) and planning designs to allow that change. However, ultimately, some new change can and will catch us off guard. At that point, we want to compartmentalize individual aspects of our system as best as we can so that change is unlikely to propogate.
All of the above shouldn’t encourage “over-design”.
Whenever designing a system, we should consider the long-term lifecycle of the software we intend, and how critical the software is (that is, what are the consequences of it failing). For long-term safety critical systems, such as Air Traffic Control systems, we obviously would rather take the risk of over-designing our system.
On the other hand, if our software is single purpose, and expected to have a short lifespan, we may find that we are okay with the software being more brittle (that is, hard to change or repurpose).
For example, in the Fall of 2022, CS 3140 used Collab, an open-sourced LMS (leaning management system). I give quizzes on the web-application, which students can take multiple times and get their highest score. If a student gets 80% or more of the answers on a quiz correct, I want to give them full credit. Collab lets me download an Excel file (specifically, .xls, or Microsoft Office 2003 and earlier) that contains all the students’ quiz submissions. So in my program, I want to:
A lot of the features here could change:
There are dozens of additional changes that could occur that might affect my script. Did I account for them in my design, and plan my design to be receptive to that change? No. Why not?
Design should take into account your expectations for the life of the software. The longer you expect software to last, as well as the more you expect software to evolve to changing user needs, the more you should consider design seriously.
In practice, most commercial software, even “micro-service” web application software, will still need to evolve and change, and have a long lifespan. But an advantage of agile development, where software is repeatedly improved and modified iteratively, is that we can refactor design elements later on as our needs change. This is why we spent so much of our last unit focusing on writing code that can be understood, changed, and resistant to change. Because ultimately, every design will eventually fail: we will have to change at least something we didn’t anticipate.
As such, a common practice in agile development is a modular design that can be evolved and changed over the life-cycle of the software. This is what we will focus on in this class.