Clean Code

High Cohesion in Software Development: Best Practices and Implementation Strategies

I had a class called UserManager. It managed users. It also sent emails, generated reports, handled file uploads, and validated payment details. It was 2,...

16 Apr 2024

High Cohesion in Software Development: Best Practices and Implementation Strategies

I had a class called UserManager. It managed users. It also sent emails, generated reports, handled file uploads, and validated payment details. It was 2,000 lines. Every change was a risk because everything was tangled together inside one class.

That's low cohesion. The class had no focus. It was a dumping ground.

What high cohesion means

Cohesion measures how well the elements inside a module relate to each other. High cohesion means everything in a class or module serves a single, well-defined purpose. Every method, every property, every line contributes to one job.

Think of a Swiss Army knife versus a chef's knife. The Swiss Army knife does everything poorly. The chef's knife does one thing brilliantly.

Why high cohesion matters

Readability. You open a class, see its name, and immediately understand what it does. No surprises. No unrelated methods hiding in the middle.

Maintainability. When you need to change how users are managed, you open UserManager. You don't also have to check EmailService, ReportGenerator, and PaymentValidator because those concerns aren't mixed in.

Reusability. A focused module can be dropped into another project. A 2,000-line god class can't.

Testability. Cohesive modules have clear inputs and outputs. You test one thing. With low cohesion, testing one behavior requires setting up five unrelated dependencies.

Example: cohesive design

Typescript
class UserManager {
  constructor(
    private userRepository: UserRepository,
    private logger: Logger
  ) {}

  addUser(user: User): void {
    this.userRepository.save(user);
    this.logger.log(`User added: ${user.name}`);
  }

  removeUser(userId: string): void {
    const user = this.userRepository.findById(userId);
    if (!user) {
      this.logger.log(`User not found with ID: ${userId}`);
      return;
    }
    this.userRepository.delete(userId);
    this.logger.log(`User removed: ${user.name}`);
  }
}

const userRepository = new InMemoryUserRepository();
const logger = new ConsoleLogger();
const userManager = new UserManager(userRepository, logger);
userManager.addUser({ id: '1', name: 'John Doe' });
userManager.removeUser('1');

UserManager manages users. That's it. Logging is injected, not owned. Data persistence is delegated. The class is focused.

How to achieve high cohesion

Apply the Single Responsibility Principle. If a class has two reasons to change, split it into two classes.

Group related data and behavior. If methods operate on the same fields, they belong together. If a method doesn't use any of the class's fields, it probably belongs somewhere else.

Watch for feature envy. If a method mostly accesses data from another class, move it to that class.

Keep classes small. A class with 50 focused lines beats a class with 500 scattered ones.

The trade-off

High cohesion means more classes and more files. You trade the simplicity of "everything in one place" for the clarity of "everything in the right place." Navigating between focused modules takes more clicks than scrolling through a god class.

But the god class approach breaks down as the system grows. High cohesion scales. Low cohesion doesn't. I'll take more files over more fear any day.

Keep reading