Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Zasada zastąpień Barbary Liskov

W 1988 roku Barbara Liskov [1] sformułowała zasadę zastąpień. Brzmi ona następująco:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is subtype of T.

Definicja ta mówi, że metoda przyjmująca jako parametr egzemplarz klasy A musi działać poprawnie także dla wszystkich klas, które dziedziczą po A, nieznając ich typów.

Ciekawa zależność ujawnia się, gdy przeanalizujemy metodę, która nie spełnia tego warunku. Taka metoda przyjmuje jako parametr egzemplarz pewnej klasy, ale żeby działać poprawnie musi znać jego dokładny typ. To oczywiście jest pogwałceniem zasady otwarty-zamknięty, gdyż taka metoda będzie zmieniana za każdym razem, gdy będziemy dodawać nową klasę dziedziczącą po typie parametru.

Wzorcowe rozwiązanie tego problemu używa takich właściwości programowania obiektowego jak abstrakcja i polimorfizm. W językach obiektowych tych własności dostarcza dziedziczenie. Zasada zastąpień B. Liskov odpowiada na kilka pytań dotyczących wykorzystywania dziedziczenia. Określa, jakie są cechy charakterystyczne najlepszej hierarchii dziedziczenia. Wskazuje przyczyny tego, co może sprawiać, że utworzona hierarchia dziedziczenia nie jest zgodna z zasadą otwarty-zamknięty.

Klasycznym przykładem obrazującym zasadę zastąpień B. Liskov jest hierarchia figur geometrycznych a w szczególności relacja między prostokątem i kwadratem. Z matematycznego punktu widzenia każdy kwadrat jest prostokątem. To stwierdzenie szybko utrwala nas w przekonaniu, że w hierarchii dziedziczenia, kwadrat powinien być podklasą prostokątu. Przyjrzyjmy się przykładowej realizacji, którą prezentuje przykład 1.

Przykład [C#] 1. Przykładowa realizacja klasy reprezentującej prostokąt.
public class Rectangle : IRectangle
{
    private double width;
    private double height;
    public virtual double Width
    {
        get { return width; }
        set { width = value; }
    }
   
    public virtual double Height
    {
        get { return height; }
        set { height = value; }
    }
}


Przykład [C#] 2. Interfejs klasy reprezentującej prostokąt.
public interface IRectangle
{
    double Width { get; set; }
    double Height { get; set; }
}


Mając przed oczami przykład 1 zastanówmy się jak będzie wyglądała klasa Square dziedzicząca po klasie Rectangle. Bedzie miała własności takie jak prostokąt, czyli wysokść i szerokość. Ale czy to nie za dużo? Przecież kwadrat ma zawsze wysokość i szerokość taką samą. Tą niedogodność możemy usnąć zmieniając realizacje własciwości Width i Height (mimo, że są zbędne nie możemy ich usunąć, gdyż są dziedziczone z klasy Rectangle). Realizację klasy Square przedstawiono w przykładzie 3.

Przykład [C#] 3. Przykładowa realizacja klasy reprezentującej kwadrat.
public class Square : Rectangle
{
    public override double Width
    {
        get { return base.Width; }
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }
    public override double Height
    {
        get { return base.Height; }
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }
}


W klasie Square z przykładu 3 zmieniono realizacje właściwości Width i Height. Już w klasie Rectangle poczyniono pewne założenia i właściwości te oznaczono jako wirtualne. Taka realizacja zapewnia, że zmiana wysokości kwadratu zmienia również jego szerokość i vice versa. Zdefiniowaliśmy zatem hierarchię dziedziczenia, w której klasa Rectangle jest klasą bazową, a klasa Square dziedziczy po niej. Teraz w kontekście zasady zastapień B. Liskov przeanalizujmy metodę z przykładu 4.

Przykład [C#] 4. Metoda naruszająca zasadę zastąpień B. Liskov.
public void AdjustWidth( Rectangle rct, double aspectRatio )
{
    rct.Width = rct.Height * aspectRatio;
    Debug.Assert( rct.Width / rct.Height == aspectRatio );
}


Zadaniem metody z przykładu 4 jest dostosowanie szerokości prostokątu tak, aby jego wysokość i szerokość były w stosunku podanym jako drugi parametr. Metoda ta jako pierwszy swój parametr przyjmuje egzemplarz klasy Rectangle. Zgodnie z zasadą zastapień B. Liskov powinna działać poprawnie również dla klas dziedziczących po tej klasie. W naszym przypadku klasą potomną jest klasa Square.

Czy metoda ta rzeczywiście zadziała? Otóz nie! Jak łatwo zauważyć, podając stosunek inny niż 1 oraz egzemplarz klasy Square, asercja nie będzie spełniona. Zatem jaki błąd popełniono? Czy programista pisząc tą metodę mógł założyć, że asercja zawsze będzie spełniona?

Spójrzmy na ten problem z trochę innej strony. Bertrand Meyer [2] pisze o projektowaniu według umowy (ang. Design by Contract, DbC). Technika ta ma swoje korzenie w formalnych metodach weryfikacji programów. Rozszerza ona definicję metody o warunki początkowe i końcowe (kontrakt) oraz klasy o niezmienniki. Warunek początkowy (ang. precondition) określa warunek, jaki klient musi spełnić, aby wykonanie metody przebiegło pomyślnie. Natomiast warunek końcowy (ang. postcondition) określa warunek, jaki będzie spełniony po wykonaniu metody. Ostatnim elementem jest niezmiennik klasy (ang. class invariant), który określa warunek, który dla danej klasy musi być zawsze spełniony.

Wracając do metody z przykładu 4 zastanówmy się, jaki warunek końcowy powinna mieć operacja przypisywania szerokości prostokątowi? Otóż powinna zagwarantować, że szerokość kwadratu zostanie zmieniona na nową wartość, natomiast jego wysokość pozostanie niezmieniona. Jak łatwo zauważyć, warunek ten nie jest spełniony w wypadku realizacji właściwości Width w klasie Square. Bertrand Meyer[2] pisze:

When redefining a routine [in derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.

Przeciażenie właściwości Width klasy Square nie jest zgodne z tym co pisze Meyer. Warunek końcowy tej właściwości jest słabszy od warunku końcowego właściwości Width z klasy bazowej, gdyż nie zapewnia niezmienności wysokości. Czy wynika z tego, że kwadrat nie jest prostokątem? Otóz nie! Oczywiście kwadrat jest prostokątem, natomiast nie jest nim z punktu widzenia dziedziczenia. Należy podkreślić zasadniczą róznicę. Zachowanie klasy Square jest inne niż klasy Rectangle. Aby być zgodnym z zasadą zastapień B. Liskov, wszystkie klasy dziedziczące z danej klasy bazowej muszą być z nią zgodne pod względem zachowań. Tego oczekują klienci używajacy klasy bazowej.

[1] Liskov B.: Data Abstraction and Hierarchy. SIGPLAN Notices, 1988. 23, 5.
[2] Meyer B.: Programowanie zorientowane obiektowo. Gliwice. Helion, 2005.
Opublikowane 17 września 2007 20:28 przez nuwanda

Komentarze:

# re: Zasada zastąpień Barbary Liskov

18 września 2007 14:27 by Wojciech Gebczyk

moim zdaniem problem lezy w tym ze wogole zmienione zostalo dzialanie Width i Height.

Kolejne kroki dzialania wygladaly:

1. jest prostokat i ma width i height KTORE MOZNA ZMIENIAC NIEZALEZNIE

2. powstaje test/funkcja z aspect ratio zalezna od pkt 1. - owa niezaleznosc

3. powstaje kwadrat i daje mozliwosc uzytkoenikowi na pewien skrot to znaczy zamiast Size mozna uzyc zamiennie Width lub Height.

wg mnie jesli zalozenie poczynione w pkt 1 jest nie naruszalne (API zostalo wypuszczone) to Square powinien miec throw Exception w Width i Height i wystawiona metode Size get/set.

BTW: tak ogolnie patrzac to zalozenie ze jak zastapisz pochodnym obiektem "i ma dzialac" jest delikatnie mowiac "slabe". Wg mnie ma sie kompilowac a czy dzialac to inna sprawa. Bo zawsze mozna popsuc dzialanie jakiejs funkcji po przz proste odziedziczenie i overridowanie pierwszej lepszej funkcji i danie jednego statementu "throw Exception();" [mozna to uogolnic na jezyki "obiektowe" z polimorfizmem i dziedziczeniem]. Tak wiec ta regula "Barbary Liskov" wg mnie jest bledna.

# re: Zasada zastąpień Barbary Liskov

18 września 2007 15:37 by mgrzeg

"If for each object o1 of type S there is an object o2 of type T such

that for all programs P defined in terms of T, the behavior of P is

unchanged when o1 is substituted for o2 then S is subtype of T"

ja czytam te regule jakos tak:

--

Zalozenie: dla kazdego obiektu o1 typu S istnieje obiekt o2 typu T

taki, ze dla kazdego programu P zdefiniowanego wzgledem T (?),

zachowanie P jest niezmienione gdy o1 zastapione zostanie przez o2

Teza: S jest typem pochodnym (podtypem) T.

--

I ja sie z ta teza w sumie zgadzam.

Odnosze wrazenie, ze obaj rozwazacie twierdzenie odwrotne i stad to nieporozumienie. A - jak zauwazyl Wojtek - nie musi byc prawidlowe.

# re: Zasada zastąpień Barbary Liskov

18 września 2007 15:44 by nuwanda

@mgrzeg: Racja. To jest implikacja i tak należy ją czytać.

@wojtek: Oczywiście, że zawsze da się popsuć, ale czy o to chodzi? Zasada ta podpowiada jak tworzyć hierarchię dziedziczenia, żeby było łatwiej. Jeżeli będziesz mógł polegać na zachowaniu klas hierarchii to będzie Ci prościej.

Przykład z praktyczny. MbUnit pozwala Ci napisać taką oto klasę testową:

[TypeFixture(typeof(IRectangle))]

public class IRectangleTest

{

   [Provider(typeof(IRectangle))]

   public IRectangle ProvideRectangle()

   {

       return new Rectangle();

   }

   [Provider(typeof(IRectangle))]

   public IRectangle ProvideSquare()

   {

       return new Square();

   }

   [Test]

   public void TestWidthAndHeight(IRectangle testSubject)

   {

       double width = 5;

       double height = 6;

       testSubject.Width = width;

       testSubject.Height = height;

       Assert.AreEqual(width, testSubject.Width);

       Assert.AreEqual(height, testSubject.Height);

   }

}

Dzięki temu, że Twoja hierarchia nie zmienia zachowania możesz testować ją za pomocą jednego zestawu testów. Oczywiście w tym przypadku z kwadratem i prostokątem powyższy test nie będzie przechodził.

Komentarze anonimowe wyłączone