High Cohesion / Low Coupling

High Cohesion

Definition: High cohesion in software development refers to the degree to which the elements inside a module, class, or component are closely related and focused on performing a single task or a closely related set of tasks. High cohesion is often seen as a desirable attribute, as it makes modules more understandable, easier to maintain, and less prone to errors.

Characteristics of High Cohesion:

  1. (SOLID Principle) Single Responsibility: Each module or class has a clear, well-defined purpose.
  2. Ease of Maintenance: Changes in one part of the system have minimal impact on other parts.
  3. Reusability: Highly cohesive modules can be easily reused in different parts of the system.
  4. Improved Readability and Understanding: Each module or class is clear and straightforward to understand.

Example: Consider a class in a music player application named AudioPlayer. If this class is highly cohesive, it would only contain methods and properties directly related to playing audio, such as play(), pause(), stop(), and volumeControl(). It wouldn’t have unrelated functionalities like managing user playlists or network handling.

Here, we’ll design a simple AudioPlayer class that exemplifies high cohesion by focusing solely on audio playback functionalities.

C++
class AudioPlayer {
public:
    void play() {
        // Code to start playback
    }

    void pause() {
        // Code to pause playback
    }

    void stop() {
        // Code to stop playback
    }

    void setVolume(int level) {
        // Code to adjust volume
    }

    // Other methods directly related to audio playing
};

In this AudioPlayer class, all methods are tightly related to the single responsibility of playing audio. This class does not include unrelated functionalities like managing playlists or network operations, thus maintaining high cohesion.

Low Coupling

Definition: Low coupling refers to the degree of independence between modules, classes, or components in a system. In a low-coupled system, changes in one module have minimal effect on other modules. This principle reduces the interdependencies between components, making the system easier to modify, extend, and maintain.

Characteristics of Low Coupling:

  1. Independence: Modules can function and be modified independently.
  2. Change Resilience: Changes in one module require fewer changes in others.
  3. Ease of Testing: Independent modules are easier to test in isolation.
  4. Flexibility and Scalability: The system can be easily expanded with minimal impact on existing components.

Example: In the same music player application, suppose there is a PlaylistManager class responsible for managing user playlists. In a low-coupled design, PlaylistManager would interact with other components like AudioPlayer through well-defined interfaces or APIs, rather than directly manipulating their internal states. This way, changes in the AudioPlayer class (like modifying its internal data structures) won’t require changes in PlaylistManager.

For low coupling, let’s have two classes, PlaylistManager and AudioPlayer. These classes interact with each other, but they are designed to minimize dependencies.

C++
class AudioPlayer {
public:
    void playSong(const std::string& song) {
        // Code to play a specific song
    }
};

class PlaylistManager {
private:
    AudioPlayer& player;
    std::vector<std::string> playlist;

public:
    PlaylistManager(AudioPlayer& player) : player(player) {}

    void addSongToPlaylist(const std::string& song) {
        playlist.push_back(song);
    }

    void playPlaylist() {
        for (const auto& song : playlist) {
            player.playSong(song);
            // Additional code to handle playlist playback
        }
    }
};

Combining High Cohesion and Low Coupling

In software design, the combination of high cohesion within modules and low coupling between modules is ideal. It allows each part of the system to be developed, maintained, and understood more easily while providing flexibility to the overall system architecture. This combination leads to a more robust, maintainable, and scalable system.

Examples

Violation the rule

These principles can lead to code that is easier to understand and maintain. Here’s an example in C++ where high cohesion and low coupling are violated in the context of a hospital, doctors, and patients:

C++
#include <iostream>
#include <string>
#include <vector>

class Patient {
public:
    Patient(const std::string& name) : name(name) {}

    void diagnose() {
        std::cout << name << " has been diagnosed." << std::endl;
    }

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class Doctor {
public:
    Doctor(const std::string& name) : name(name) {}

    void treatPatient(Patient& patient) {
        std::cout << name << " is treating patient: " << patient.getName() << std::endl;
        patient.diagnose();
        std::cout << "Prescribing medication to " << patient.getName() << std::endl;
    }

private:
    std::string name;
};

class Hospital {
public:
    void admitPatient(Patient& patient) {
        patients.push_back(patient);
        std::cout << "Admitted patient: " << patient.getName() << std::endl;
    }

    void assignDoctor(Doctor& doctor, Patient& patient) {
        std::cout << "Assigning " << doctor.getName() << " to treat " << patient.getName() << std::endl;
        doctor.treatPatient(patient);
    }

private:
    std::vector<Patient> patients;
};

int main() {
    Hospital hospital;
    Patient patient1("Alice");
    Patient patient2("Bob");
    Doctor doctor1("Dr. Smith");
    Doctor doctor2("Dr. Johnson");

    hospital.admitPatient(patient1);
    hospital.admitPatient(patient2);

    hospital.assignDoctor(doctor1, patient1);
    hospital.assignDoctor(doctor2, patient2);

    return 0;
}

In this code, high cohesion and low coupling are violated in the following ways:

  1. Violation of Low Coupling:
    • The Doctor class is tightly coupled with the Patient class because it directly treats patients by calling their diagnose method.
    • The Hospital class is also tightly coupled with both Doctor and Patient classes as it assigns doctors to patients and calls methods on both.
  2. Violation of High Cohesion:
    • The Doctor class has responsibilities that are not directly related to being a doctor, such as prescribing medication. This leads to low cohesion within the class.

To improve the design and adhere to high cohesion and low coupling principles, you could refactor the code to have separate classes or interfaces for tasks like diagnosis and medication prescription, ensuring each class has a clear and single responsibility and reducing the dependencies between classes.

Refactoring by adherence to the rule

To improve the design and adhere to high cohesion and low coupling principles in the context of a hospital, doctors, and patients, we can refactor the code by introducing additional classes that better separate responsibilities. Here’s an improved example:

C++
#include <iostream>
#include <string>
#include <vector>

class Patient {
public:
    Patient(const std::string& name) : name(name) {}

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class DiagnosisService {
public:
    void diagnosePatient(const Patient& patient) {
        std::cout << "Diagnosing patient: " << patient.getName() << std::endl;
        // Perform diagnosis logic here
    }
};

class MedicationService {
public:
    void prescribeMedication(const Patient& patient) {
        std::cout << "Prescribing medication to patient: " << patient.getName() << std::endl;
        // Perform medication prescription logic here
    }
};

class Doctor {
public:
    Doctor(const std::string& name) : name(name) {}

    void treatPatient(const Patient& patient, DiagnosisService& diagnosisService, MedicationService& medicationService) {
        std::cout << name << " is treating patient: " << patient.getName() << std::endl;
        diagnosisService.diagnosePatient(patient);
        medicationService.prescribeMedication(patient);
    }

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class Hospital {
public:
    void admitPatient(const Patient& patient) {
        patients.push_back(patient);
        std::cout << "Admitted patient: " << patient.getName() << std::endl;
    }

    void assignDoctor(Doctor& doctor, const Patient& patient) {
        std::cout << "Assigning " << doctor.getName() << " to treat " << patient.getName() << std::endl;
        doctor.treatPatient(patient, diagnosisService, medicationService);
    }

private:
    std::vector<Patient> patients;
    DiagnosisService diagnosisService;
    MedicationService medicationService;
};

int main() {
    Hospital hospital;
    Patient patient1("Alice");
    Patient patient2("Bob");
    Doctor doctor1("Dr. Smith");
    Doctor doctor2("Dr. Johnson");

    hospital.admitPatient(patient1);
    hospital.admitPatient(patient2);

    hospital.assignDoctor(doctor1, patient1);
    hospital.assignDoctor(doctor2, patient2);

    return 0;
}

In this improved example:

  1. Low Coupling:
    • The Doctor class is no longer directly coupled with the Patient class. Instead, it relies on DiagnosisService and MedicationService to perform diagnosis and prescription, respectively.
    • The Hospital class also reduces its direct coupling with Doctor, Patient, and the associated services.
  2. High Cohesion:
    • Responsibilities are separated into distinct classes: Patient for patient-related information, DiagnosisService for diagnosis, MedicationService for medication prescription, Doctor for doctor-related information and treatment, and Hospital for patient management.
    • Each class now has a clearer and more focused responsibility.

This refactoring improves code maintainability and makes extending and modifying the system easier while adhering to the principles of high cohesion and low coupling.