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

      Комментарии к записи Принцип подстановки Лисков отключены

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 3 нед., 5 дн. назад.

  • Автор
    Сообщения
  • #4031

    Принцип подстановки Лисков (LSP) звучит следующим образом: подтипы должны быть заменимы базовыми типами. Что это значит?

    В оригинальной формулировке пояснение принципа подстановки звучит так:

    если для каждого объекта o1 типа S существует объект o2 типа T, который для всех программ P определен в терминах T, то поведение P не изменится, если o1 заменить на o2 при условии, что S является подтипом T.

    Второе пояснение этого принципа уже более читабельное: функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

    Более простыми словами можно сказать, что поведение наследуемых классов не должно противоречить поведению, заданному базовым классом, т.е. поведение наследуемых классов должно быть ожидаемым для кода, использующего переменную базового типа.

    Перефразировав ещё раз, получаем следующее утверждение: передача наследника объекта вместо базового класса не должна разрушать никакую существующую функциональность в вызываемом методе. У вас должна быть возможность подстановки всех реализаций данного интерфейса.

    Чтобы всё вышесказанное не выглядело голословно, подкрепим теорию практическими примерами, и сначала посмотрим пример программы, в которой этот принцип подстановки нарушен. Он представляет базовый класс Bird (птица). Естественно, главное функциональное поведение для птицы – способность летать, поэтому метод, который реализован в этом классе – Fly().

    Два класса, производные от него — это Pegion (голубь) и Ostrich (страус). И тот, и другой являются птицами. В классе Pegion метод Fly() переопределяется по-своему )), а в классе Ostrich — нам приходится столкнуться с фактом, что, к сожалению, этот вид птиц не способны летать…

    using System;
    using System.Collections.Generic;
    
    class Bird
    {
       public virtual void Fly()
       {
          Console.WriteLine("Flying ....");
       }
    }
    class Pigeon: Bird
    {
       public override void Fly()
      {
          //implementing some flying abilities
          base.Fly();
          Console.WriteLine("...and shitting on your head wherever you go ;)");
       }
    }
    class Ostrich : Bird
    {
       public override void Fly()
       {
          //Oops....
         Console.WriteLine("I wish i could but ....");
       }
    }
    
    class Program
    {
       static void Main()
       {
           var birds = new List<Bird>();        
           birds.Add(new Pigeon());
           birds.Add(new Ostrich());        
           foreach (Bird i in birds)        
          {
             i.Fly();       }
       }
    }

    Когда мы тестируем работу всех трёх классов классов в точке входа – становится очевидным, что хотя синтаксически мы можем подставить экземпляры класса Ostrich везде, где ожидается экземпляр класса Bird, мы не можем рассчитывать на адекватное использование этой функциональной способности в данных наследниках. Получается, что в случае класса Ostrich, хоть он и является наследником класса Bird, нельзя полагаться на поведение, характерное для других видов птиц.

    Теперь посмотрим другой пример, в котором представлен класс для общего описания почтового адреса. В нём перечислены все атрибуты почтового адреса, такие как страна, город, почтовый индекс и прочее. Все эти атрибуты присутствуют в описании почтового адреса любой страны. Но в разных странах последовательность определения этих атрибутов своя.

    Далее, как мы, видим представлены 3 разных конкретных реализации заданного интерфейса, UkrainePostalAddress, UKPostalAddress, GermanyPostalAddress, где в методе WriteAddress() чётко определяется, как выглядит адрес Великобритании, Германии и Украины. В точке входа создаётся коллекция объектов-наследников класса PostalAddress и в цикле тестируется вызов метода WriteAddress() для каждого из них. Какова бы последовательность определения атрибутов ни была в каждом конкретном случае, это не меняет общий формат определения почтового адреса.

    Таким образом, где бы не использовали любую из конкретных реализаций почтового адреса, они выглядят совершенно однозначно, предсказуемо и корректно. Поэтому можно сделать вывод, что данный пример кода демонстрирует корректное использование принципа подстановки. Теперь посмотрим детали:

    using
     System;
    using System.Collections.Generic;
    
    abstract class PostalAddress
    {
       public string Addressee { get; set; }
       public string Country { get; set; }
       public string PostalCode { get; set; }
       public string City { get; set; }
       public string Street { get; set; }
       public int House { get; set; }
       public int Apartment { get; set; }
    
       public abstract string WriteAddress();
    }
    
    class UkrainePostalAddress : PostalAddress
    {
       public override string WriteAddress()
       {
          return String.Format("{0}\n{1}\n{2}\n{3} street, 
    {4}, apartment {5}\n{6}", Country, PostalCode, City, Street, 
     House, Apartment, Addressee);
       }
    }
    class UKPostalAddress : PostalAddress
    {
       public override string WriteAddress()
       {
           return String.Format("{0}\n{1}, 
    {2}-{3},\n{4}\n{5}\t{6}", Addressee, House, Street, Apartment, City, 
    PostalCode, Country);
       }
    }
    class GermanyPostalAddress : PostalAddress
    {
       public override string WriteAddress()
       {
          return String.Format("{0}\n{1} street, {2}, 
    apartment #{3},\n{4}\t{5}\n{6}", Addressee, Street, House, Apartment, 
    PostalCode, City, Country);
       }
    }
    class AddressWriter
    {
       public string PrintPostalAddress(PostalAddress writer)
       {
          return writer.WriteAddress();
       }
    }
    class Program
    {
       static void Main()
       {
          var writer = new AddressWriter();
          var ukrAddress = new UkrainePostalAddress() { 
    Country = "Ukraine", PostalCode = "61098", City = "Kharkiv", Street = 
    "Svobody", House = 12, Apartment = 42, Addressee = "Bella Earlich" };
           var ukAddress = new UKPostalAddress() { 
    Country = "UK", PostalCode = "GU177 7AS", City = "Aldershot", Street = 
    "Lovelyview Road", House = 7, Apartment = 1, Addressee = "Ian Durbridge"
     };
           var germanAddress = new 
    GermanyPostalAddress() { Country = "Deutschland", PostalCode = "12345", 
    City = "Berlin", Street = "Musterstr", Apartment = 120, House = 14, 
    Addressee = "Max Mustermann" };
    
           var postAddreses = new 
    List<PostalAddress>() { ukrAddress, ukAddress, germanAddress };
    
           foreach (var address in postAddreses)
          {
            
     Console.WriteLine(writer.PrintPostalAddress(address));
            
     Console.WriteLine("----------------------------");
          }
    
       }
    }

    Из примера видно, что класс AddressWriter готов принять в метод PrintPostalAddress() в качестве параметра любой класс, который реализует интерфейс PostalAddress.

    Надеюсь, из двух представленных в этой статье становится понятно, в чём заключается принцип подстановки (LSP) и в чём может заключаться его нарушение, то есть неправильное использование иерархии наследования.

Для ответа в этой теме необходимо авторизоваться.