Mastering Code Quality: A Deep Dive into Cohesion and Coupling in Python
Overview
Software design often feels like a balancing act between making things work and making them sustainable. Two critical metrics for measuring this balance are
Prerequisites
To follow this guide, you should have a solid grasp of self, and understanding basic data structures like dictionaries. No advanced libraries are required, though a basic understanding of type hinting will help in reading the refactored code.
Key Libraries & Tools
- Python Standard Library: The core logic uses standard Python features. Specifically, the
mathlibrary is often cited as a gold standard for cohesion (e.g., thecos()function). - Type Hinting (
typing): Used to define data structures clearly, making the code more readable and easier to debug. - GRASP Principles: The GRASPprovide the conceptual framework, specifically the Information Expert pattern.
Code Walkthrough
The Problem: Low Cohesion and High Coupling
The initial code suffered from a "God Method" syndrome. The register_vehicle function was responsible for generating IDs, calculating prices, determining tax rates based on brand names, and printing results. It knew too much about the internal workings of other classes.
Step 1: Defining the Information Expert
According to VehicleInfo (for brand-level data) and Vehicle (for specific instances).
class VehicleInfo:
def __init__(self, brand: str, electric: bool, catalog_price: int):
self.brand = brand
self.electric = electric
self.catalog_price = catalog_price
class Vehicle:
def __init__(self, id: str, license_plate: str, info: VehicleInfo):
self.id = id
self.license_plate = license_plate
self.info = info
Step 2: Improving Cohesion through Focused Methods
We moved the tax calculation logic into VehicleInfo because that class owns the catalog_price and electric status. This is high cohesion: the class manages its own data logic.
def compute_tax(self) -> float:
tax_percentage = 0.05
if self.electric:
tax_percentage = 0.02
return tax_percentage * self.catalog_price
Step 3: Reducing Coupling in the Registry
Instead of the application logic manually stitching together IDs and licenses, we moved that into a VehicleRegistry. The main application now only needs to know about one method: create_vehicle.
class VehicleRegistry:
def create_vehicle(self, brand: str) -> Vehicle:
vehicle_id = self.generate_vehicle_id(12)
license_plate = self.generate_vehicle_license(vehicle_id)
brand_info = self.vehicle_info[brand]
return Vehicle(vehicle_id, license_plate, brand_info)
Syntax Notes
This tutorial utilizes f-strings (string interpolation) for clean output formatting. It also relies heavily on Type Hinting, which doesn't change runtime behavior in Python but serves as critical documentation for the developer. By using self within the Vehicle class to call methods from the linked VehicleInfo object, we demonstrate composition, a key OOP pattern that favors flexibility over rigid inheritance.
Practical Examples
These principles apply whenever you face "spaghetti code." Real-world applications include:
- E-commerce Systems: Separating
Product(specs) fromLineItem(quantity in a specific cart). - Game Development: Moving damage calculation into a
Weaponclass rather than the globalPlayercontroller. - API Clients: Ensuring a
Requestbuilder doesn't also handle the complexResponseparsing logic.
Tips & Gotchas
- The Zero-Coupling Myth: You can never achieve zero coupling; objects must interact. Aim for loose coupling where objects interact through stable interfaces rather than internal implementation details.
- Naming is a Signal: If your function name contains the word "and" (e.g.,
calculate_and_print), it likely has low cohesion and should be split. - Avoid String Matching: Initially, tax was calculated by checking if a string was "Tesla." Refactoring this to a boolean
electricattribute makes the system much easier to extend without modifying the logic for every new brand.
