← Назад к вопросам

Что означает буква L в SOLID?

1.6 Junior🔥 251 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Принцип подстановки Барбары Лисков (LSP)

Буква L в акрониме SOLID означает Принцип подстановки Лисков (Liskov Substitution Principle, LSP). Это третий из пяти фундаментальных принципов объектно-ориентированного программирования и дизайна, сформулированных Робертом Мартином, который основывается на более ранней работе Барбары Лисков (именно в её честь назван принцип).

Суть принципа

Формально принцип звучит так:

Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности этой программы.

Проще говоря, если у вас есть класс Родитель и унаследованный от него класс Ребенок, то вы должны иметь возможность использовать объект класса Ребенок везде, где ожидается объект класса Родитель, и программа при этом продолжит работать корректно. Программа не должна "ломаться" или вести себя непредсказуемо.

Ключевые аспекты и "дух" принципа

Принцип Лисков — это не просто о синтаксической совместимости (компиляции кода). Это о семантической совместимости. Подкласс должен не просто реализовывать интерфейс родителя, но и сохранять его контракты, гарантии и инварианты.

Основные правила, вытекающие из LSP:

  1. Предусловия (требования к входным данным) не могут быть усилены в подклассе.
  2. Постусловия (гарантии на выходе) не могут быть ослаблены в подклассе.
  3. Инварианты (неизменные условия состояния) родительского класса должны сохраняться в подклассе.
  4. Подкласс не должен генерировать исключения, которые не ожидаются от базового класса (новые типы исключений, не являющиеся подтипами исключений базового класса).

Пример нарушения LSP в iOS (Swift)

Классический пример — иерархия "Прямоугольник — Квадрат".

class Rectangle {
    var width: Double = 0
    var height: Double = 0

    func setDimensions(width: Double, height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double {
        return width * height
    }
}

class Square: Rectangle {
    // Нарушение LSP: Подкласс изменяет инвариант базового класса (независимость ширины и высоты).
    override func setDimensions(width: Double, height: Double) {
        // Квадрат принудительно делает стороны равными, ломая ожидаемое поведение.
        let side = max(width, height)
        self.width = side
        self.height = side
    }
}

// Функция, работающая с базовым классом
func printArea(of rectangle: Rectangle) {
    rectangle.setDimensions(width: 5, height: 4)
    print("Ожидаемая площадь: 20. Фактическая площадь: \(rectangle.area)")
}

let rect = Rectangle()
let square = Square()

printArea(of: rect) // Вывод: Ожидаемая площадь: 20. Фактическая площадь: 20.0
printArea(of: square) // Вывод: Ожидаемая площадь: 20. Фактическая площадь: 25.0 (НАРУШЕНИЕ!)

В этом примере Square является подклассом Rectangle, но он нарушает контракт метода setDimensions. Функция printArea ожидает, что после установки ширины 5 и высоты 4 площадь будет 20. Square ломает это ожидание, изменяя переданные значения. С точки зрения LSP, Square не может быть корректной заменой Rectangle, хотя с точки зрения компилятора всё в порядке.

Пример соблюдения LSP в iOS (Swift)

Рассмотрим более жизненный пример, связанный с абстракцией данных.

// Корректная иерархия, соответствующая LSP
protocol DataFetcher {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}

class NetworkFetcher: DataFetcher {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        // Сетевой запрос...
        URLSession.shared.dataTask(with: someURL) { data, _, error in
            if let data = data {
                completion(.success(data))
            } else if let error = error {
                completion(.failure(error))
            }
        }.resume()
    }
}

class CacheFetcher: DataFetcher {
    let cachedData: Data

    init(cachedData: Data) {
        self.cachedData = cachedData
    }

    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        // Немедленно возвращаем кэшированные данные
        completion(.success(cachedData))
    }
}

// Клиентский код, который зависит от абстракции DataFetcher
class DataProcessor {
    private let fetcher: DataFetcher

    init(fetcher: DataFetcher) {
        self.fetcher = fetcher // Может быть ЛЮБОЙ объект, соблюдающий протокол.
    }

    func process() {
        fetcher.fetchData { result in
            switch result {
            case .success(let data):
                self.handleData(data)
            case .failure(let error):
                self.handleError(error)
            }
        }
    }

    private func handleData(_ data: Data) { /* ... */ }
    private func handleError(_ error: Error) { /* ... */ }
}

// Использование: взаимозаменяемость объектов соблюдена.
let networkProcessor = DataProcessor(fetcher: NetworkFetcher())
let cacheProcessor = DataProcessor(fetcher: CacheFetcher(cachedData: Data()))

Здесь протокол DataFetcher определяет четкий контракт. Классы NetworkFetcher и CacheFetcher соблюдают этот контракт, не нарушая ожиданий. DataProcessor может работать с любым из них, и его корректность не пострадает. Это и есть соблюдение принципа подстановки Лисков.

Зачем он нужен в iOS-разработке?

  1. Уменьшение связанности (Coupling): Код начинает зависеть от абстракций (протоколов), а не от конкретных реализаций.
  2. Повышение тестируемости: Возможность подменять реальные объекты (например, сетевой слой) моками в юнит-тестах напрямую следует из LSP.
  3. Облегчение рефакторинга и расширения: Добавление новых классов (например, нового типа кэша или источника данных) не требует изменений в существующем клиентском коде.
  4. Предсказуемость системы: Поведение подклассов остается в рамках ожиданий, определенных базовым классом или протоколом.

Вывод: Принцип Лисков (L) — это краеугольный камень для создания устойчивых, гибких иерархий наследования и корректного использования полиморфизма. В контексте Swift и iOS-разработки его соблюдение чаще всего достигается через грамотное проектирование протоколов (Protocols) и композицию, что позволяет избежать хрупких иерархий наследования классов. Нарушение LSP ведет к появлению скрытых багов, когда код, работающий с базовым типом, внезапно ломается при передаче ему экземпляра подкласса.