В идеальном мире
В идеально спроектированной системе модули зависят только от абстракций, зацепление — минимально, а связность — максимальна.
Аутентификация пользователей
Рассмотрим в качестве примера модуль аутентификации пользователей.
class MySqlConnection {/*...*/}
class Auth {
connection: MySqlConnection
constructor(connection: MySqlConnection) {
this.connection = connection
}
async authenticate(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
}
authenticate(login: string, password: string) {/*...*/}
}
OCP и LSP автоматом
Исправленный вариант автоматически удовлетворяет двум другим принципам: открытости-закрытости (OCP) и подстановки Лисков (LSP).
Мы можем заменить одну базу данных на другую, если она реализует интерфейс DataBaseConnection
, и приложение не сломается, как этого требует LSP. При изменении базы нам не придётся изменять код модуля Auth
— так мы удовлетворяем OCP.