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