Files
gruperly/.agents/skills/clean-code-principles/rules/solid-isp-clients.md
2026-09-04 16:49:24 -03:00

7.9 KiB

id, title, category, priority, tags, related
id title category priority tags related
solid-isp-clients SOLID - Interface Segregation (Client-Specific) solid-principles critical
SOLID
ISP
interface-segregation
client-design
solid-isp-interfaces
solid-srp-class
solid-lsp-contracts

Interface Segregation Principle - Client-Specific Interfaces

Clients should not be forced to depend on interfaces they do not use. Design interfaces from the client's perspective, not the implementation's.

Bad Example

// Anti-pattern: Fat interface that forces clients to depend on methods they don't use

interface UserService {
  // Authentication
  login(email: string, password: string): Promise<AuthToken>;
  logout(userId: string): Promise<void>;
  refreshToken(token: string): Promise<AuthToken>;
  validateToken(token: string): Promise<boolean>;

  // User management
  createUser(data: CreateUserData): Promise<User>;
  updateUser(id: string, data: UpdateUserData): Promise<User>;
  deleteUser(id: string): Promise<void>;
  getUser(id: string): Promise<User>;
  listUsers(filters: UserFilters): Promise<User[]>;

  // Profile
  updateProfile(userId: string, profile: ProfileData): Promise<Profile>;
  uploadAvatar(userId: string, image: Buffer): Promise<string>;
  getProfile(userId: string): Promise<Profile>;

  // Notifications
  sendNotification(userId: string, notification: Notification): Promise<void>;
  getNotificationPreferences(userId: string): Promise<NotificationPrefs>;
  updateNotificationPreferences(userId: string, prefs: NotificationPrefs): Promise<void>;

  // Analytics
  trackUserEvent(userId: string, event: AnalyticsEvent): Promise<void>;
  getUserAnalytics(userId: string): Promise<UserAnalytics>;
}

// Login page only needs authentication but must depend on entire interface
class LoginPage {
  constructor(private userService: UserService) {} // Depends on 15+ methods!

  async handleLogin(email: string, password: string): Promise<void> {
    // Only uses 1 method
    const token = await this.userService.login(email, password);
    this.storeToken(token);
  }
}

// Profile component forced to depend on authentication and analytics
class ProfileEditor {
  constructor(private userService: UserService) {} // Depends on 15+ methods!

  async saveProfile(userId: string, data: ProfileData): Promise<void> {
    // Only uses 2 methods
    await this.userService.updateProfile(userId, data);
    const profile = await this.userService.getProfile(userId);
  }
}

// Mock implementation nightmare for testing
class MockUserService implements UserService {
  // Must implement ALL methods even for simple tests
  login = jest.fn();
  logout = jest.fn();
  refreshToken = jest.fn();
  validateToken = jest.fn();
  createUser = jest.fn();
  updateUser = jest.fn();
  deleteUser = jest.fn();
  getUser = jest.fn();
  listUsers = jest.fn();
  updateProfile = jest.fn();
  uploadAvatar = jest.fn();
  getProfile = jest.fn();
  sendNotification = jest.fn();
  getNotificationPreferences = jest.fn();
  updateNotificationPreferences = jest.fn();
  trackUserEvent = jest.fn();
  getUserAnalytics = jest.fn();
}

Good Example

// Correct approach: Client-specific interfaces

// Authentication client interface
interface Authenticator {
  login(email: string, password: string): Promise<AuthToken>;
  logout(userId: string): Promise<void>;
  refreshToken(token: string): Promise<AuthToken>;
  validateToken(token: string): Promise<boolean>;
}

// User management client interface
interface UserManager {
  createUser(data: CreateUserData): Promise<User>;
  updateUser(id: string, data: UpdateUserData): Promise<User>;
  deleteUser(id: string): Promise<void>;
  getUser(id: string): Promise<User>;
  listUsers(filters: UserFilters): Promise<User[]>;
}

// Profile client interface
interface ProfileManager {
  updateProfile(userId: string, profile: ProfileData): Promise<Profile>;
  uploadAvatar(userId: string, image: Buffer): Promise<string>;
  getProfile(userId: string): Promise<Profile>;
}

// Notification client interface
interface NotificationManager {
  sendNotification(userId: string, notification: Notification): Promise<void>;
  getNotificationPreferences(userId: string): Promise<NotificationPrefs>;
  updateNotificationPreferences(userId: string, prefs: NotificationPrefs): Promise<void>;
}

// Analytics client interface
interface UserAnalyticsTracker {
  trackUserEvent(userId: string, event: AnalyticsEvent): Promise<void>;
  getUserAnalytics(userId: string): Promise<UserAnalytics>;
}

// Read-only user lookup for components that only need to fetch
interface UserLookup {
  getUser(id: string): Promise<User>;
  getProfile(userId: string): Promise<Profile>;
}

// Login page depends only on what it needs
class LoginPage {
  constructor(private auth: Authenticator) {} // Only 4 methods

  async handleLogin(email: string, password: string): Promise<void> {
    const token = await this.auth.login(email, password);
    this.storeToken(token);
  }
}

// Profile editor depends only on profile operations
class ProfileEditor {
  constructor(private profileManager: ProfileManager) {} // Only 3 methods

  async saveProfile(userId: string, data: ProfileData): Promise<void> {
    await this.profileManager.updateProfile(userId, data);
  }

  async loadProfile(userId: string): Promise<Profile> {
    return this.profileManager.getProfile(userId);
  }
}

// User display component only needs read access
class UserCard {
  constructor(private userLookup: UserLookup) {} // Only 2 methods

  async render(userId: string): Promise<void> {
    const user = await this.userLookup.getUser(userId);
    const profile = await this.userLookup.getProfile(userId);
    // Render user card
  }
}

// Admin panel needs user management
class AdminUserPanel {
  constructor(private userManager: UserManager) {} // Only 5 methods

  async deleteUserAccount(id: string): Promise<void> {
    await this.userManager.deleteUser(id);
  }
}

// Implementation can still implement multiple interfaces
class UserServiceImpl implements
  Authenticator,
  UserManager,
  ProfileManager,
  NotificationManager,
  UserAnalyticsTracker,
  UserLookup
{
  // Implement all methods...
  async login(email: string, password: string): Promise<AuthToken> { /* ... */ }
  async logout(userId: string): Promise<void> { /* ... */ }
  // ... rest of implementation
}

// Testing is now simple - only mock what you need
describe('LoginPage', () => {
  it('should login user', async () => {
    const mockAuth: Authenticator = {
      login: jest.fn().mockResolvedValue({ token: 'abc123' }),
      logout: jest.fn(),
      refreshToken: jest.fn(),
      validateToken: jest.fn()
    };

    const loginPage = new LoginPage(mockAuth);
    await loginPage.handleLogin('test@example.com', 'password');

    expect(mockAuth.login).toHaveBeenCalledWith('test@example.com', 'password');
  });
});

describe('ProfileEditor', () => {
  it('should save profile', async () => {
    const mockProfileManager: ProfileManager = {
      updateProfile: jest.fn().mockResolvedValue({}),
      uploadAvatar: jest.fn(),
      getProfile: jest.fn()
    };

    const editor = new ProfileEditor(mockProfileManager);
    await editor.saveProfile('user1', { name: 'John' });

    expect(mockProfileManager.updateProfile).toHaveBeenCalled();
  });
});

Why

  1. Minimal Dependencies: Each client depends only on the methods it actually uses, reducing coupling.

  2. Easier Testing: Mocks are small and focused. Testing LoginPage requires mocking 4 methods, not 15+.

  3. Better Encapsulation: Clients can't accidentally call methods they shouldn't have access to.

  4. Clearer Intent: Interface names describe what clients need: Authenticator, ProfileManager, UserLookup.

  5. Independent Evolution: Changes to analytics don't affect authentication clients. Interfaces can evolve separately.

  6. Flexible Composition: Different implementations can provide different subsets of functionality.

  7. Single Responsibility: Each interface has a focused responsibility, making the system easier to understand.