Как связаны принцип Барбары Лисков и полиморфизм?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как связаны принцип Барбары Лисков и полиморфизм?
Принцип подстановки Лисков (Liskov Substitution Principle, LSP) — это фундаментальная основа правильного полиморфизма. Давайте разберёмся, как они связаны.
1. Определение LSP
Liskov Substitution Principle (LSP): "Функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом."
Другими словами: если S — подтип T, то объекты типа S должны без проблем подставляться вместо объектов типа T.
2. LSP как основа полиморфизма
Полиморфизм работает ТОЛЬКО если выполняется LSP:
# Базовый класс
class Animal:
def make_sound(self) -> str:
return 'Some sound'
# Правильный подтип (соблюдает LSP)
class Dog(Animal):
def make_sound(self) -> str:
return 'Woof!'
class Cat(Animal):
def make_sound(self) -> str:
return 'Meow!'
# Полиморфная функция
def animal_sounds(animals: list[Animal]) -> None:
for animal in animals:
print(animal.make_sound())
# Это работает потому что Dog и Cat соблюдают LSP
animals = [Dog(), Cat(), Dog()]
animal_sounds(animals) # Meow!, Woof!, Woof!
# Функция работает с любыми подтипами Animal
3. Нарушение LSP — проблемы с полиморфизмом
Пример нарушения LSP:
class Bird:
def fly(self) -> str:
return 'Flying...'
# ПЛОХО: Penguin нарушает LSP
class Penguin(Bird):
def fly(self) -> str:
raise NotImplementedError('Penguins cannot fly!')
# Полиморфная функция
def make_bird_fly(bird: Bird):
return bird.fly() # Ожидаем, что любая Bird может летать
# Падает!
bird = Penguin()
make_bird_fly(bird) # NotImplementedError!
# LSP нарушена: Penguin не может заменить Bird
Правильное решение:
# Правильно: разделяем иерархию
class Bird:
def move(self) -> str:
pass
class FlyingBird(Bird):
def fly(self) -> str:
return 'Flying...'
class NonFlyingBird(Bird):
def walk(self) -> str:
return 'Walking...'
class Penguin(NonFlyingBird):
def walk(self) -> str:
return 'Waddling...'
class Sparrow(FlyingBird):
def fly(self) -> str:
return 'Flying high!'
# Теперь LSP соблюдается
def make_flying_bird_fly(bird: FlyingBird):
return bird.fly()
make_flying_bird_fly(Sparrow()) # OK
make_flying_bird_fly(Penguin()) # Ошибка типа (правильно!)
4. LSP и контракты методов
LSP требует соблюдения контрактов — предусловий и постусловий:
class Rectangle:
def __init__(self, width: float, height: float):
self._width = width
self._height = height
def set_width(self, width: float) -> None:
self._width = width
def set_height(self, height: float) -> None:
self._height = height
def area(self) -> float:
return self._width * self._height
# ПЛОХО: нарушает контракт Rectangle
class Square(Rectangle):
def set_width(self, width: float) -> None:
# Квадрат: ширина == высота
self._width = width
self._height = width # Нарушение!
def set_height(self, height: float) -> None:
self._width = height
self._height = height
# Проблема
def calculate_area(rect: Rectangle) -> float:
rect.set_width(5)
rect.set_height(4)
# Ожидаем площадь 5 * 4 = 20
return rect.area()
print(calculate_area(Rectangle(0, 0))) # 20 ✓
print(calculate_area(Square(0, 0))) # 16 ✗ (5*4 but 4*4)
# LSP нарушена!
# Правильное решение
class Shape:
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self._width = width
self._height = height
def area(self) -> float:
return self._width * self._height
class Square(Shape):
def __init__(self, side: float):
self._side = side
def area(self) -> float:
return self._side * self._side
5. LSP и исключения
Потомок не должен выбрасывать неожиданные исключения:
class PaymentProcessor:
def process(self, amount: float) -> bool:
'''Обрабатывает платёж, возвращает успех'''
pass
# ПЛОХО: выбрасывает неожиданное исключение
class CreditCardProcessor(PaymentProcessor):
def process(self, amount: float) -> bool:
if amount > 1000:
raise ValueError('Amount too large') # Нарушение!
return True
# Клиентский код ломается
def pay(processor: PaymentProcessor, amount: float):
try:
result = processor.process(amount)
return result
except Exception as e:
print(f'Payment failed: {e}')
# Ожидали другие ошибки, не ValueError!
pay(CreditCardProcessor(), 1500) # Неожиданное исключение
# Правильно
class CreditCardProcessor(PaymentProcessor):
def process(self, amount: float) -> bool:
if amount > 1000:
return False # Возвращаем False, не выбрасываем
return True
6. LSP и return types (ковариантность)
Потомок может возвращать более специализированный тип (covariance):
class Animal:
pass
class Dog(Animal):
def bark(self):
print('Woof!')
# Правильно: Dog — подтип Animal
class AnimalShelter:
def get_animal(self) -> Animal:
return Animal()
class DogShelter(AnimalShelter):
def get_animal(self) -> Dog: # Более специализированный тип
return Dog()
# LSP соблюдается: Dog IS-A Animal
def adopt_pet(shelter: AnimalShelter):
pet = shelter.get_animal()
if isinstance(pet, Dog):
pet.bark()
adopt_pet(DogShelter()) # OK
7. LSP и parameter types (контравариантность)
Потомок может принимать более общие типы (contravariance):
class EventHandler:
def handle(self, event: 'ClickEvent') -> None:
pass
class ClickEvent:
pass
class CustomClickEvent(ClickEvent):
pass
# ПРАВИЛЬНО: принимаем более общий тип
class UniversalEventHandler(EventHandler):
def handle(self, event: 'Event') -> None: # Event — более общий
pass
class Event:
pass
# LSP соблюдается
def process_event(handler: EventHandler, event: ClickEvent):
handler.handle(event) # Работает с обоими
process_event(EventHandler(), ClickEvent())
process_event(UniversalEventHandler(), ClickEvent()) # OK
8. LSP и Design by Contract
Укрепляйте инварианты через контракты:
class Account:
def __init__(self, balance: float = 0):
self.balance = balance
def withdraw(self, amount: float) -> bool:
"""Снять деньги. Постусловие: баланс >= 0"""
if amount <= self.balance:
self.balance -= amount
return True
return False # Не может быть отрицательный баланс
# Потомок должен соблюдать контракт
class OverdraftAccount(Account):
def __init__(self, balance: float = 0, limit: float = 100):
super().__init__(balance)
self.overdraft_limit = limit
def withdraw(self, amount: float) -> bool:
"""Снять деньги с овердрафтом. Постусловие: баланс >= -limit"""
if amount <= self.balance + self.overdraft_limit:
self.balance -= amount
return True
return False # Усилили контракт, не ослабили
# Правильно: OverdraftAccount можно использовать везде где Account
def transfer(from_account: Account, to_account: Account, amount: float):
if from_account.withdraw(amount):
to_account.balance += amount
transfer(OverdraftAccount(100, 50), Account(50), 120) # OK
9. LSP и реальный пример: Django ORM
from django.db import models
# Базовый класс QuerySet
class QuerySet:
def filter(self, **kwargs):
return self # Возвращает QuerySet для chaining
def all(self):
return self
def count(self):
return 0
# Потомки соблюдают тот же контракт (LSP)
class Model:
objects: QuerySet # Manager, подтип QuerySet
class User(Model):
name = models.CharField()
email = models.CharField()
# Полиморфный код работает
def get_users(queryset: QuerySet):
return queryset.filter(active=True).count()
get_users(User.objects) # Работает благодаря LSP
10. Признаки нарушения LSP
# Антипаттерны
# 1. Type checking в клиентском коде
def process(obj: BaseClass):
if isinstance(obj, SpecificClass):
# Нарушение LSP!
specific_method()
else:
normal_method()
# 2. Проверка перед вызовом
def use_animal(animal: Animal):
if hasattr(animal, 'fly'): # Нарушение!
animal.fly()
# 3. Ловля специфичных исключений
def safe_operation(processor: Processor):
try:
processor.process()
except SpecificException: # Нарушение!
handle_specific_case()
Вывод
Связь LSP и полиморфизма:
- Полиморфизм работает только если LSP соблюдена
- LSP гарантирует предсказуемое поведение потомков
- Иерархия классов должна быть логичной (не Rectangle vs Square)
- Контракты методов должны усиливаться, не ослабляться
- Клиентский код НЕ должен знать о конкретных типах
Это делает код гибким, расширяемым и безопасным для расширения.