Зависьте от абстракций
Принцип инверсии зависимостей гласит: высокоуровневый код — ваша бизнес-логика — не должен зависеть от низкоуровневых деталей, таких как конкретная база данных или формат файла. Обе стороны должны зависеть от абстракции: в TypeScript это интерфейс.
«Инверсия» — это направление стрелки. Вместо того чтобы сервис указывал на одно конкретное хранилище, сервис указывает на интерфейс — и каждое хранилище указывает на тот же интерфейс с другой стороны.
Жёстко привязан к деталям
Этот сервис пользователей создаёт и использует конкретный DiskStore напрямую. Это работает — но сервис приварен к одному хранилищу. Другое хранилище, более быстрое хранилище, поддельное хранилище для тестов: каждое из них означает правку самого сервиса.
Сервис, приваренный к своему хранилищу
UserService сам создаёт внутри себя DiskStore. Высокоуровневая логика регистрации теперь зависит от низкоуровневой детали — файла на диске.
Вывод: программа на TypeScript и захваченный вывод
class DiskStore {
save(user: string): void {
console.log("записываем", user, "в файл users.txt на диске")
}
}
// UserService сам создаёт своё хранилище — приварен к детали.
class UserService {
private store: DiskStore
constructor() {
this.store = new DiskStore()
}
register(user: string): void {
console.log("регистрируем пользователя:", user)
this.store.save(user)
}
}
const service = new UserService()
service.register("ana")
Инвертируйте зависимость
Решение: сервис запрашивает интерфейс UserStorage в своём конструкторе и никогда не узнаёт, какую именно реализацию получил. И дисковое хранилище, и хранилище в памяти реализуют этот интерфейс.
Теперь стрелка указывает на абстракцию с обеих сторон — и замена хранилища сводится к изменению одной строки там, где создаётся сервис, а не внутри него.
Сервис принимает интерфейс
Та же логика регистрации, два разных хранилища. new UserService(new DiskStore()) или new UserService(new MemoryStore()) — код самого сервиса не меняется.
Вывод: программа на TypeScript и захваченный вывод
// Абстракция, от которой зависят обе стороны.
interface UserStorage {
save(user: string): void
}
class DiskStore implements UserStorage {
save(user: string): void {
console.log("записываем", user, "в файл users.txt на диске")
}
}
class MemoryStore implements UserStorage {
save(user: string): void {
console.log("храним", user, "в памяти")
}
}
class UserService {
private store: UserStorage
constructor(store: UserStorage) {
this.store = store
}
register(user: string): void {
console.log("регистрируем пользователя:", user)
this.store.save(user)
}
}
const onDisk = new UserService(new DiskStore())
onDisk.register("ana")
const inMemory = new UserService(new MemoryStore())
inMemory.register("ben")
Выгода: тестирование без диска
Поскольку сервис зависит от абстракции, тест может передать ему хранилище в памяти и мгновенно проверить результат — никаких файлов, никакой уборки. Именно инверсия зависимостей делает код удобным для модульного тестирования.
Вывод: программа на TypeScript и захваченный вывод
interface UserStorage {
save(user: string): void
}
// MemoryStore записывает то, что было сохранено — идеально для проверок.
class MemoryStore implements UserStorage {
users: string[] = []
save(user: string): void {
this.users.push(user)
}
}
class UserService {
private store: UserStorage
constructor(store: UserStorage) {
this.store = store
}
register(user: string): void {
this.store.save(user)
}
}
const store = new MemoryStore()
const service = new UserService(store)
service.register("ana")
service.register("ben")
if (store.users.length === 2) {
console.log("проверка пройдена: сохранено 2 пользователя")
}
console.log("сохранённые пользователи:", store.users.join(", "))