Страницы

Поиск по вопросам

вторник, 10 декабря 2019 г.

Как immutable объекты позволяют соблюдать принцип подстановки Лисков?

#java #ооп #solid


На эти размышления меня натолкнула следующая статья.

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

class Rectangle {

private int width;
private int height;

public int getWidth() {
    return width;
}

public void setWidth(int width) {
    this.width = width;
}

public int getHeight() {
    return height;
}

public void setHeight(int height) {
    this.height = height;
}

public int area() {
    return width * height;
}

}

class Square extends Rectangle{

@Override
public void setWidth(int width){
    super.setWidth(width);
    super.setHeight(width);
}

@Override
public void setHeight(int height){
    super.setHeight(height);
    super.setWidth(height);
}

}

public class Use {

public static void main(String[] args) {
    Rectangle sq = new Square();
    LSPTest(sq);
}

public static void LSPTest(Rectangle rec) {
    rec.setWidth(5);
    rec.setHeight(4);

    if (rec.area() == 20) {
        // делать что то полезное
    }
}

}


Если в метод LSPTest подставить объект Square вместо Recatangle поведение программы
изменится. Это противоречит принципу Лисков.

Автор вышеупомянутой статьи делает такое заявление:


  Квадрат перестает быть нормальным прямоугольником, ТОЛЬКО если квадрат и прямоугольник
являются изменяемыми! Так, если мы сделаем их неизменяемыми (immutable), то проблема
с контрактами, принципом подстановки и нарушением поведения клиентского кода при замене
прямоугольников квадратами пропадет. Если клиент не может изменить ширину и высоту,
то его поведение будет одинаковым как для квадратов, так и для прямоугольников!


Я не понимаю почему. Может это от того что я не хорошо понимаю сам LSP или immutable.
Я переписал пример:

Добавил конструктор в Rectangle:

public Rectangle(int width, int height) {
    this.width = width;
    this.height = height;
}


И изменил методы установки длины и ширины.

public Rectangle setWidth(int width) {
    return new Rectangle(width, this.height);
}

public Rectangle setHeight(int height) {
    return new Rectangle(this.width, height);
}


Вот как изменился класс Square:

public Square() {

}

public Square(int width, int height) {
    super(width, height);
}

@Override
public Rectangle setWidth(int width) {
    return new Rectangle(width, width);
}

@Override
public Rectangle setHeight(int height) {
    return new Rectangle(height, height);
}

}


И клиентский код:

public class Use {

public static void main(String[] args) {
    Rectangle sq = new Square(4, 4);
    LSPTest(sq);
}

public static void LSPTest(Rectangle rec) {
    rec = rec.setHeight(5);

    if (rec.area() == 20) {
        System.out.println("yes");
    }
}

}


Все те же проблемы остались. Какая разница, изменяется ли сам объект или возвращается
новый объект? Программа то ведет себя по-разному для базового класса и его подкласса.
    


Ответы

Ответ 1



Призвали бы в пост, ответил бы раньше:) Можно рассматривать разные варианты неизменяемости. С одной стороны, объект может быть неизменяемым, но при этом предоставлять методы withNewValue или setValue, которые вернут новый экземпляр объекта. А с другой стороны, тип может не предоставлять даже этих возможностей. В первом случае объект неизменяемый, но мы можем огрести проблемы с LSP, как и проблемы с многопоточностью (это, на самом же деле, в некоторой мере один из мифов, что неизменыемые объекты безопасны в многопоточной среде; они менее опасны, но наличие методов setXXX может привести к гонкам). В случае "полной неизменяемости" (да, такого термина нет), этих проблем не будет. Еще раз приведу приведенную в вопросе цитату: Квадрат перестает быть нормальным прямоугольником, ТОЛЬКО если квадрат и прямоугольник являются изменяемыми! Так, если мы сделаем их неизменяемыми (immutable), то проблема с контрактами, принципом подстановки и нарушением поведения клиентского кода при замене прямоугольников квадратами пропадет. Если клиент не может изменить ширину и высоту, то его поведение будет одинаковым как для квадратов, так и для прямоугольников! Я зря сделал акцент на неизменямость. Главная мысль выделена мною сейчас жирным: даже при наличии setWidth методов, возвращающие новый экземпляр, нарушение LSP возможно, вы правы. Если же у клиента этой возможности нет совсем (просто такого API не существует), то тогда нарушение невовзможно. З.Ы. Приведенный пример уважаемого @VladD будет нарушать LSP;), поскольку трюк с final противоречит следующему неформальному, но вполне вменяемому контракту: вызов метод withXXX не меняет тип объекта: public class Use { public static void main(String[] args) { Rectangle sq = new Square(3); LSPTest(sq); } public static void LSPTest(Rectangle rec) { Rectangle oldRec = rec; rec = rec.withWidth(4).withHeight(5); // Первое из следующих утрвеждений будет нарушено! assert(oldRec.getClass().equals(rec.getClass()), "Неявный контракт: метод withWidth/withHeight не должны поменять тип объекта"); assert(rec.getWith() == 4); assert(rec.getHeight() == 5); if (rec.area() == 20) { // делать что то полезное } } }

Ответ 2



Смотрите, в чём проблема. Пусть у нас есть ссылка на прямоугольник, которяа пришла из другой части программы. И мы надеемся, что если вы установим его ширину в 3, а длину в 4, то у нас таки-будет прямоугольник шириной в 3 и длиной в 4. Но если нам кто-то подсунул вместо прямоугольника квадрат (они совместимы по присваиванию, так что мы ничего и не заподозрим!), то у нас получится прямоугольник длиной и шириной в 4. Проблема, правда? Теперь, в случае иммутабельности у нас не возникает этой проблемы. Что бы у нас в начале ни было — прямоугольник или квадрат — когда мы просим установить ширину в 3 и длину в 4, мы получаем новый прямоугольник (а не квадрат) нужных размеров. Всё работает как нужно. Код, который вы привели в качестве примера для immutable-квадрата, неправильный. Вам вовсе не нужно для этого случая переопределять setWidth и setHeight, унаследованные функции делают уже в точности то, что надо. Ведь если поменять квадрату только длину, но не ширину, полученная фигура будет прямоугольником. Вы можете, если хотите, добавить для квадрата метод public Square setSideLength(int width) { return new Square(width, width); } Фокус в том, что с мутабельными объектами вы не можете правильным образом переопределить методы типа setWidth для квадрата. А вот для иммутабельного случая можете. Итак, правильный код для иммутабельного случая такой: Прямоугольник: class Rectangle { private int width; private int height; public final int getWidth() { return width; } // setWidth переименовали в withWidth public final Rectangle withWidth(int width) { return new Rectangle(width, this.height); } public final int getHeight() { return height; } // setHeight переименовали в withHeight public final Rectangle withHeight(int height) { return new Rectangle(this.width, height); } public final int area() { return width * height; } } Класс квадрата: class Square extends Rectangle { public Square(int sideLength) { super(sideLength, sideLength); } public final Square withSideLength(int sideLength) { return new Square(sideLength); } } Теперь использование: public class Use { public static void main(String[] args) { Rectangle sq = new Square(3); LSPTest(sq); } public static void LSPTest(Rectangle rec) { rec = rec.withWidth(4).withHeight(5); if (rec.area() == 20) { // делать что то полезное } } } Этот тест работает без проблем.

Комментариев нет:

Отправить комментарий