DEV Community

Abhinaw
Abhinaw

Posted on • Originally published at bytecrafted.dev

Refactoring Messy Code: High Cohesion and Low Coupling

After 10+ years building enterprise applications, I've learned that two principles guide every successful refactoring: high cohesion and low coupling.

Cohesion = Does this class have a clear, single purpose?

Coupling = How many parts break when I change this?

Spotting Low Cohesion

Here's the mess I typically inherit:

class Student {
    name: string;
    grade: number;

    constructor(name: string, grade: number) {
        this.name = name;
        this.grade = grade;
    }

    getGPA(): number {
        return this.grade / 100;
    }

    // This shouldn't be here!
    save(): void {
        console.log(`Saving ${this.name} to database...`);
    }

    // Neither should this!
    sendEmail(): void {
        console.log(`Sending email to ${this.name}...`);
    }
}
Enter fullscreen mode Exit fullscreen mode

This violates Single Responsibility Principle by mixing student data, database operations, and communication logic.

My Refactoring Approach

I break it into focused classes:

// High cohesion - focused on student data only
class Student {
    constructor(
        public readonly name: string,
        private grade: number
    ) {}

    getGPA(): number {
        return this.grade / 100;
    }
}

// Handles database operations
class StudentRepository {
    saveStudent(student: Student): void {
        // Database logic here
    }
}

// Manages communications  
class StudentNotifier {
    sendEmailToStudent(student: Student): void {
        // Email logic here
    }
}
Enter fullscreen mode Exit fullscreen mode

Reducing Coupling with Dependency Injection

Instead of tight coupling:

public class Order
{
    public void Ship()
    {
        // Tightly coupled - creates concrete dependencies
        var shipper = new UPSShipper();
        shipper.Ship(this);
    }
}
Enter fullscreen mode Exit fullscreen mode

Use interfaces and injection:

public class OrderProcessor
{
    private readonly IShippingProvider _shippingProvider;

    public OrderProcessor(IShippingProvider shippingProvider)
    {
        _shippingProvider = shippingProvider;
    }

    public void ProcessOrder(Order order)
    {
        _shippingProvider.ShipOrder(order);
    }
}

public interface IShippingProvider
{
    void ShipOrder(Order order);
}
Enter fullscreen mode Exit fullscreen mode

Angular Example

These principles work perfectly in Angular:

@Injectable({ providedIn: 'root' })
export class UserService {
    constructor(private http: HttpClient) {}

    getUser(id: string): Observable<User> {
        return this.http.get<User>(`/api/users/${id}`);
    }
}

@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 updated = await this.userService.updateUser(user);
            this.user$.set(updated);
            this.notificationService.showSuccess('Saved!');
        } catch {
            this.notificationService.showError('Failed!');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Quick Quality Check

Ask yourself:

  • Can I describe this class's purpose in one sentence? (cohesion test)
  • How many classes break when requirements change? (coupling test)
  • How much setup do my unit tests need? (both tests)

The Payoff

Systems built with these principles are:

  • Easier to maintain - changes stay isolated
  • Simpler to test - focused classes, mockable dependencies
  • More flexible - swap implementations without breaking things
  • Less buggy - failures don't cascade

Start applying this in your next feature. Your future self will thank you.


Originally published on my blog with more detailed examples and implementation strategies.


Related Posts

Top comments (0)