- Clean Code in C#
- Jason Alls
- 970字
- 2025-04-04 12:56:14
Cohesion and coupling
In a well-designed C# assembly, code will be correctly grouped together. This is known as high cohesion. Low cohesion is when you have code grouped together that does not belong together.
You want related classes to be as independent as possible. The more dependent one class is on another class, the higher the coupling. This is known as tight coupling. The more independent classes are of one another, the lower the cohesion. This is known as low cohesion.
So, in a well-defined class, you want high cohesion and low coupling. We'll now look at examples of tight coupling followed by low coupling.
An example of tight coupling
In the following code example, the TightCouplingA class breaks encapsulation and makes the _name variable directly accessible. The _name variable should be private and modified only by the properties of methods within its enclosing class. The Name property provides get and set methods to validate the _name variable, but this is pretty pointless as those checks can be bypassed and the properties not called:
using System.Diagnostics;
namespace CH3.Coupling
{
public class TightCouplingA
{
public string _name;
public string Name
{
get
{
if (!_name.Equals(string.Empty))
return _name;
else
return "String is empty!";
}
set
{
if (value.Equals(string.Empty))
Debug.WriteLine("String is empty!");
}
}
}
}
On the other hand, in the following code, the TightCouplingBclass creates an instance of TightCouplingA. It then introduces tight coupling between the two classes by directly accessing the _name member variable and setting it to null, and then directly accessing to print its value to the debug output window:
using System.Diagnostics;
namespace CH3.Coupling
{
public class TightCouplingB
{
public TightCouplingB()
{
TightCouplingA tca = new TightCouplingA();
tca._name = null;
Debug.WriteLine("Name is " + tca._name);
}
}
}
Now let's look at the same simple example using a low coupling.
An example of low coupling
In this example, we have two classes, LooseCouplingA and LooseCouplingB. LooseCouplingA declares a private instance variable named _name, and this variable is set via a public property.
LooseCouplingB creates an instance of LooseCouplingA and gets and sets the value of Name. Because the _name data member cannot be set directly, the checks on setting and getting the value of that data member are performed.
And so we have an example of loose coupling. Let's have a look at the two classes called LooseCouplingA and LooseCouplingB that show this in action:
using System.Diagnostics;
namespace CH3.Coupling
{
public class LooseCouplingA
{
private string _name;
private readonly string _stringIsEmpty = "String is empty";
public string Name
{
get
{
if (_name.Equals(string.Empty))
return _stringIsEmpty;
else
return _name;
}
set
{
if (value.Equals(string.Empty))
Debug.WriteLine("Exception: String length must be
greater than zero.");
}
}
}
}
In the LooseCouplingA class, we declare the _name field as private and so prevent the data from being directly modified. The _name data is made indirectly accessible by the Name property:
using System.Diagnostics;
namespace CH3.Coupling
{
public class LooseCouplingB
{
public LooseCouplingB()
{
LooseCouplingA lca = new LooseCouplingA();
lca = null;
Debug.WriteLine($"Name is {lca.Name}");
}
}
}
The LooseCouplingB class is unable to directly access the _name variable of the LooseCouplingB class, and so modifies the data member via a property.
Well, we've looked at coupling and now know how to avoid tightly coupled code and implement loosely coupled code. So now, it is time for us to look at some examples of low cohesion and high cohesion.
An example of low cohesion
When a class has more than one responsibility, it is said to be a low cohesive class. Have a look at the following code:
namespace CH3.Cohesion
{
public class LowCohesion
{
public void ConnectToDatasource() { }
public void ExtractDataFromDataSource() { }
public void TransformDataForReport() { }
public void AssignDataAndGenerateReport() { }
public void PrintReport() { }
public void CloseConnectionToDataSource() { }
}
}
As we can see, the preceding class has at least three responsibilities:
- Connecting to and disconnecting from a data source
- Extracting data and transforming it ready for report insertion
- Generating a report and printing it out
You will see clearly how this breaks the SRP. Next, we will break this class down into three classes that adhere to the SRP.
An example of high cohesion
In this example, we are going to break down the LowCohesion class into three classes that obey the SRP. These will be called Connection, DataProcessor, and ReportGenerator. Let's see how much cleaner the code is after we implement the three classes.
In the following class, you can see that the only methods in that class are related to connecting to a data source:
namespace CH3.Cohesion
{
public class Connection
{
public void ConnectToDatasource() { }
public void CloseConnectionToDataSource() { }
}
}
The class itself is named Connection, so this is an example of a high cohesive class.
In the following code, the DataProcessor class contains two methods that process data by extracting data from the data source and transforming that data for insertion into the report:
namespace CH3.Cohesion
{
public class DataProcessor
{
public void ExtractDataFromDataSource() { }
public void TransformDataForReport() { }
}
}
So this is another example of a highly cohesive class.
In the following code, the ReportGenerator class only has methods associated with generating and outputting the report:
namespace CH3.Cohesion
{
public class ReportGenerator
{
public void AssignDataAndGenerateReport() { }
public void PrintReport() { }
}
}
Again, this is another example of a highly cohesive class.
Looking at each of the three classes, we can see that they contain only methods that pertain to their single responsibility. And so each of the three preceding classes is highly cohesive.
It is now time to look at how we design our code for change by using interfaces in place of classes so that code can be injected into constructors and methods using dependency injection and inversion of control.