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

Можешь привести пример использования принципа L в SOLID

2.8 Senior🔥 163 комментариев
#UI и вёрстка#Жизненный цикл и навигация

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

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

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

Пример принципа подстановки Лисков (LSP)

Принцип подстановки Лисков (Liskov Substitution Principle - LSP) гласит, что объекты суперкласса должны быть заменяемы объектами подклассов без изменения корректности программы. Нарушение LSP часто приводит к нарушению инвариантов, появлению исключительных ситуаций или неожиданному поведению системы.

Классический антипаттерн, нарушающий LSP

Рассмотрим пример с геометрическими фигурами, где квадрат наследуется от прямоугольника:

// НЕПРАВИЛЬНАЯ реализация - нарушает LSP
open class Rectangle {
    open var width: Double = 0.0
    open var height: Double = 0.0
    
    fun area(): Double = width * height
}

class Square : Rectangle() {
    override var width: Double
        get() = super.width
        set(value) {
            super.width = value
            super.height = value // Нарушение инварианта прямоугольника!
        }
    
    override var height: Double
        get() = super.height
        set(value) {
            super.height = value
            super.width = value // Нарушение инварианта прямоугольника!
        }
}

Проблема: Квадрат математически является прямоугольником, но в ООП эта иерархия приводит к нарушению LSP. Клиентский код, ожидающий Rectangle, может сломаться при работе с Square:

fun resizeRectangle(rectangle: Rectangle) {
    // Предполагается, что ширина и высота изменяются независимо
    rectangle.width = 5.0
    rectangle.height = 4.0
    
    // Ожидаемая площадь: 20.0
    // Для Square площадь будет: 16.0 (последнее присвоение изменило обе стороны!)
    println("Area: ${rectangle.area()}")
}

Правильная реализация с соблюдением LSP

Есть несколько подходов для соблюдения LSP:

1. Общий интерфейс для фигур

interface Shape {
    fun area(): Double
    fun perimeter(): Double
}

class Rectangle(private val width: Double, private val height: Double) : Shape {
    override fun area(): Double = width * height
    override fun perimeter(): Double = 2 * (width + height)
}

class Square(private val side: Double) : Shape {
    override fun area(): Double = side * side
    override fun perimeter(): Double = 4 * side
}

2. Использование паттерна "Спецификация"

// Базовый интерфейс для всех фигур
interface Shape {
    fun area(): Double
}

// Конкретные реализации независимы друг от друга
class Rectangle(val width: Double, val height: Double) : Shape {
    override fun area(): Double = width * height
}

class Square(val side: Double) : Shape {
    override fun area(): Double = side * side
}

// Общий код работает с интерфейсом Shape
class AreaCalculator {
    fun totalArea(shapes: List<Shape>): Double {
        return shapes.sumOf { it.area() }
    }
}

3. Практический пример из Android-разработки

Рассмотрим иерархию View в Android, которая правильно соблюдает LSP:

// Все View-компоненты могут быть отрисованы и измерены
abstract class View {
    abstract fun draw(canvas: Canvas)
    abstract fun measure(width: Int, height: Int)
    open fun setOnClickListener(listener: OnClickListener) {
        // Базовая реализация
    }
}

// TextView и Button могут использоваться взаимозаменяемо там, где ожидается View
class TextView : View() {
    override fun draw(canvas: Canvas) {
        // Отрисовка текста
    }
    
    override fun measure(width: Int, height: Int) {
        // Измерение текста
    }
    
    fun setText(text: String) {
        // Специфичный для TextView метод
    }
}

class Button : View() {
    override fun draw(canvas: Canvas) {
        // Отрисовка кнопки
    }
    
    override fun measure(width: Int, height: Int) {
        // Измерение кнопки
    }
    
    override fun setOnClickListener(listener: OnClickListener) {
        // Button может расширять поведение кликов
        super.setOnClickListener(listener)
        // Дополнительная логика
    }
}

// Клиентский код работает с базовым классом View
fun renderViews(views: List<View>) {
    views.forEach { view ->
        view.measure(100, 50)
        view.draw(Canvas())
        // Мы можем безопасно вызывать setOnClickListener для любого View
        view.setOnClickListener {
            println("View clicked")
        }
    }
}

Ключевые признаки соблюдения LSP:

  • Предусловия не усиливаются в подклассах
  • Постусловия не ослабляются в подклассах
  • Инварианты суперкласса сохраняются
  • История объекта не нарушается
  • Исключения того же или более конкретного типа

Практические преимущества LSP в Android:

  1. Безопасность рефакторинга - можно заменять реализации без страха сломать систему
  2. Тестируемость - моки и стабы работают корректно
  3. Расширяемость - новые подклассы легко интегрируются
  4. Полиморфизм - возможность использовать общий интерфейс для различных реализаций

Нарушение LSP часто маскируется под "особенности реализации", но на деле приводит к хрупкому коду, где знание о конкретных типах проникает в клиентский код через проверки instanceof или when. Соблюдение принципа подстановки Лисков делает систему более устойчивой к изменениям и легче поддерживаемой.