In software architecture, Cohesion and Coupling are the yin and yang of design. While they are related, they refer to different aspects of your code structure.
- Cohesion refers to the internal consistency of a class. (Does this class do one thing well?)
- Coupling refers to the external dependency between classes. (How glued together are these classes?)
Below, we will explore these concepts separately with concrete C# examples.
Part 1: Cohesion (Internal Focus)
Goal: We want High Cohesion.
High cohesion means a class focuses on a single purpose (The Single Responsibility Principle).
Low Cohesion means a class is a "Jack of all trades," handling unrelated tasks like logging, database access, and validation all in one place.
❌ Bad Example: Low Cohesion
In this example, the UserService is doing too much. It handles logic, database operations, and email notifications. This is hard to read and maintain.
// LOW COHESION: This class has too many mixed responsibilities.
public class UserService
{
public void RegisterUser(string username, string password, string email)
{
// Responsibility 1: Validation Logic
if (string.IsNullOrEmpty(username) || password.Length < 6)
{
throw new Exception("Invalid user data");
}
// Responsibility 2: Database Logic (Direct SQL)
// If the DB changes, we have to change this class.
System.IO.File.AppendAllText("users.db", $"{username},{password}\n");
// Responsibility 3: Notification Logic
// If the email provider changes, we have to change this class.
Console.WriteLine($"Sending Welcome Email to {email}...");
}
}
✅ Good Example: High Cohesion
Here, we break the code into three small, highly cohesive classes. Each class does exactly one thing.
// HIGH COHESION: 1. Specialized Validator
public class UserValidator
{
public bool IsValid(string username, string password)
{
return !string.IsNullOrEmpty(username) && password.Length >= 6;
}
}
// HIGH COHESION: 2. Specialized Repository (Data Access)
public class UserRepository
{
public void Save(string username, string password)
{
System.IO.File.AppendAllText("users.db", $"{username},{password}\n");
}
}
// HIGH COHESION: 3. Specialized Notifier
public class EmailNotifier
{
public void SendWelcome(string email)
{
Console.WriteLine($"Sending Welcome Email to {email}...");
}
}
// The Coordinator (Clean and Easy to Read)
public class UserRegistrationService
{
public void Register(string user, string pass, string email)
{
var validator = new UserValidator();
if (!validator.IsValid(user, pass)) return;
var repo = new UserRepository();
repo.Save(user, pass);
var notifier = new EmailNotifier();
notifier.SendWelcome(email);
}
}
Part 2: Coupling (External Focus)
Goal: We want Loose Coupling.
Loose coupling means classes interact through abstractions (Interfaces), not concrete implementations.
Tight Coupling occurs when one class creates an instance of another class directly (using the new keyword), making it impossible to swap out parts or test them independently.
❌ Bad Example: Tight Coupling
In this example, the OrderProcessor is tightly coupled to SqlDatabaseLogger. You cannot run the order processor without also triggering the real SQL database.
public class SqlDatabaseLogger
{
public void Log(string message)
{
// Imagine this connects to a real SQL server
Console.WriteLine($"Writing to SQL Database: {message}");
}
}
public class OrderProcessor
{
// TIGHT COUPLING: The dependency is hard-coded inside the class.
private SqlDatabaseLogger _logger = new SqlDatabaseLogger();
public void ProcessOrder(int orderId)
{
// Processing logic...
_logger.Log($"Order {orderId} processed successfully.");
}
}
Why this is bad: If you want to switch from SQL to a Text File logger, or if you want to unit test OrderProcessor without a database connection, you can't. You have to rewrite the code.
✅ Good Example: Loose Coupling
Here, we use an Interface (ILogger). The OrderProcessor doesn't know (and doesn't care) if we are logging to a database, a file, or the console. We inject the specific logger via the constructor.
// The Abstraction
public interface ILogger
{
void Log(string message);
}
// Implementation A
public class SqlLogger : ILogger
{
public void Log(string message) => Console.WriteLine($"SQL: {message}");
}
// Implementation B (We can easily swap to this!)
public class FileLogger : ILogger
{
public void Log(string message) => Console.WriteLine($"File: {message}");
}
public class OrderProcessor
{
private readonly ILogger _logger;
// LOOSE COUPLING: We ask for an Interface, not a specific class.
// This is Dependency Injection.
public OrderProcessor(ILogger logger)
{
_logger = logger;
}
public void ProcessOrder(int orderId)
{
_logger.Log($"Order {orderId} processed.");
}
}
Usage:
// We can now decide which logger to use at runtime!
ILogger myLogger = new FileLogger();
var processor = new OrderProcessor(myLogger);
processor.ProcessOrder(101);
Summary
| Concept | The Goal | Why? |
| Cohesion | High | We want classes to be focused. Small classes are easier to understand and debug. |
| Coupling | Loose | We want classes to be independent. This allows us to swap components and write Unit Tests easily. |