Можешь привести пример использования принципа L в SOLID
Комментарии (3)
Ответ сгенерирован нейросетью и может содержать ошибки
Пример принципа подстановки Лисков (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:
- Безопасность рефакторинга - можно заменять реализации без страха сломать систему
- Тестируемость - моки и стабы работают корректно
- Расширяемость - новые подклассы легко интегрируются
- Полиморфизм - возможность использовать общий интерфейс для различных реализаций
Нарушение LSP часто маскируется под "особенности реализации", но на деле приводит к хрупкому коду, где знание о конкретных типах проникает в клиентский код через проверки instanceof или when. Соблюдение принципа подстановки Лисков делает систему более устойчивой к изменениям и легче поддерживаемой.