Cohesion and coupling – establishing a common language

Without a pattern like MVC, your code could turn out to be hard to maintain as it could have low cohesion and high coupling. Those are fancy words, so what do we mean? Cohesion is about focus and what the class should do. The lower the cohesion, the more different things are performed by a class and therefore it has no clear intention of what it should perform.

The following code shows what happens when a class has low cohesion; it does a lot more than storing data about an invoice, such as being able to log to a file or talk to a database:

Invoice
details
total
date
validate()
print()
log()
saveToDatabase()

Now we have introduced new dedicated classes and moved methods out of the Invoice class to make sure that each and every class now has high cohesion, that is, is more focused on doing one thing well. We therefore now have the classes InvoicePrinterLogger, and InvoiceRepository:

Invoice
details
total
date
validate()

Printer
print(document)

Logger
log()

InvoiceRepository
saveToDatabase(invoice)

The point I am trying to make here is that a class should only do one thing well. This is illustrated by the unfocused Invoice class being split into four different classes that each do only one focused thing well. 

So that deals with cohesion/focus. What about coupling? Coupling is about how strongly connected a software element is to another software element. Ultimately, the higher the coupling, the harder/more tedious it is to change. Let's look at the following example of high coupling written in Java:

// cohesion-and-coupling/invoice-system.java

class Printer {
print(Invoice invoice) {
String total ="";
total += invoice.getTitle();
total += invoice.getDetails();
total += invoice.getDate();
//print 'total'
}
}

class Invoice {
String title;
String details;
int total;
Date date;
public String getTitle() { return this.title; }
public String getDetails() { return this.details; }
public String getDate() { return this.date; }
}

public class Program {
private Printer printer = new Printer();
public void run(ArrayList list) {
for(int i=0; i< list.length; i++) {
Object item = list.getItem(i);
if(item instanceof Invoice) {
Invoice invoice = (Invoice) item;
printer.print(invoice);
}
}
}

public static void main(String [] args) {
ArrayList list = new ArrayList();
list.add(new Invoice());
Program program = new Program();
program.run( list );
}
}

There are multiple problems with this code, especially if you aim to change the code in any way. Let's say we wanted to print an email as well. It is tempting to think we would need an Email class and need to add another print() method override to the Printer class.  We would also need to add branching logic to the Program class. Furthermore, testing the Program class cannot be achieved without causing a side-effect: calling the run() method would cause an actual call to a printer. The way we tend to work with tests nowadays is to run our tests every time the code changes, which it might do quite a lot as we are developing our program. We might end up with thousands of printed papers just developing our code. For that reason, we need to isolate ourselves from side effects when developing code and tests. What we want to test at the end of the day is that our code behaves correctly, not that the physical printer seems to work.

In the following code, we see an example of high coupling. We add another type, Email. The purpose of doing that is to see the effects of doing so, which is that we need to add code to several places at once. Having to do so is a sign of a code smell. The fewer changes you need to make, the better it usually is:

// cohesion-and-coupling/invoice-systemII.java

class Email {
String from;
String to;
String subject;
String body;
String getSubject() { return this.subject; }
String getFrom() { return this.from; }
String getTo() { return this.to; }
String getBody() { return this.body; }
}


class Invoice {
String title;
String details;
int total;
Date date;
String getTitle(){ return this.title; }
String getDetails() { return this.details; }
Date getDate() { return this.date; }
}

class Printer {
print(Invoice invoice) {
String total ="";
total += invoice.getTitle();
total += invoice.getDetails();
total += invoice.getDate();
//print 'total'
}

print(Email email) {
String total ="";
total += email.getSubject();
total += email.getFrom();
total += email.getTo();
total += email.getBody();
}
}

class Program {
private Printer printer = new Printer();
run(ArrayList list) {
for(int i=0; i< list.length; i++) {
Object item = list.getItem(i);
if(item instanceof Invoice) {
Invoice invoice = (Invoice) item;
printer.print( invoice );
} else if( item instanceof Email ) {
Email email = (Email) item;
printer.print( email );
}
}
}

public static void main(String [] args) {
ArrayList list = new ArrayList();
list.add( new Invoice() );
list.add( new Email() );
Program program = new Program();
program.run( list );
}
}

So let's rearrange the code a bit:

// cohesion-and-coupling/invoice-systemIII.java

class Email implements IPrintable {
String from;
String to;
String subject;
String body;
String getSubject() { return this.subject; }
String getFrom() { return this.from; }
String getTo() { return this.to; }
String getBody() { return this.body; }
public String getContent() {
String total = "";
total += email.getSubject();
total += email.getFrom();
total += email.getFrom();
total += email.getBody();
return total;
}
}

class Invoice implements IPrintable {
String title;
String details;
int total;
Date date;
String getTitle() { return this.title; }
String getDetails() { return this.details; }
String getDate() { return this.date; }
public
String getContent() {
String total = "";
total += invoice.getTitle();
total += invoice.getDetails();
total += invoice.getDate();
return total;
}
}

interface IPrintable {
String getContent();
}


interface IPrinter {
print(IPrintable printable);
}

class Printer implements IPrinter {
print( IPrintable printable ) {
String content = printable.getContent();
// print content
}
}

class Program {
private IPrinter printer;
public
Program(IPrinter printer) {
this.printer = printer;
}

run(ArrayList<IPrintable> list) {
for(int i=0; i< list.length; i++) {
IPrintable item = list.getItem(i);
printer.print(item);
}
}

public static void main(String [] args) {
ArrayList<IPrintable> list = new ArrayList<IPrintable>();
Printer printer = new Printer();
list.add(new Invoice());
list.add(new Email());
Program program = new Program(printer);
}
}

At this point, we have made our program open to extension. How can we say that, you ask? Clearly, we have removed the printer methods from printer. We also removed the switch logic from the method run in the Program class. We have also added the abstraction IPrintable, which makes anything printable responsible for telling a printer what the printable content is.

You can clearly see how we went from high coupling to low coupling when we introduced the types Document and Note. The only change they cause is themselves being added and implementing the IPrintable interface. Nothing else has to change. Success!

// invoice-systemIV.java

class Document implements IPrintable {
String title;
String body;

String getContent() {
return this.title + this.body;
}
}

class Note implements IPrintable {
String message;

String getContent() {
return this.message;
}
}

// everything else stays the same

// adding the new types to the list
class Program {
public static void main(String[] args) {
list.add(new Note());
list.add(new Document());
}
}

OK, so to sum up our changes:

  • We added the IPrintable interface 
  • We simplified/removed the branching logic in the Program.run() method
  • We made each printable class implement IPrintable
  • We added some code at the end of the previous snippet to demonstrate how easy it would be to add new types
  • We injected an IPrinter through the Program class constructor to ensure that we can easily test the Program class 

In particular note that we did not need to change any logic in either Printer or Program, when adding the Document and Note types. The only thing we needed to do was add Document and Notes as classes and ensure they implemented the IPrintable interface. To put emphasis on this, any addition to a program should not lead to an overall system change in the code

Let's reiterate the last bullet of adding IPrinter. Testability is a very good measurement to see whether your code has low coupling. If you depend on abstractions rather than actual classes, you are able to easily switch out one concrete class for another, while maintaining high-level behavior.

Another reason for switching Printer to IPrinter is so that we remove side effects from the program when we test our code. Side effects are when we talk to files, mutate states, or talk over the network for example. Testing the Program class means we want to get rid of a side effect such as actual printing and have it call something fake, or we would have a large stack of papers every time we run our tests. So to instantiate our Program class for the purposes of testing, we would write something like this instead:

// cohesion-and-coupling/invoice-systemV.java

class FakePrinter implements IPrinter {
print(IPrintable printable) { System.out.println("printing"); }
}

class Program {
FakePrinter fakePrinter;
Program(FakePrinter fakePrinter) {
this.fakePrinter = fakePrinter;
}

public static void main(String[] args) {
ArrayList<IPrintable> list = new ArrayList<IPrintable>();
Printer printer = new FakePrinter();
list.add(new Invoice());
list.add(new Email());
Program program = new Program(printer);
}
}

What we see from this code is how we shift from instantiating the Printer class (which prints to a real printer) to the Program class using an instance of FakePrinter. In a testing scenario, this is exactly what you would do, if wanting to test the Program class. What you most likely care about is the print() method being called with the correct arguments.

OK, so this was a pretty long way of expressing what low coupling is about. It is, however, important to establish what crucial terms such as coupling and cohesion are, especially when talking about patterns.