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}...`);
}
}
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
}
}
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);
}
}
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);
}
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!');
}
}
}
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
- How Does Composition Support the SOLID Principles? (C# Examples & Best Practices)
- Composition Over Inheritance in C#: Write Flexible, Maintainable Code
- DIP vs DI vs IoC: Understanding Key Software Design Concepts
- Polymorphism in C#: How Template Method, Strategy, and Visitor Patterns Make Your Code Flexible
Top comments (0)