#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) { // делать что то полезное } } } Этот тест работает без проблем.
Комментариев нет:
Отправить комментарий