A Proven Two-Step Process for Improved Code Maintainability
“At the software-architecture level, the complexity of a problem is reduced by dividing the system into subsystems. Humans have an easier time comprehending several simple pieces of information than one complicated piece.” — Steve McConnell
Software architecture is often looked at as a work of art, and, frankly, just like other disciplines in the field, it embraces creativity, science, and style.
Is it similar to buildings architecture? let's figure it out.
Imagine designing a skyscraper; I assume you would want a design that comprises a fancy entrance, a friendly lobby, safe and habitable spaces. Plus, the building must support its weight and resist wind and earthquakes.
Likewise, you would want intuitive interfaces, clean and robust code components when designing software. The design should also be feasible, modular, and safe with no chances for crashes or leaks, and ultimately, resist regressions when editing one of its components.
I’d say there are some points of similarity, and even though it is a bit intimidating to think about your codebase as a skyscraper, no worries! There are rules — even science — behind designing good software.
In today's article, we will cover a two-step process for obtaining better maintainability.
To ensure that refactoring one module in the future will not affect the rest of the modules, you will need to anticipate potential code changes, which is not an easy task at all.
How do cohesion and coupling come to the rescue? Read on to find out.
Let’s Introduce the terms
Before getting to the process, it is very important to understand the mentioned terms cohesion and coupling. So let’s take a few seconds to give a brief introduction.
Suppose you have a modular design, with functional partitioning into discrete scalable and reusable modules.
Cohesion is a measure of how related everything is to the purpose of the module.
Coupling is the degree to which modules are dependent on other modules or external resources.
Let’s dive in.
Step #1: Maximize the cohesion
Cohesion describes the relationship within the module, and it is essential to make sure that every element inside a given module is related to its purpose.
Let’s see an example of a class with high cohesion.
A “House” class should contain a door, an array of windows, and an array of rooms.
House
-----
Door _door;
Array<Window> _windows;
Array<Room> _rooms;
Let’s add an “Address” to our “House” class.
House
-----
Door _door;
Array<Window> _windows;
Array<Room> _rooms;
String _address;
Now, a question will arise. What are the elements of _address? And how can clients check it for correctness?
Assuming that _address consists of a Street name, a City, and a Postal Code.
First option:
House
-----
Door _door;
Array<Window> _windows;
Array<Room> _rooms;
String _address;
-----
boolean ValidateAdress();
In this option, “House” should implement the function “ValidateAdress(),” which is not really related to the purpose of “House” as a class. Is there a better option?
To achieve maximum cohesion, you should add a new class for Address.
Second option:
Address
-------
String _street;
String _city;
Long _postalCode;
-------
boolean ValidateAdress();
The result will be:
House
-----
Door _door;
Array<Window> _windows;
Array<Room> _rooms;
Address _address;
In this option, “House” will not be containing any logic that is not related to its purpose. That’s all. Maximum cohesion has been achieved.
Step #2: Minimize the coupling
While cohesion shows the relationship within the module, coupling shows the relationships between modules. It would be best if you tried to reduce dependencies between modules as much as possible.
Now let’s see how to minimize coupling.
Let’s add a “Garden” to the “House” we’ve designed.
Garden
------
Array<Tree> _trees;
Grass _grass;
Now let’s see how we can support these functionalities without causing coupling between the classes.
Recommended by LinkedIn
void WaterTheGarden();
void CutATree();
void CleanTheHouse();
void PrintTheAddress();
One option is that a “House” will contain a “Garden” and an “Address” inside of it(composition). And as a result, calling any of the functionalities must go through “House.”
First option:
House
-----
Door _door;
Array<Window> _windows;
Array<Room> _rooms;
Address _address;
Garden _garden;
-----
void WaterTheGarden();
void CutATree();
void CleanTheHouse();
void PrintTheAddress();
A content coupling between “House” and “Garden” can occur if “CutATree()” is implemented inside “House” to remove a tree from the array of “Tree”s inside “Garden.”
A better approach would be to create a new class called “Property,” which will contain a “House,” a “Garden,” and an “Address.”
Second option:
Property
-----
House _house;
Address _address;
Garden _garden;
-----
void WaterTheGarden();
void CutATree();
void CleanTheHouse();
void PrintTheAddress();
Using the last approach. Plus, implementing all the functionalities above inside the corresponding classes will reduce coupling to its minimum.
In conclusion, looking at the final design that has both high cohesion and loose coupling, it is clear that editing or refactoring one of its classes, “House,” “Address,” or “Garden,” won’t affect the other two classes.
Orthogonality is a more advanced mathematical term that describes this approach in software design.
In case you are not a big fan of mathematics, I assume you might want to skip the next part and jump right to the takeaway.
Coming next is a mathematical proof of why orthogonality does its job.
What is orthogonality?
Orthogonality is the generalization of perpendicularity — a mathematic term indicating whether the dot product of two vectors equals 0.
To add some clarity, In geometry, two Euclidean vectors are orthogonal if they are perpendicular, i.e., they form a right angle(90 degrees).
What is a dot product?
The magnitude of a vector a is denoted by ‖a‖
‖a‖ = ∑ an² = a1² + a2² + ,..., an²
The dot product of two vectors a and b is defined as
a·b = ‖a‖ * ‖b‖ * cos(θ)
θ is the angle between a and b.
Two vectors are orthogonal if their dot product is equal to 0.
a·b = ‖a‖ * ‖b‖ * cos(θ) = 0
Since
cos(90) = 0
If two vectors form a right angle(90 degrees), their dot product is equal to 0. No matter what values we assign to a and b, or what their magnitude is.
You can achieve a better understanding of the Dot Product by trying this cool Dot Product Visualization tool:
Now orthogonality can finally be explained in software terms; Picture it this way.
Suppose we think of the two vectors a and b as module-a and module-b. And the dot product as the “amount of work” required in module-a due to editing module-b.
Then assuming that θ is the degree to which module-a is dependent on module-b, where θ=0 is the case, they are very dependant(low cohesion and high coupling), and θ=90 is the case they are not dependant at all (high cohesion and low coupling).
We can learn that if two modules are orthogonal(not dependant), the “amount of work”(dot product) needed in module-a as a result of editing module-b is 0.
How powerful is that?!
The takeaway
Code is an ever-evolving living organism; And subject to continuous modifications and revisions.
Aside from the common things like documentation, simplicity, and consistency — One of the things that differentiate “unmaintainable” from “maintainable” code is the absence of orthogonality, i.e., low cohesion and high coupling.
Try this process while working on your next project; orthogonality will help it live long and prosper.
“… with proper design, the features come cheaply. This approach is arduous, but continues to succeed.”— Dennis Ritchie
תודה רבה על השיתוף! אני מזמין אותך לקבוצת הווצאפ🙂 הקבוצה מחברת בין עסקים ללקוחות מישראל והעולם במגוון תחומים: https://chat.whatsapp.com/BubG8iFDe2bHHWkNYiboeU
Good stuff Sameeh not often I get to see the relationship between mathematics and real world problems of software design. That helps.