DEV Community

Parikshith Kedilaya Mallar
Parikshith Kedilaya Mallar

Posted on

Software Engineering Coupling – A Practical Approach

Modern day software design has a big set of challenges. Software development is no more a one-time work. It has evolved a lot into a continuous, never-ending development process, where new requirements and modification to existing functionalities need to be incorporated every now and then based upon the ever-changing business requirements. With the widespread popularity of Agile methodology of software development, changes no longer need to wait until next version of the software is taken up for development. Agile welcomes new changes to be a part of ongoing development work, which makes it necessary for software developers to have accurate design and architecture for every piece of code they are working on. This challenge can be handled by following proper software engineering protocols.

One of the key approaches to have a good software is Clean Code. Clean Code essentially means how easy it is for a programmer to read and understand a piece of code written by someone else. It is not easy to achieve this by any programmer. It requires a lot of knowledge on software design techniques, and lots of practice.
A few key points that every programmer must keep in mind while writing any code are:
• How easy is the code to be understood by others? Does it have appropriate documentation? Is it well-structured? Does it follow coding standards?
• How simple is the code? Does it follow KISS principle?
• Is the code having low-coupling? Does it follow Law of Demeter?
• Does the software contain code bloat? Does it follow DRY principle?
Here, in this article, we mainly discuss about Coupling.

Coupling:

As per Wikipedia, the definition of Coupling is “In software engineering, coupling is the degree of interdependence between software modules; a measure of how closely connected two routines or modules are; the strength of the relationships between modules.

Coupling can simply be understood as how the different modules in a software interact with each other. It is imperative to have low coupling which means, every module must have very minimal interaction with other modules. One thumb rule is that any module can interact with only its friends and not to strangers. However, the minimal the interaction, the better the software. This makes the software more modular and easier to modify any existing functionality or add any new feature.

Coupling in software engineering is broadly classified into five categories:

  1. Content Coupling
  2. Common Coupling
  3. Control Coupling
  4. Stamp Coupling
  5. Data Coupling

Content Coupling

Content Coupling is observed when one module/class accesses and modifies the data of another module. This type of coupling can be reduced by having private instance variables and using getters and setters. However, calling setters from other classes will still constitute to content coupling, since members of another class can access and modify the data.

Suppose Class A has a private instance member int age; Class B sets the value of age directly by a.age = 15; This is content coupling. Now, if for some reason, variable age must be renamed to ageLived or the data type of age must be changed from int to String, then, Class A also must modify its code.

Instead, by using a setter in Class A, we can still do the same changes without changing the code in Class A. How? See the examples below.
The below is an example for content coupling:

public class CouplingExample {
    public static void main(String[] args) {
        ContentCoupling contentCoupling = new ContentCoupling();
        contentCoupling.age = 20;
    }
}
class ContentCoupling {
    public int age;
}

In the above example, CouplingExample class directly modifies the data of ContentCoupling class. Now, in order to change the variable name of age, we must modify the variable name in CouplingExample class as well. Now imagine if age is used by 20 classes. We need to change everywhere. Solution? Use getters and setters.

public class CouplingExample {
    public static void main(String[] args) {
        ContentCoupling contentCoupling = new ContentCoupling();
        contentCoupling.setAge(20);
    }
}
public class ContentCoupling {
    private String ageLived;
    public int getAge() {
        return Integer.parseInt(ageLived);
    }
    public void setAge(int age) {
        this.ageLived = String.valueOf(age);
    }
}

Now, since we have used getter and setter, we changed the data type of variable age and changed its name to ageLived. But we do not need to make any changes in CouplingExample class!
There are also concerns regarding using setters to assign data to variables, which can be solved using various design pattern techniques such as builder patterns, but it is beyond the scope of this article.

Common Coupling

Next very important type of coupling is Common coupling. When certain data present in one class must be shared with multiple classes, you might think of using static variable types. Having static variables is against the Object-Oriented Concept of encapsulation, i.e. data must be bound to objects. Static variables are like having global variables in Imperative Programming paradigm.
Below example illustrates common coupling:

public class CommonCouplingA {
    private static int counter;

    public static int getCounter() {
        return counter;
    }

    public static void setCounter(int counter) {
        CommonCouplingA.counter = counter;
    }
}
public class CommonCouplingB {
    public void incrementCounter() {
        int counter = CommonCouplingA.getCounter();
        CommonCouplingA.setCounter(counter++);
    }
}

Here, CommonCouplingB class is incrementing the value of a counter variable in CommonCouplingA class. Since counter is a static variable, there is no need to instantiate CommonCouplingA class in CommonCouplingB class to access counter. Even if there was a new object instantiated, the value of counter would not be bound to the new object rather still have the previously set value.
So, how do we tackle this scenario?

There are two ways in which we might look at this:

  1. Pass the instantiated objects of the class containing variables that will be shared, through constructor parameterization or as method arguments. By doing so, we can avoid object instantiation every time and still share data across classes. Though this results in stamp coupling, it is acceptable.
public class CommonCouplingA {
    private int counter;

    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }
}

public class CommonCouplingB {
    private CommonCouplingA commonCouplingA;
    public CommonCouplingB(CommonCouplingA commonCouplingA) {
        this.commonCouplingA = commonCouplingA;
    }
    public void incrementCounter() {
        int counter = commonCouplingA.getCounter();
        commonCouplingA.setCounter(counter++);
    }
   }

public class CouplingExample {
    public static void main(String[] args) {
        CommonCouplingA commonCouplingA = new CommonCouplingA();
        commonCouplingA.setCounter(10);
        CommonCouplingB b = new CommonCouplingB(commonCouplingA);
        b.incrementCounter();
    }
}

Since, we are passing the object instance of CommonCouplingA to CommonCouplingB class, we are avoiding creating preserving the data bound to the instance created in CommonCouplingA by CouplingExample class.

  1. Having singleton design pattern. By having singleton class for the class which shares data, we can make sure that every time the same instance of the class is invoked.
public class SingletonDesignPattern {
    private int counter;
    private static SingletonDesignPattern instance;

    // Make constructor private so that other classes cannot instantiate a singleton class
    private SingletonDesignPattern() {};

    public static SingletonDesignPattern getInstance() {
        if (instance == null) {
            instance = new SingletonDesignPattern();
        }
        return instance;
    }

    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }
   }

   public class X {
    public void methodA() {
        SingletonDesignPattern instance = SingletonDesignPattern.getInstance();
        instance.getCounter();
    }
   }
   public class Y {
    public void methodB() {
        SingletonDesignPattern instance = SingletonDesignPattern.getInstance();
        instance.getCounter();
    }
   }

By having a singleton class, we make sure that any class that accesses the data from SingletonDesignPattern class will have consistent data. Since this class is instantiated only once, the data is always bound to the object.

Control Coupling

A module is said to be control coupled when the flow of execution is decided by a variable of another class. This means which lines of code should be executed is decided by some parameter or a variable. This is more commonly found where there are control structures like if-else conditions.

public class ClassA {
    public void method() {
    CouplingExample couplingExample = new CouplingExample();
        couplingExample.methodA("A");
    }
}

public class CouplingExample {
    public void methodA(String arg) {
        if (args[0].equals("A")) {
            PrintA printA = new PrintA();
            printA.print();
        } else {
            PrintB printB = new PrintB();
            printB.print();
        }
    }
}

Here, ClassA is controlling the execution flow of CouplingExample class. There are many ways in which control coupling can be avoided. One way is using Factory design pattern. This can be used along with Delegate design pattern to effectively rule out control coupling.

public class CouplingExample {
    public void methodA(String arg) {
        PrintFactory printFactory = new PrintFactory();
        PrintInterface printInterface = printFactory.printObject(arg);
        printInterface.print();
    }
}
interface PrintInterface {
    public void print();
}
public class PrintA implements PrintInterface {
    @Override
    public void print() {
        System.out.println("Print A");
    }
}
public class PrintB implements PrintInterface {
    @Override
    public void print() {
        System.out.println("Print B");
    }   
}
public class PrintFactory {
    public PrintInterface printObject(String letter) {
        switch(letter) {
            case "A" : return new PrintA();
            case "B" : return new PrintB();
            default : return null;
        }
    }
}

Stamp Coupling

Two classes are said to be stamp coupled if one class sends a collection or object as parameter and only a few data members of it is used in the second class. This type of coupling is desirable when the number of parameters that must be passed exceeds three. However, this needs to be further reduced to data coupling whenever possible.

public class StampCouplingA {
    private int[] array = {1, 2, 3, 4, 5};
    StampCouplingC c = new StampCouplingC();
    StampCouplingB b = new StampCouplingB();
    public void start() {
        b.callB(array, c);
    }
}

public class StampCouplingB {
    public void callB(int[] array, StampCouplingC c ) {
        System.out.println("Array length: " + array.length);
        System.out.println("Name: " + c.getName());
    }
}

public class StampCouplingC {
    private String name = "Parikshith";
    public String getName() {
        return name;
    }
}

Here StampCouplingA class is sending array and object c as parameters to StampCouplingB class. But StampCouplingB class is using the length of the array and the getName() method of class StampCouplingC. This is stamp coupling. This can be eliminated by passing only the necessary primitive types in the function call. Example below illustrates it:

public class StampCouplingA {
    private int[] array = {1, 2, 3, 4, 5};
    StampCouplingC c = new StampCouplingC();
    StampCouplingB b = new StampCouplingB();
    public void start() {
        b.callB(array.length, c.getName());
    }
}

public class StampCouplingB {
    public void callB(int length, String name ) {
        System.out.println("Array length: " + length);
        System.out.println("Name: " + name);
    }
}

public class StampCouplingC {
    private String name = "Parikshith";
    public String getName() {
        return name;
    }
}

Here only length and name are passed to callB() method as parameters, thus eliminating stamp coupling.

Data Coupling

Data coupling is the type of coupling that occurs when necessary data is sent as parameters between methods and classes. Data coupling is the most desirable among the 5 main types of coupling. However, if the number of parameters is more than three, then we may consider to create objects of the parameters if possible pass the object which contains the data itself, resulting in stamp coupling. An alternative is you may consider breaking the method into smaller methods and passing only lesser number of parameters to it.

public class DataCoupling {
    int numberA = 1;
    int numberB = 2;
    int numberC = 3;
    boolean allNumbersSet = true;
    Printer printer = new Printer();
    public void caller() {
        printer.print(numberA, numberB, numberC, allNumbersSet);
    }
}

public class Printer {
    public void print(int numberA, int numberB, int numberC, boolean allNumbersSet) {
        System.out.println("Number A: " + numberA);
        System.out.println("Number B: " + numberB);
        System.out.println("Number C: " + numberC);
        System.out.println("All numbers set? " + allNumbersSet);
    }
}

In the example above, we can see that four parameters are being passed to print() method. This is not a good coding practice. We can prevent data coupling here by breaking the print() method into two simpler methods.

public class DataCoupling {
    int numberA = 1;
    int numberB = 10;
    int numberC = 20;
    boolean allNumbersSet = true;
    Printer printer = new Printer();
    public void caller() {
        printer.printNumbers(numberA, numberB);
        printer.printNumberBoolean(numberC, allNumbersSet);
    }
}

public class Printer {
    public void printNumbers(int numberA, int numberB) {
        System.out.println("Number A: " + numberA);
        System.out.println("Number B: " + numberB);
    }
    public void printNumberBoolean(int numberC, boolean allNumbersSet) {
        System.out.println("Number C: " + numberC);
        System.out.println("All Numbers Set? " + allNumbersSet);
    }
}

Apart from these five major types of coupling there are other types of coupling for which various design patterns have been proposed to eliminate them. We generally concentrate on these five types mainly since these are frequently found in our software applications and these coupling violations causes a major damage in the system design. Being aware of these coupling types and using design patterns wherever possible makes the software application more flexible to changes in many cases by having low coupling. This is a very important aspect of a good software design.

Top comments (1)

Collapse
 
frisby37 profile image
frisby37

Very clear explanation, thank you