В идеальном мире

В идеально спроектированной системе модули зависят только от абстракций, зацепление — минимально, а связность — максимальна.

Аутентификация пользователей

Рассмотрим в качестве примера модуль аутентификации пользователей.

class MySqlConnection {/*...*/}

class Auth {
  connection: MySqlConnection

  constructor(connection: MySqlConnection) {
    this.connection = connection
  }

  async authentificate(login: string, password: string): Promise<AuthResult> {/*...*/}
}

В примере выше модуль Auth — высокоуровневый, а MySqlConnection — низкоуровневый. Пример нарушает DIP, потому что Auth зависит напрямую от MySqlConnection.

Если мы сменим базу данных, то нам придётся менять и код модуля Auth. По-хорошему, Auth не должен ничего знать о базе данных, которую мы используем. Ему достаточно знать, что есть какая-то база, к которой можно достучаться через определённые методы — это работа интерфейса.

interface DataBaseConnection {
  connect(host: string, user: string, password: string): void
}

class MySqlConnection implements DataBaseConnection {
  constructor() {/*...*/}
  connect(host: string, user: string, password: string): void {/*...*/}
}

Теперь мы можем отвязать Auth от конкретной базы данных, указав в зависимости интерфейс.

class Auth {
  // тип зависимости поменялся:
  connection: DataBaseConnection

  constructor(connection: DataBaseConnection) {
    this.connection = connection
  }

  authentificate(login: string, password: string) {/*...*/}
}

OCP и LSP автоматом

Исправленный вариант автоматически удовлетворяет двум другим принципам: открытости-закрытости (OCP) и подстановки Лисков (LSP).

Мы можем заменить одну базу данных на другую, если она реализует интерфейс DataBaseConnection, и приложение не сломается, как этого требует LSP. При изменении базы нам не придётся изменять код модуля Auth — так мы удовлетворяем OCP.

Материалы к разделу

Вопросы