Страницы

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

суббота, 13 октября 2018 г.

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

На эти размышления меня натолкнула следующая статья
В ней приведен классический для принципа Лисков пример с прямоугольник и квадратом. В коде это можно выразить так:
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"); } }
}
Все те же проблемы остались. Какая разница, изменяется ли сам объект или возвращается новый объект? Программа то ведет себя по-разному для базового класса и его подкласса.


Ответ

Призвали бы в пост, ответил бы раньше:)
Можно рассматривать разные варианты неизменяемости. С одной стороны, объект может быть неизменяемым, но при этом предоставлять методы 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) { // делать что то полезное } }
}

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

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