Let try to design and write better code by looking at two 💕 of the important software quality metrics – Cohesion and Coupling. This is applicable for any programming language and help you write high quality code which ensure high reusability and easy maintenance. These high level topics in general talk about how easily our code can be changed and extended. Most people think they are the same – to a certain point it is, but there is a difference –
Cohesion🔗
Cohesion is the degree to which the elements of a certain class or function belong together. Let's take a look at an example –
update_database(d, quantity)
for i -> [0:quantity):
calculate_profit(.. , ..)
status = "SUCCESS"
display_status(status, c)
...
.
Before we start with cohesion, we see the name of the function is a red flag! But it is clear that the function has a weak cohesion. It does many things that really do not belong together. A function with a strong cohesion on the other hand has a clear responsibility. An example of one such function is the sine function available in your preferred language - it does only one thing– to calculate the sine of a number!
Having a strong cohesion makes the code more readable and easy to maintain.
Coupling 🖇️
Coupling is the measure of how dependent two parts of your code are on each other. Again, let's take a look at an example –
if email.header.bearer.invalid():
return "IT'S SPAM - HEADER IS INVALID"
else if email.header.sender in email.header.bloocked_list :
return "MAIL FROM BLOCKED LIST"
else:
...
...
This function checks if the email is spam by checking various parts of the header. This code accesses data that's deep in the structure of the email object. This means that this function is highly coupled to the Email object. Having high coupling means, changing one part of the program leads to changes in several other places which we tend to miss! Here, changes in the data structure of the email object leads to changes in this function as well.
In our real world applications we need to work together – the more coupling you introduce to your app, you tend to have more knots and twists in the spider web you are building – this even makes the software harder to maintain where one change breaks other functionalities which you never expect.
In this case, to solve this coupling, we could pass along only the data which the function needs instead of the whole object, also– other way of doing this is to make this function part of the Email class
How to measure these metrics?
Unfortunately, we cannot come up with a number to determine how cohesive or how coupled your code is, but it is the duty of the developer to understand the code structure and be able to analyze the code to eliminate coupling and cohesion as much as possible to ensure the software quality which comes through experience.
Code Example
Let's walk through some examples and eliminate cohesion and coupling in them which you can use in your software development –
function generate_vehicle_id(..):
...
function generate_vehicle_license(id, ..):
...
class App():
function register_vehicle(brand):
registry = VehicleRegistry()
vehicle_id = registry.generate_vehicle_id(..)
license_plate = registry.generate_vehicle_license(vehicle_id)
catelogue_price = 0
if brand == "Audi A8":
catelogue_price = 60_000
elif brand == "Tesla Model 3 Plaid"
catelogue_price = 45_000
else:
...
tax_percentage = 0.05
if brand == "Tesla Model 3 Plaid"
tax_percentage = 0.02 //Electric ;)
print("Registration is complete. Your vehicle information..")
print("Brand : ", brand)
....
..
.
app = App()
app.register_vehicle("Audi A8")
Code analysis
In the above code,
- The main problem is with the register_vehicle function is that it is doing a lot of different things – generates id and license, calculates price and tax, prints every details – this means that this method has very low cohesion – too many responsibilities.
- It also has high coupling because it's directly depending on the vehicle registry class – App() should know that it should generate an id and pass it to generate_vehicle_license method. This means that if i change anything in VehicleRegistry() class, I have to come back and change my App implementation
- There are other problems like, if I need to add any new brands, I have to go across a big if-else ladder.
- If it is an electric car, the tax computation must also change
General thoughts
One of the easy and efficient ways to decouple and remove cohesion is to look at the data flow and see where the information is stored in the code.
When we know that and have a defined logical structure of information flow, you can start to group the code around that which leads to –
- Lower coupling because the code is closer to the information it needs
- Methods that are less cohesive because it forces us to write methods that can do one thing with that particular information that it needs
This is more in line towards GRASP Design Principles by Craig Larman
Where is the Information?
In the above example, we can see that the data is not stored logically –
- When computing catalogue price, it is tightly coupled with brand name.
- When computing tax percentage, it is not really dependent on the brand name, rather it is dependent on whether the vehicle is electric or not – so, we are missing some information here.
- In register_vehicle we have both brand name and registration details within it, which tends to extend it's responsibility. We can split this to store the brand name in a different place and we use this information to register specific vehicles.
So, let's make the changes to solve these problems –
string brand
int catalogue_price
bool electric
int tax
Constructor(brand, catalogue_price, electric):
self.brand = brand
self.catalogue_price = catalogue_price
self.electric = electric
class Vehicle():
string id
string license_plate
VehicleInfo info
Constructor(id, license_plate, info):
self.id = id
self.license_plate = license_plate
self.info = info
Reducing coupling and cohesion – (●'◡'●)
We are well aware that the vehicle_registry method has too many responsibilities, we can try to reduce it's responsibilities by the below changes –
- Move the catalogue price calculation to VehicleRegistry constructor where we add all the information to the Vehicleinfo class and move the tax calculations into VehicleInfo class as it is the responsibility of the class
- Move the printing as part of the respective information classes (VehicleInfo and Vehicle)
- Doing 1 and 2 reduced the responsibility of the register_vehicle method and the introduction of separate classes
- And, printing out the values – that doesn't sound like the responsibility of register_vehicle, so removed that.
string brand
int catalogue_price
bool electric
int tax
Constructor(brand, catalogue_price, electric):
self.brand = brand
self.catalogue_price = catalogue_price
self.electric = electric
self.tax = compute_tax()
function compute_tax():
if self.electric:
return 0.02
else:
return 0.05
function print_vehicle_info()
//Format and print Vehicle Info
...
----------------------------------------------------------------------
class Vehicle():
string id
string license_plate
VehicleInfo info
Constructor(id, license_plate, info):
self.id = id
self.license_plate = license_plate
self.info = info
function print_vehicle():
//Format and Print Vehicle Information
...
info.print_vehicle_info()
----------------------------------------------------------------------
class VehicleRegistry():
vehicle_info = {}
function generate_vehicle_id(..):
...
function generate_vehicle_license(id, ..):
...
function add_vehicle_info(brand, electric, catalogue_price):
vehicle_info[brand] = VehicleInfo(brand, electric, catalogue_price)
Constructor():
add_vehicle_info("Tesla Model Y Plaid", true, 60000)
//Add all vehicles here - probably fetching info from a DB
function create_vehicle(brand):
vehicle_id = generate_vehicle_id()
license_plate = generate_vehicle_license(vehicle_id)
return vehicle_info[brand]
function print_vehicle_information():
...
----------------------------------------------------------------------
class App():
function register_vehicle(brand):
registry = VehicleRegistry()
return registry.create_vehicle(brand)
-----------------------------------------------------------------------
app = App()
vehicle =app.register_vehicle("Tesla Model Y Plaid")
vehicle.print()
Conclusion
After this refactoring, we could easily say that each line of code is very specific in what it does – less cohesive and less coupled – which makes the code easy to understand and scale without breaking the code. And always remember to keep practicing!