Перейти к содержанию

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

Принцип подстановки Лисков (Liskov Substitution Principle) для многих - один из самых сложных принципов. Принцип подстановки Лисков гласит: «Объекты в программе должны быть заменяемы экземплярами их подтипов без ущерба корректности работы программы». Принцип подстановки Лисков был предложен Барбарой Лисков. Он предполагает отношение подтипов, называемое сильным поведенческим подтипом. Этот принцип говорит нам о том, что если класс Sub является подтипом класса Sup, тогда в программе объекты типа Sup должны легко заменяться объектами типа Sub без необходимости изменения кода. Дядя Боб включил этот принцип в число 5 лучших принципов проектирования SOLID. Допустим, у нас есть базовый класс Car, который отвечает за тип автомобиля. Класс Car наследуется подклассом PetrolCar. Аналогично, базовый класс Car может быть унаследован другими классами, которые могут расширять его возможности.

class Car():
    def __init__(self, type):
        self.type = type

class PetrolCar(Car):
    def __init__(self, type):
        self.type = type


car = Car("SUV")
car.properties = {"Color": "Red", "Gear": "Auto", "Capacity": 6}

petrol_car = PetrolCar("Sedan")
petrol_car.properties = ("Blue", "Manual", 4)

cars = [car, petrol_car]

def find_red_cars(cars):
red_cars = 0
for car in cars:
    if car.properties['Color'] == "Red":
        red_cars += 1
print(f'Number of Red Cars = {red_cars}')

find_red_cars(cars)
<?php
class Car {
    public array $properties = [];
    public string $type;

    public function __construct(string $type) {
        $this->type = $type;
    }
}

class PetrolCar extends Car {}

$car = new Car("SUV");
$car->properties = [
    "Color" => "Red",
    "Gear" => "Auto",
    "Capacity" => 6
];

$petrol_car = new PetrolCar("Sedan");
$petrol_car->properties = ["Blue", "Manual", 4];

$cars = [$car, $petrol_car];

function find_red_cars($cars) {
    $red_cars = 0;
    foreach ($cars as $car) {
        if ($car->properties['Color'] === "Red") {
            $red_cars += 1;
        }
    }
    echo 'Number of Red Cars = ' . $red_cars;
}

find_red_cars($cars);
Warning: Undefined array key "Color" in /home/user/scripts/code.php on line 29
Number of Red Cars = 1

Как видно из кода, мы пытаемся просмотреть список объектов Car. Именно здесь мы нарушаем принцип подстановки Лисков, поскольку мы не можем заменить объекты супертипа Car объектами подтипа PetrolCar внутри функции поиска красных автомобилей. Лучшим вариантом было бы реализовать методы setter и getter в суперклассе Car. С их помощью мы можем устанавливать и получать свойства автомобиля, не оставляя эту реализацию последующим разработчикам. Таким образом, мы просто получаем свойства с помощью метода setter, и его реализация остается инкапсулированной в суперклассе. Так мы сможем соблюсти принцип подстановки Лисков, как показано ниже:

class Car():
    def __init__(self, type):
        self.type = type
        self.car_properties = {}

    def set_properties(self, color, gear, capacity):
        self.car_properties = {"Color": color, "Gear": gear, "Capacity": capacity}

    def get_properties(self):
        return self.car_properties

class PetrolCar(Car):
    def __init__(self, type):
        self.type = type
        self.car_properties = {}


car = Car("SUV")
car.set_properties("Red", "Auto", 6)

petrol_car = PetrolCar("Sedan")
petrol_car.set_properties("Blue", "Manual", 4)

cars = [car, petrol_car]

def find_red_cars(cars):
    red_cars = 0
    for car in cars:
        if car.get_properties()['Color'] == "Red":
            red_cars += 1
    print(f'Number of Red Cars = {red_cars}')

find_red_cars(cars)
<?php
class Car {
    protected array $properties = [];
    public string $type;

    public function __construct(string $type) {
        $this->type = $type;
    }

    public function set_properties(string $color, string $gear, int $capacity) {
        $this->properties['Color'] = $color;
        $this->properties['Gear'] = $gear;
        $this->properties['Capacity'] = $capacity;
    }

    public function get_properties() {
        return $this->properties;
    }
}

class PetrolCar extends Car {}

$car = new Car("SUV");
$car->set_properties("Red", "Auto", 6);

$petrol_car = new PetrolCar("Sedan");
$petrol_car->set_properties("Blue", "Manual", 4);

$cars = [$car, $petrol_car];

function find_red_cars($cars) {
    $red_cars = 0;
    foreach ($cars as $car) {
        if ($car->get_properties()['Color'] === "Red") {
            $red_cars += 1;
        }
    }
    echo 'Number of Red Cars = ' . $red_cars;
}

find_red_cars($cars);
Number of Red Cars = 1

Задачи

Задача 1. Птицы и полёт

Класс Bird имеет метод fly(). Подкласс Penguin наследует Bird, но не умеет летать и бросает исключение. Нарушает ли это LSP? Перепроектируйте иерархию.

# Нарушение LSP — Penguin нельзя подставить вместо Bird
class Bird:
    def fly(self) -> str:
        return "Лечу!"

class Sparrow(Bird):
    def fly(self) -> str:
        return "Воробей летит"

class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("Пингвины не летают!")

def make_birds_fly(birds: list[Bird]):
    for bird in birds:
        print(bird.fly())

# Вызов
birds = [Sparrow(), Penguin()]
make_birds_fly(birds)  # упадёт на Penguin
<?php
// Нарушение LSP — Penguin нельзя подставить вместо Bird
class Bird {
    public function fly(): string {
        return "Лечу!";
    }
}

class Sparrow extends Bird {
    public function fly(): string {
        return "Воробей летит";
    }
}

class Penguin extends Bird {
    public function fly(): string {
        throw new \RuntimeException("Пингвины не летают!");
    }
}

function make_birds_fly(array $birds): void {
    foreach ($birds as $bird) {
        echo $bird->fly() . PHP_EOL;
    }
}

// Вызов
$birds = [new Sparrow(), new Penguin()];
make_birds_fly($birds); // упадёт на Penguin

Задача 2. Прямоугольник и квадрат

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

# Нарушение LSP
class Rectangle:
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def set_width(self, width: float):
        self.width = width

    def set_height(self, height: float):
        self.height = height

    def area(self) -> float:
        return self.width * self.height

class Square(Rectangle):
    def set_width(self, width: float):
        self.width = width
        self.height = width  # нарушает инвариант Rectangle

    def set_height(self, height: float):
        self.width = height
        self.height = height  # нарушает инвариант Rectangle

def print_area(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(10)
    print(f"Площадь: {rect.area()}")  # ожидаем 50

# Вызов
print_area(Rectangle(2, 3))  # Площадь: 50
print_area(Square(2, 2))     # Площадь: 100 — неожиданный результат!
<?php
// Нарушение LSP
class Rectangle {
    public function __construct(
        protected float $width,
        protected float $height
    ) {}

    public function set_width(float $width): void { $this->width = $width; }
    public function set_height(float $height): void { $this->height = $height; }

    public function area(): float {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle {
    public function set_width(float $width): void {
        $this->width = $width;
        $this->height = $width; // нарушает инвариант Rectangle
    }

    public function set_height(float $height): void {
        $this->width = $height;
        $this->height = $height; // нарушает инвариант Rectangle
    }
}

function print_area(Rectangle $rect): void {
    $rect->set_width(5);
    $rect->set_height(10);
    echo "Площадь: " . $rect->area() . PHP_EOL; // ожидаем 50
}

// Вызов
print_area(new Rectangle(2, 3)); // Площадь: 50
print_area(new Square(2, 2));    // Площадь: 100 — неожиданный результат!

Задача 3. Читатель файлов

Класс FileReader читает файл и возвращает содержимое. Подкласс NetworkFileReader переопределяет метод, но при отсутствии сети возвращает null вместо строки. Исправьте иерархию так, чтобы LSP соблюдался.

# Нарушение LSP — подкласс меняет тип возвращаемого значения
class FileReader:
    def read(self, path: str) -> str:
        return f"Содержимое файла: {path}"

class NetworkFileReader(FileReader):
    def __init__(self, available: bool):
        self.available = available

    def read(self, path: str) -> str | None:  # нарушение: может вернуть None
        if not self.available:
            return None  # вызывающий код не ожидает None
        return f"Содержимое по сети: {path}"

def process(reader: FileReader, path: str):
    content = reader.read(path)
    print(content.upper())  # упадёт, если content == None

# Вызов
process(FileReader(), "local.txt")
process(NetworkFileReader(available=False), "remote.txt")  # AttributeError
<?php
// Нарушение LSP — подкласс меняет контракт метода
class FileReader {
    public function read(string $path): string {
        return "Содержимое файла: {$path}";
    }
}

class NetworkFileReader extends FileReader {
    public function __construct(private bool $available) {}

    public function read(string $path): ?string { // нарушение: может вернуть null
        if (!$this->available) {
            return null; // вызывающий код не ожидает null
        }
        return "Содержимое по сети: {$path}";
    }
}

function process(FileReader $reader, string $path): void {
    $content = $reader->read($path);
    echo strtoupper($content) . PHP_EOL; // упадёт, если content === null
}

// Вызов
process(new FileReader(), "local.txt");
process(new NetworkFileReader(false), "remote.txt"); // TypeError

Задача 4. Аккаунты пользователей

Класс UserAccount имеет метод get_discount(), возвращающий скидку. Подкласс GuestAccount переопределяет его и бросает исключение, так как гостям скидки не положены. Устраните нарушение LSP.

# Нарушение LSP
class UserAccount:
    def __init__(self, name: str):
        self.name = name

    def get_discount(self) -> float:
        return 0.10

class PremiumAccount(UserAccount):
    def get_discount(self) -> float:
        return 0.25

class GuestAccount(UserAccount):
    def get_discount(self) -> float:
        raise PermissionError("Гостям скидки недоступны!")

def print_discounts(accounts: list[UserAccount]):
    for acc in accounts:
        print(f"{acc.name}: скидка {acc.get_discount() * 100}%")

# Вызов
accounts = [UserAccount("Анна"), PremiumAccount("Иван"), GuestAccount("Гость")]
print_discounts(accounts)  # упадёт на GuestAccount
<?php
// Нарушение LSP
class UserAccount {
    public function __construct(public readonly string $name) {}

    public function get_discount(): float {
        return 0.10;
    }
}

class PremiumAccount extends UserAccount {
    public function get_discount(): float {
        return 0.25;
    }
}

class GuestAccount extends UserAccount {
    public function get_discount(): float {
        throw new \RuntimeException("Гостям скидки недоступны!");
    }
}

function print_discounts(array $accounts): void {
    foreach ($accounts as $acc) {
        echo "{$acc->name}: скидка " . ($acc->get_discount() * 100) . "%" . PHP_EOL;
    }
}

// Вызов
$accounts = [new UserAccount("Анна"), new PremiumAccount("Иван"), new GuestAccount("Гость")];
print_discounts($accounts); // упадёт на GuestAccount

Задача 5. Хранилища данных

Базовый класс Storage имеет методы read() и write(). Подкласс ReadOnlyStorage наследует его, но метод write() бросает исключение. Перепроектируйте иерархию, соблюдая LSP.

# Нарушение LSP — подкласс сужает контракт базового класса
class Storage:
    def read(self, key: str) -> str:
        return f"Данные по ключу {key}"

    def write(self, key: str, value: str):
        print(f"Запись: {key} = {value}")

class ReadOnlyStorage(Storage):
    def write(self, key: str, value: str):
        raise PermissionError("Хранилище доступно только для чтения!")

def save_data(storage: Storage, key: str, value: str):
    storage.write(key, value)
    print(storage.read(key))

# Вызов
save_data(Storage(), "user", "Алексей")
save_data(ReadOnlyStorage(), "user", "Алексей")  # PermissionError
<?php
// Нарушение LSP — подкласс сужает контракт базового класса
class Storage {
    public function read(string $key): string {
        return "Данные по ключу {$key}";
    }

    public function write(string $key, string $value): void {
        echo "Запись: {$key} = {$value}" . PHP_EOL;
    }
}

class ReadOnlyStorage extends Storage {
    public function write(string $key, string $value): void {
        throw new \RuntimeException("Хранилище доступно только для чтения!");
    }
}

function save_data(Storage $storage, string $key, string $value): void {
    $storage->write($key, $value);
    echo $storage->read($key) . PHP_EOL;
}

// Вызов
save_data(new Storage(), "user", "Алексей");
save_data(new ReadOnlyStorage(), "user", "Алексей"); // RuntimeException