Skip to content

Separation of Concerns (SoC)

This principle states that different concerns or responsibilities of a program should be separated into distinct sections.

Purpose: Separation of concerns helps make the code more modular, easier to maintain, and allows changes to one part of the system without affecting others.

Problem

In this example, a class mixes the concerns of data storage, business logic, and presentation:

ts
class UserManager {
  private users: string[] = [];

  constructor() {
    // Initialization
  }

  // Concern 1: Data handling
  addUser(user: string): void {
    this.users.push(user);
    console.log(`User ${user} added to the system.`);  // Mixing logic with UI
  }

  // Concern 2: Business logic
  getUser(userId: number): string {
    if (userId < 0 || userId >= this.users.length) {
      throw new Error("User not found.");
    }
    return this.users[userId];
  }

  // Concern 3: Presentation logic (UI logic mixed with data handling)
  renderUserList(): void {
    this.users.forEach((user) => {
      console.log(`<li>${user}</li>`);  // Presentation logic mixed in
    });
  }
}

Issues with this code:

  • Data handling (adding and retrieving users) is mixed with presentation logic (rendering user list).
  • Business logic is also embedded in the UserManager class, making it harder to maintain and test.

Solution

Here’s how you could refactor the code to follow the Separation of Concerns principle by dividing the responsibilities into different classes:

ts
// Concern 1: Data handling (Model)
class UserManager {
  private users: string[] = [];

  addUser(user: string): void {
    this.users.push(user);
  }

  getUser(userId: number): string {
    if (userId < 0 || userId >= this.users.length) {
      throw new Error("User not found.");
    }
    return this.users[userId];
  }

  getAllUsers(): string[] {
    return this.users;
  }
}

// Concern 2: Presentation (View)
class UserView {
  renderUserList(users: string[]): void {
    users.forEach((user) => {
      console.log(`<li>${user}</li>`);  // Purely presentation logic
    });
  }
}

// Concern 3: Business Logic (Controller)
class UserController {
  private userManager: UserManager;
  private userView: UserView;

  constructor(userManager: UserManager, userView: UserView) {
    this.userManager = userManager;
    this.userView = userView;
  }

  addNewUser(user: string): void {
    this.userManager.addUser(user);
    this.displayUserList();
  }

  displayUserList(): void {
    const users = this.userManager.getAllUsers();
    this.userView.renderUserList(users);
  }
}

Explanation:

  • UserManager: Handles data management (add user, get user, get all users).
  • UserView: Takes care of presentation (rendering the user list).
  • UserController: Coordinates between UserManager and UserView. It handles the business logic of adding a user and displaying the list.

Benefits of this structure

  • Maintainability: Each class has a single responsibility and can be modified independently without affecting the others.
  • Testability: The logic for handling users, presentation, and business rules are separated, making it easier to test each part individually.
  • Extensibility: You can add new features (like different types of views or complex user management features) without tightly coupling the system components.

Released under the MIT License.