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