Software Principle 11: The Only Constant is Change
The only constant you should hold in life is change. If you hold change as a constant, you will learn to adapt and manage life’s complexities well.
Your software should reflect this principle and allow itself to evolve and adapt.
It’s essential to keep things simple while also ensuring that you can adapt the system to the inevitable yet unpredictable future.
A simple system is easy to evolve.
Things that can evolve have an ability to take a more significant and critical form in their given ecosystem: incredibly important systems and technologies are built with the ability to change in mind.
Many open source projects and business systems are built one small change at a time but evolve over an incredible and long journey that teaches the maintainers and developers about the context and changes to the context that they are executing in.
There are multiple ways to handle change in software: abstractions, encapsulation, automated testing to name a few. For the sake of brevity, we'll explore abstractions and its more specific form of encapsulation as the primary ways of dealing with change within codebases and architectures.
Techniques like automated testing, ABC schemas, design patterns are mostly implementations of these two higher order ideas, and thus discussion would ultimately devolve into discussing either abstraction or encapsulation anyways.
Let's start with abstraction. How do we build abstractions that are simple and easy to understand while also ensuring changes are taken into account? The answer comes down to a simple insight: anticipation vs. evolution.
Most abstractions in software are built in anticipation of changes, which leads to abstractions that are often not used because the anticipated events never come about.
What if we got X number of users?
What if we need to build feature X?
These are pervasive questions that come up in meetings with developers that cause the use of anticipation abstractions.
Most systems don't get to "web scale," and 90% of anticipated features never get built because there are often more important things to build. Rather than building anticipatory abstractions, we should build abstractions that evolve with our changing contexts.
That is to say: abstractions should be extensible, supportable and most of all, understandable rather than revolutionary. This allows the abstraction to always exist in a state that is both useful and meaningful to the system, while also not "over-existing" within the system and being hard to understand, extend or support.
Abstractions like "entity" and "object" have a time and place, however, often exist at the highest level of abstraction that most problems do not require.
These abstractions require many lines of logic to determine which type of “entity” exists and how it may behave in a given scenario within a context. These lines of logic then must be tested and enforced, or the system becomes buggy and unusable in some instances.
This results in incredibly complex, oppressive systems that are hard to maintain and even harder to extend.
Next, let’s turn our focus to encapsulation.
There is a common idiom in software: encapsulate that which changes. This thought usually revolves around business logic or algorithms and creating functions, methods or objects to ensure that the rest of the code is coding against interfaces rather than the implementation of the business logic or algorithm.
Encapsulation, at its core, is a particular form of abstraction involving hiding the details of implementation behind an interface to the code or component that is consuming it; which allows the underlying details of the actual implementation to change without the consuming module/code to change as well.
This makes changes more manageable and less risky. Encapsulation is becoming increasingly important as systems interface with more and more external APIs and web services.
Without encapsulation, code that interfaces with these services will change an incredible amount as new versions of services are released. Most of these services ship with an SDK to alleviate these concerns but even then, it can be wise to wrap even the SDK in your code to ensure that your system owns its interfaces.
Encapsulation ensures that most of the change happens in a single location and therefore isolates change within a system. This allows for systems to evolve instead of having to anticipate everything up front.
Ultimately, knowing that change is inevitable and holding that as the only constant fact is an important step to building systems that are resilient, extendable, and supportable.
Creating a system that can change is a sign of great architecture. Architecture at the end of the day is business enablement, business being ultimately contextual (open source is still a business). Excellent architectures allow for the climate of the business to change and system to be able to adaptable to those changes.