TL;DR: High cohesion means classes focus on one job. Low coupling means classes don’t depend too much on each other. When I refactor messy code, these two principles guide every decision I make.
Why I Focus on Cohesion and Coupling When Refactoring
After 10+ years of building enterprise applications, I’ve inherited my fair share of messy codebases. The pattern is always the same: tangled classes doing too much, components that break when you touch something seemingly unrelated, and tests that require half the application to run.
When I approach refactoring messy code, I always start with the same two questions:
- Does this class have a clear, single purpose? (cohesion)
- How many other parts of the system would break if I changed this? (coupling)
Cohesion measures how closely related a class’s responsibilities are. I think of it as the “focus score” of your class.
Coupling measures how dependent one class is on others. It’s the “independence score” between your components.
These principles aren’t just theory, they’re my practical roadmap for turning spaghetti code into maintainable systems.
My Approach to Identifying Low Cohesion
What Good Cohesion Looks Like
When I find a class with high cohesion, it’s a breath of fresh air. Here’s what I consider a well-focused class:
public class Customer
{
private readonly string _name;
private readonly string _email;
public Customer(string name, string email)
{
_name = name;
_email = email;
}
public string GetName() => _name;
public string GetEmail() => _email;
public void UpdateEmail(string newEmail)
{
// Validation logic would go here
_email = newEmail;
}
}
This Customer
class has high cohesion because:
- It focuses solely on customer identity
- Every method works with the customer’s core data
- It doesn’t try to handle database operations or business logic
The Messy Reality: Low Cohesion in Action
Here’s the kind of mess I encounter when refactoring legacy code:
class Student {
name: string;
grade: number;
isEnrolled: boolean = false;
constructor(name: string, grade: number) {
this.name = name;
this.grade = grade;
}
enroll(): void {
this.isEnrolled = true;
}
getGPA(): number {
return this.grade / 100;
}
// This shouldn't be here!
save(): void {
console.log(`Saving ${this.name} to database...`);
// Database code
}
// Neither should this!
sendEmail(): void {
console.log(`Sending email to ${this.name}...`);
// Email code
}
}
This class violates the Single Responsibility Principle by mixing student data, database operations, and communication logic. When I see code like this, I know it’s time to refactor.
How I Refactor for Better Cohesion
Here’s my systematic approach to breaking down that messy Student class:
// Focused on student data only
class Student {
constructor(
public readonly name: string,
private grade: number,
private isEnrolled: boolean = false
) {}
getGPA(): number {
return this.grade / 100;
}
enroll(): void {
this.isEnrolled = true;
}
isCurrentlyEnrolled(): boolean {
return this.isEnrolled;
}
}
// Handles all database operations
class StudentRepository {
saveStudent(student: Student): void {
console.log(`Saving ${student.name}...`);
// Database logic here
}
}
// Manages communications
class StudentNotifier {
sendEmailToStudent(student: Student, message: string): void {
console.log(`Sending "${message}" to ${student.name}`);
// Email logic here
}
}
Now each class has a single, clear responsibility.
Managing Coupling in Object-Oriented Design
High Coupling Problem
public class Order
{
private readonly Product _product;
private readonly int _quantity;
public Order(Product product, int quantity)
{
_product = product;
_quantity = quantity;
}
public decimal CalculateTotal()
{
return _product.Price * _quantity;
}
public void Ship()
{
// Tightly coupled to specific implementations
var shipper = new UPSShipper();
shipper.Ship(this);
EmailService.SendConfirmation(this);
}
}
This creates tight coupling because:
- Order directly creates UPSShipper instances
- Changes to shipping providers require Order class modifications
- Testing becomes complex due to concrete dependencies
Low Coupling Solution with Dependency Injection
public class OrderProcessor
{
private readonly IShippingProvider _shippingProvider;
private readonly INotificationService _notificationService;
public OrderProcessor(
IShippingProvider shippingProvider,
INotificationService notificationService)
{
_shippingProvider = shippingProvider;
_notificationService = notificationService;
}
public void ProcessOrder(Order order)
{
// Use interfaces, not concrete classes
_shippingProvider.ShipOrder(order);
_notificationService.NotifyCustomer(order);
}
}
public interface IShippingProvider
{
void ShipOrder(Order order);
}
public class UPSShippingProvider : IShippingProvider
{
public void ShipOrder(Order order)
{
// UPS-specific implementation
}
}
public class FedExShippingProvider : IShippingProvider
{
public void ShipOrder(Order order)
{
// FedEx-specific implementation
}
}
This achieves low coupling by:
- Depending on interfaces rather than concrete classes
- Injecting dependencies instead of creating them
- Allowing easy substitution of implementations
Benefits of High Cohesion and Low Coupling
High Cohesion Benefits
Benefit | Description |
---|---|
Maintainability | Changes are isolated to single classes |
Testability | Focused classes are easier to unit test |
Reusability | Single-purpose classes work in multiple contexts |
Readability | Clear class purpose improves code understanding |
Low Coupling Benefits
Benefit | Description |
---|---|
Flexibility | Easy to swap implementations |
Parallel Development | Teams can work independently |
Fault Isolation | Bugs don’t cascade across components |
Testing | Mock dependencies easily |
Practical Implementation Strategies
Achieving High Cohesion
- Apply Single Responsibility Principle: Each class should have one reason to change
- Use meaningful class names: If you can’t describe a class in one sentence, it’s probably doing too much
- Group related data and behavior: Keep data and the methods that operate on it together
- Extract specialized classes: When methods don’t use class fields, consider moving them
Reducing Coupling
- Program to interfaces: Depend on abstractions, not concrete implementations
- Use dependency injection: Pass dependencies through constructors
- Apply the Dependency Inversion Principle: High-level modules shouldn’t depend on low-level modules
- Minimize class knowledge: Classes should know as little as possible about their collaborators
Real-World Angular Example
Here’s how these principles apply in an Angular service:
// High cohesion - focused on user data management
@Injectable({
providedIn: 'root'
})
export class UserService {
private readonly apiUrl = '/api/users';
constructor(private http: HttpClient) {}
getUser(id: string): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
updateUser(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}
}
// Low coupling - depends on abstractions
@Component({
selector: 'app-user-profile',
template: '...'
})
export class UserProfileComponent {
user$ = signal<User | null>(null);
constructor(
private userService: UserService,
private notificationService: INotificationService
) {}
async saveUser(user: User): Promise<void> {
try {
const updatedUser = await this.userService.updateUser(user);
this.user$.set(updatedUser);
this.notificationService.showSuccess('Profile updated successfully');
} catch (error) {
this.notificationService.showError('Failed to update profile');
}
}
}
Measuring Code Quality
Use these metrics to evaluate your code:
- Cohesion indicators: Can you describe a class’s purpose in one sentence?
- Coupling indicators: How many classes need changes when requirements change?
- Test complexity: How much setup is required for unit tests?
Common Pitfalls to Avoid
- God classes: Classes that know or do too much
- Feature envy: Methods that use data from other classes more than their own
- Inappropriate intimacy: Classes that know too much about each other’s internals
- Shotgun surgery: Small changes requiring modifications across many classes
Conclusion
High cohesion and low coupling aren’t just theoretical concepts, they’re practical tools that directly impact your development experience. When you build systems with focused classes and minimal dependencies, you create code that’s easier to understand, test, and maintain.
Start applying these principles in your next feature. Ask yourself: “Does this class have a single, clear purpose?” and “Can I change this component without breaking others?” The answers will guide you toward better software design.
The investment in understanding cohesion and coupling pays dividends every time you need to modify, debug, or extend your code. Your future self, and your teammates, will thank you.
References
- Cohesion and Coupling in Software Engineering - Engati
- Coupling (computer programming) - Wikipedia
- Differences between Coupling and Cohesion - Software Engineering
Frequently Asked Questions
What is high cohesion in software design?
What is low coupling in object-oriented programming?
How do you identify low cohesion in a class?
How can I reduce coupling in my code?
Can I apply these principles in frontend frameworks like Angular?
UserService
that only handles user data), and coupling is reduced by depending on interfaces or injection tokens instead of concrete classes.Why are high cohesion and low coupling important?
Related Posts
- Prefer Interfaces Over Abstract Classes in C#: Build Flexible, Testable, and Maintainable Code
- Dependency Inversion Principle in C#: Flexible Code with ASP.NET Core DI
- Violating SOLID for Performance: When It’s Okay and How to Isolate It
- What “One Reason to Change” Really Means in SRP
- Why Dependency Inversion Improves C# Code Quality