LSP - Liskov Substitution Principle

Барбара Лисков, дефинира принципа през 1988 г., като:

Ако за всеки обект o1 от тип S има обект o2 от тип T такъв, че за всички програми P, дефинирани по отношение на T, поведението на P е непроменено, когато o1 е заместен за o2 тогава S е подтип на T.

Или както го обобщава Робърт С. Мартин:

Подтиповете трябва да бъдат заместващи на базовите си типове.

По отношение на LSP методите, които използват извиккване на базови класове, трябва да могат да използват обекти от производния клас, без да го знаят. С прости думи производните класове трябва да бъдат заместващи на базовия клас, за да е приложен принципа на LSP.

Нека вземем пример за правоъгълници и квадратчета. Има тенденция да се установи връзката, по този начин, можете да кажете, че квадрат е правоъгълник. Възниква обаче проблем (оттук — нарушение на LSP), което се доказва със следния пример.

public class Rectangle {
    private int length;
    private int breadth;
    
    public int getLength() {
        return length;
    }
    public void setLength(int length) {
        this.length = length;
    }
    public int getBreadth() {
        return breadth;
    }
    public void setBreadth(int breadth) {
        this.breadth = breadth;
    }
    public int getArea() {
        return this.length * this.breadth;
    }
}
public class Square extends Rectangle {
    @Override
    public void setBreadth(int breadth) {
        super.setBreadth(breadth);
        super.setLength(breadth);
    }
    @Override
    public void setLength(int length) {
        super.setLength(length);
        super.setBreadth(length);
    }
}
public class LSPDemo {
    public void calculateArea(Rectangle r) {
        r.setBreadth(2);
        r.setLength(3);

        r.getArea() == 6 : printError("area", r);

        r.getLength() == 3 : printError("length", r);
        r.getBreadth() == 2 : printError("breadth", r);
    }

    private String printError(String errorIdentifer, Rectangle r) {
        return "Unexpected value of " + errorIdentifer + "  for instance of " + r.getClass().getName();
    }

    public static void main(String[] args) {
        LSPDemo lsp = new LSPDemo();
        //
        // An instance of Rectangle is passed
        //
        lsp.calculateArea(new Rectangle());
        //
        // An instance of Square is passed
        //
        lsp.calculateArea(new Square());
    }
}

Кодът на метода за пресмятане има Правоъгълник като аргумент. Що се отнася до принципа, функциите, които използват препратки към базовите класове, трябва да могат да използват обекти от производен клас, без да го знаят. По този начин, в примера, метода calculateArea, който използва препратката на "Правоъгълник," трябва да бъде в състояние да използва обектите от производен клас, като Square, и да изпълни изискването, породено от Правоъгълник определение.

Погледнете calculateArea методa за който трябва да отбележи, че според определението за Правоъгълник, следното трябва винаги да бъде вярно:

  • Двете дължини трябва да са винаги равни, setLength.

  • Двете ширини трябва да са винаги равни, setBreadth.

  • Площта трябва винаги да е равна на произведението от дължина и ширина.

В този случай се опитваме да установим връзка между Square и Rectangle такава, че да наричаме "Square е Rectangle" кода би започнал да се държи непредвидено, ако се подаде екземпляр на Square. Грешка в твърдението ще бъде хвърлена в случай на проверка за "Площ" и проверка за "Ширина", въпреки че програмата ще прекрати, тъй като грешката в твърдението е хвърлена поради неуспеха на проверката на Площа.

Като се има предвид примера, какъв е проблемът с отношенията Квадрат-Правоъгълник?

  • Класът Square не се нуждае от методи като setBreadth или setLength, тъй като страните на квадрат са равни. Представете си за стотици хиляди обекти, Square че трябва да се сетват две повтарящи се стойности вместо една.

  • Класът LSPDemo ще трябва да знае подробностите за производните класове Rectangle (като Square), за да изпълнява по подходящ начин, и да се избегне грешка. Ще има нужда от промяната в съществуващия код, за да се грижи за производния клас, на първо място нарушава принципа на еденичната отговорност.

Трябва да се следват някои от характеристиките за качество на кода, които са представени от принципа на Лисков.

  • Само когато производните типове са напълно заместващи за базовите си типове, функциите, които използват тези базови типове, могат да се използват повторно безнаказано, а производните типове могат да бъдат променяни безнаказано.

  • Използвайки този принцип, методите на класовете декларират предварителни условия и постустановени условия. Предварителните условия трябва да са верни, за да може методът да се изпълни. След завършване методът гарантира, че след условието ще бъде вярно.

  • Пройзводните класове трябва да хвърлят изключения за свойствата който не могат да обработят от базовите класове

Правила

  • Правило за подпис относно видове аргументи за метод - Това правило гласи, че типовете аргументи за метода на надместен подтип могат да бъдат идентични или по-широки от типовете аргументи за метода на супертипа.

  • Правило за подпис относно видове типове за връщане - Типът на връщане на метода на замествания метод може да бъде по-конкретен от типа на връщане на метода на супертипа.

  • Правило за свойствата при класовете наследници. Клас наследник трябва да се погрижи да дефинира условие относно свойствата на обекта, които трябва да са верни за всички валидни състояния на обекта.

  • Правило за свойствата ограничение в историята - методите на подкласа не трябва да позволяват промени в състоянието, които базовият клас не е позволил.

  • Правило за методите за предварителни условия - Предварително условие (валидация на данните) трябва да бъде удовлетворено, преди да може да бъде изпълнен метод.

  • Правило за методи следусловия - условие което трябва да бъде изпълнено след изпълнение на метод.

Нека да разгледаме пример:

Last updated