Принцип подстановки Лисков (Liskov Substitution Principle) для многих - один из самых сложных принципов. Принцип подстановки Лисков гласит: «Объекты в программе должны быть заменяемы экземплярами их подтипов без ущерба корректности работы программы». Принцип подстановки Лисков был предложен Барбарой Лисков. Он предполагает отношение подтипов, называемое сильным поведенческим подтипом. Этот принцип говорит нам о том, что если класс Sub является подтипом класса Sup, тогда в программе объекты типа Sup должны легко заменяться объектами типа Sub без необходимости изменения кода. Дядя Боб включил этот принцип в число 5 лучших принципов проектирования SOLID. Допустим, у нас есть базовый класс Car, который отвечает за тип автомобиля. Класс Car наследуется подклассом PetrolCar. Аналогично, базовый класс Car может быть унаследован другими классами, которые могут расширять его возможности.
classCar():def__init__(self,type):self.type=typeclassPetrolCar(Car):def__init__(self,type):self.type=typecar=Car("SUV")car.properties={"Color":"Red","Gear":"Auto","Capacity":6}petrol_car=PetrolCar("Sedan")petrol_car.properties=("Blue","Manual",4)cars=[car,petrol_car]deffind_red_cars(cars):red_cars=0forcarincars:ifcar.properties['Color']=="Red":red_cars+=1print(f'Number of Red Cars = {red_cars}')find_red_cars(cars)
<?phpclassCar{publicarray$properties=[];publicstring$type;publicfunction__construct(string$type){$this->type=$type;}}classPetrolCarextendsCar{}$car=newCar("SUV");$car->properties=["Color"=>"Red","Gear"=>"Auto","Capacity"=>6];$petrol_car=newPetrolCar("Sedan");$petrol_car->properties=["Blue","Manual",4];$cars=[$car,$petrol_car];functionfind_red_cars($cars){$red_cars=0;foreach($carsas$car){if($car->properties['Color']==="Red"){$red_cars+=1;}}echo'Number of Red Cars = '.$red_cars;}find_red_cars($cars);
Как видно из кода, мы пытаемся просмотреть список объектов Car. Именно здесь мы нарушаем принцип подстановки Лисков, поскольку мы не можем заменить объекты супертипа Car объектами подтипа PetrolCar внутри функции поиска красных автомобилей. Лучшим вариантом было бы реализовать методы setter и getter в суперклассе Car. С их помощью мы можем устанавливать и получать свойства автомобиля, не оставляя эту реализацию последующим разработчикам. Таким образом, мы просто получаем свойства с помощью метода setter, и его реализация остается инкапсулированной в суперклассе. Так мы сможем соблюсти принцип подстановки Лисков, как показано ниже:
classCar():def__init__(self,type):self.type=typeself.car_properties={}defset_properties(self,color,gear,capacity):self.car_properties={"Color":color,"Gear":gear,"Capacity":capacity}defget_properties(self):returnself.car_propertiesclassPetrolCar(Car):def__init__(self,type):self.type=typeself.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]deffind_red_cars(cars):red_cars=0forcarincars:ifcar.get_properties()['Color']=="Red":red_cars+=1print(f'Number of Red Cars = {red_cars}')find_red_cars(cars)
<?phpclassCar{protectedarray$properties=[];publicstring$type;publicfunction__construct(string$type){$this->type=$type;}publicfunctionset_properties(string$color,string$gear,int$capacity){$this->properties['Color']=$color;$this->properties['Gear']=$gear;$this->properties['Capacity']=$capacity;}publicfunctionget_properties(){return$this->properties;}}classPetrolCarextendsCar{}$car=newCar("SUV");$car->set_properties("Red","Auto",6);$petrol_car=newPetrolCar("Sedan");$petrol_car->set_properties("Blue","Manual",4);$cars=[$car,$petrol_car];functionfind_red_cars($cars){$red_cars=0;foreach($carsas$car){if($car->get_properties()['Color']==="Red"){$red_cars+=1;}}echo'Number of Red Cars = '.$red_cars;}find_red_cars($cars);
NumberofRedCars=1
Задачи
Задача 1. Птицы и полёт
Класс Bird имеет метод fly(). Подкласс Penguin наследует Bird, но не умеет летать и бросает исключение. Нарушает ли это LSP? Перепроектируйте иерархию.
# Нарушение LSP — Penguin нельзя подставить вместо BirdclassBird:deffly(self)->str:return"Лечу!"classSparrow(Bird):deffly(self)->str:return"Воробей летит"classPenguin(Bird):deffly(self):raiseNotImplementedError("Пингвины не летают!")defmake_birds_fly(birds:list[Bird]):forbirdinbirds:print(bird.fly())# Вызовbirds=[Sparrow(),Penguin()]make_birds_fly(birds)# упадёт на Penguin
<?php// Нарушение LSP — Penguin нельзя подставить вместо BirdclassBird{publicfunctionfly():string{return"Лечу!";}}classSparrowextendsBird{publicfunctionfly():string{return"Воробей летит";}}classPenguinextendsBird{publicfunctionfly():string{thrownew\RuntimeException("Пингвины не летают!");}}functionmake_birds_fly(array$birds):void{foreach($birdsas$bird){echo$bird->fly().PHP_EOL;}}// Вызов$birds=[newSparrow(),newPenguin()];make_birds_fly($birds);// упадёт на Penguin
Задача 2. Прямоугольник и квадрат
Классический пример нарушения LSP: Square наследует Rectangle и переопределяет сеттеры так, что инвариант прямоугольника нарушается. Продемонстрируйте проблему и предложите корректную структуру.
Класс FileReader читает файл и возвращает содержимое. Подкласс NetworkFileReader переопределяет метод, но при отсутствии сети возвращает null вместо строки. Исправьте иерархию так, чтобы LSP соблюдался.
# Нарушение LSP — подкласс меняет тип возвращаемого значенияclassFileReader:defread(self,path:str)->str:returnf"Содержимое файла: {path}"classNetworkFileReader(FileReader):def__init__(self,available:bool):self.available=availabledefread(self,path:str)->str|None:# нарушение: может вернуть Noneifnotself.available:returnNone# вызывающий код не ожидает Nonereturnf"Содержимое по сети: {path}"defprocess(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 — подкласс меняет контракт методаclassFileReader{publicfunctionread(string$path):string{return"Содержимое файла: {$path}";}}classNetworkFileReaderextendsFileReader{publicfunction__construct(privatebool$available){}publicfunctionread(string$path):?string{// нарушение: может вернуть nullif(!$this->available){returnnull;// вызывающий код не ожидает null}return"Содержимое по сети: {$path}";}}functionprocess(FileReader$reader,string$path):void{$content=$reader->read($path);echostrtoupper($content).PHP_EOL;// упадёт, если content === null}// Вызовprocess(newFileReader(),"local.txt");process(newNetworkFileReader(false),"remote.txt");// TypeError
Задача 4. Аккаунты пользователей
Класс UserAccount имеет метод get_discount(), возвращающий скидку. Подкласс GuestAccount переопределяет его и бросает исключение, так как гостям скидки не положены. Устраните нарушение LSP.
<?php// Нарушение LSPclassUserAccount{publicfunction__construct(publicreadonlystring$name){}publicfunctionget_discount():float{return0.10;}}classPremiumAccountextendsUserAccount{publicfunctionget_discount():float{return0.25;}}classGuestAccountextendsUserAccount{publicfunctionget_discount():float{thrownew\RuntimeException("Гостям скидки недоступны!");}}functionprint_discounts(array$accounts):void{foreach($accountsas$acc){echo"{$acc->name}: скидка ".($acc->get_discount()*100)."%".PHP_EOL;}}// Вызов$accounts=[newUserAccount("Анна"),newPremiumAccount("Иван"),newGuestAccount("Гость")];print_discounts($accounts);// упадёт на GuestAccount
Задача 5. Хранилища данных
Базовый класс Storage имеет методы read() и write(). Подкласс ReadOnlyStorage наследует его, но метод write() бросает исключение. Перепроектируйте иерархию, соблюдая LSP.
# Нарушение LSP — подкласс сужает контракт базового классаclassStorage:defread(self,key:str)->str:returnf"Данные по ключу {key}"defwrite(self,key:str,value:str):print(f"Запись: {key} = {value}")classReadOnlyStorage(Storage):defwrite(self,key:str,value:str):raisePermissionError("Хранилище доступно только для чтения!")defsave_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 — подкласс сужает контракт базового классаclassStorage{publicfunctionread(string$key):string{return"Данные по ключу {$key}";}publicfunctionwrite(string$key,string$value):void{echo"Запись: {$key} = {$value}".PHP_EOL;}}classReadOnlyStorageextendsStorage{publicfunctionwrite(string$key,string$value):void{thrownew\RuntimeException("Хранилище доступно только для чтения!");}}functionsave_data(Storage$storage,string$key,string$value):void{$storage->write($key,$value);echo$storage->read($key).PHP_EOL;}// Вызовsave_data(newStorage(),"user","Алексей");save_data(newReadOnlyStorage(),"user","Алексей");// RuntimeException