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

Что такое принцип Барбаре Лисков (Liskov Substitution Principle)?

2.0 Middle🔥 81 комментариев
#Архитектура и паттерны

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

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

Принцип Барбары Лисков (Liskov Substitution Principle)

Лисков Субституция (LSP) — третий принцип SOLID, сформулированный Барбарой Лисков в 1987 году. Звучит просто:

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

Другими словами: если класс S является подтипом класса T, то объекты типа T могут быть заменены объектами типа S без нарушения желаемых свойств программы.

Правило: замещаемость

# ✅ Хорошо — полиморфизм работает правильно
class Animal:
    def make_sound(self) -> str:
        raise NotImplementedError

class Dog(Animal):
    def make_sound(self) -> str:
        return "Woof!"

class Cat(Animal):
    def make_sound(self) -> str:
        return "Meow!"

def play_sound(animal: Animal):
    # Здесь может быть любое Animal — работает LSP
    print(animal.make_sound())

# Используем
dog = Dog()
cat = Cat()

play_sound(dog)  # Woof!
play_sound(cat)  # Meow!
# Обе замены работают правильно

Нарушение LSP — классический пример

# ❌ НАРУШЕНИЕ LSP
class Rectangle:
    def __init__(self, width: int, height: int):
        self._width = width
        self._height = height
    
    def set_width(self, width: int):
        self._width = width
    
    def set_height(self, height: int):
        self._height = height
    
    def get_area(self) -> int:
        return self._width * self._height

# Квадрат является прямоугольником? Математически да...
class Square(Rectangle):
    def set_width(self, width: int):
        # Но для квадрата оба измерения должны быть одинаковы
        self._width = width
        self._height = width  # 🚨 нарушаем ожидание от Rectangle
    
    def set_height(self, height: int):
        self._width = height
        self._height = height

# Клиент ожидает поведение Rectangle
def test_rectangle(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(3)
    assert rect.get_area() == 15  # 5 * 3 = 15

# Подаём Square вместо Rectangle — УПАЛ ТЕСТ!
square = Square(0, 0)
test_rectangle(square)
# square.get_area() == 9, а не 15 (3 * 3) 🔴

Проблема: Square нарушает контракт Rectangle. Клиентский код ломается.

Как исправить — правильная иерархия

# ✅ ИСПРАВЛЕННЫЙ КОД
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def get_area(self) -> int:
        pass

class Rectangle(Shape):
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
    
    def get_area(self) -> int:
        return self.width * self.height

class Square(Shape):
    def __init__(self, side: int):
        self.side = side
    
    def get_area(self) -> int:
        return self.side ** 2

# Теперь нет наследования Square от Rectangle
# Обе реализации удовлетворяют контракту Shape
shapes: list[Shape] = [
    Rectangle(5, 3),
    Square(4)
]

for shape in shapes:
    print(shape.get_area())  # 15, 16 — всё правильно

Практический пример: Database коннектор

from abc import ABC, abstractmethod

class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def execute(self, query: str):
        pass
    
    @abstractmethod
    def close(self):
        pass

class PostgresConnection(DatabaseConnection):
    def connect(self):
        print("Connecting to PostgreSQL...")
    
    def execute(self, query: str):
        print(f"Executing: {query}")
    
    def close(self):
        print("Closing PostgreSQL connection")

class MongoConnection(DatabaseConnection):
    def connect(self):
        print("Connecting to MongoDB...")
    
    def execute(self, query: str):
        print(f"Executing: {query}")
    
    def close(self):
        print("Closing MongoDB connection")

# Клиентский код не знает конкретной БД
class DataRepository:
    def __init__(self, db: DatabaseConnection):
        self.db = db
    
    def save_user(self, user_data):
        self.db.connect()
        self.db.execute(f"INSERT INTO users VALUES {user_data}")
        self.db.close()

# Используем с любой БД
postgres = PostgresConnection()
mongo = MongoConnection()

repo_pg = DataRepository(postgres)
repo_mongo = DataRepository(mongo)

# Обе работают одинаково для клиента
repo_pg.save_user({"name": "John"})
repo_mongo.save_user({"name": "John"})

Признаки нарушения LSP

# 1️⃣ Type-checking перед использованием
def process(animal: Animal):
    if isinstance(animal, Dog):
        # Специальная логика для Dog
        ...
    elif isinstance(animal, Cat):
        # Специальная логика для Cat
        ...
    # 🚨 Это красный флаг!

# 2️⃣ Пустые реализации методов
class Bird(Animal):
    def fly(self):
        raise NotImplementedError("This bird cannot fly")
        # 🚨 Нарушаем контракт

# 3️⃣ Усиление предусловий (требуем больше в подклассе)
class PaymentProcessor:
    def process_payment(self, amount: float):
        assert amount > 0  # предусловие

class SpecialPaymentProcessor(PaymentProcessor):
    def process_payment(self, amount: float):
        assert amount > 100  # 🚨 более сильное условие

# 4️⃣ Ослабление постусловий (гарантируем меньше в подклассе)
class Logger:
    def log(self, message: str) -> str:
        # Гарантирует, что вернёт логированное сообщение
        return f"[LOG] {message}"

class SilentLogger(Logger):
    def log(self, message: str) -> str:
        return None  # 🚨 нарушили постусловие

Как проверить соответствие LSP

# ✅ Правильный тест
def test_liskov_substitution():
    animals: list[Animal] = [Dog(), Cat(), Bird()]
    
    # Все работают с одним кодом
    for animal in animals:
        sound = animal.make_sound()
        assert isinstance(sound, str)
        assert len(sound) > 0
    # Тест проходит — LSP соблюдается

# ❌ Тест провалится если подкласс нарушает LSP
def test_rectangle_square():
    shapes: list[Rectangle] = [Rectangle(5, 3), Square(4)]
    
    for shape in shapes:
        shape.set_width(10)
        shape.set_height(5)
        assert shape.get_area() == 50  # FAIL для Square!

LSP — это гарант того, что полиморфизм работает правильно. Если соблюдать этот принцип, код становится предсказуемым и безопасным для расширения через наследование.