2016-04-02 12 views
0

Я хочу показать пример с прямоугольниками и квадратами:Почему неизменные объекты позволяют соблюдать Принцип замещения Лискова?

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) { 
     // do Something 
    } 
} 

} 

Если я заменить экземпляр Square вместо Rectangle в методе LSPTest поведение моей программы будет изменено. Это противоречит LSP.

Я слышал, что неизменный объект позволяет решить эту проблему. Но почему?

Я изменил пример. добавить конструктор в 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 выглядит следующим образом:

class 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"); 
    } 
} 

} 

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

+2

Что заставляет вас думать, что непреложные объекты неотъемлемо украшены LSP? (намек, они этого не делают). – jtahlborn

+0

Я согласен с @jtahlborn. И попытка реализовать неизменяемый объект с сеттером, который возвращает новый объект, кажется мне довольно запутанной. –

+1

Проблема с квадратом неизменяема. Он не должен принимать два аргумента в своем конструкторе, но только один. И он не должен переопределять setWidth и setHeight. Контракт baseWidth() должен возвращать прямоугольник с той же высотой, что и исходный и заданная ширина. Нет причин переопределять это на Square: базовый метод также подходит для Square: он просто не возвращает квадрат. –

ответ

2

From here Я схватил эти цитаты (курсив мой):

Представьте, что вы имели SetWidth и SetHeight методы на вашем Rectangle базового класса; это кажется совершенно логичным. Однако, если ваша ссылка Rectangle указала на Square, то SetWidth и SetHeight не имеет смысла, поскольку установка одной из них изменит другую, чтобы соответствовать ей. В этом случае Square проваливает Лиск Замену тест с Rectangle и абстракцией, имеющие Square наследовать от Rectangle является плохо один.

... и ...

Что LSP указывает на то, что поведение подтипа должно соответствовать поведение базового типа, как это определено в спецификации базового типа. Если спецификация базового типа прямоугольника говорит, что высота и ширина могут быть установлены независимо, тогда LSP говорит, что квадрат не может быть подтипом прямоугольника. Если спецификация прямоугольника говорит о том, что прямоугольник неизменен, квадрат может быть подтипом прямоугольника. Это все о подтипах, поддерживающих поведение, заданное для базового типа.

Я предполагаю, что это будет работать, если у вас есть конструктор, как:

Square(int side){ 
    super(side,side); 
    ... 
} 

Потому что нет никакого способа изменить что-то непреложное, нет сеттеров. Квадрат всегда будет квадратным.

Но должно быть возможно иметь отношение между двумя, которое не нарушает LSP, что также не заставляет вас использовать неизменяемые объекты. Мы просто ошибаемся.

В математике квадрат можно рассматривать как тип прямоугольника. Это, по сути, более конкретный тип прямоугольника. Наивно, может показаться логичным сделать Square extends Rectangle, потому что прямоугольники просто выглядят так super. Но точка наличия подкласса заключается не в создании более слабой версии существующего класса, а в улучшении функциональности.

Почему бы не иметь что-то вроде:

class Square{ 
    void setSides(int side); 
    Boundary getSides(); 
} 
class Rectangle extends Square{ 
    //Overload 
    void setSides(int width, int height); 
    @Override 
    Boundary getSides(); 
} 

Я хотел бы также отметить, что сеттеры предназначены для установки . Код ниже ужасен, потому что вы по существу создали метод, который не будет делать то, что он говорит.

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

Вопрос заключается с контрактов, то есть ожидания программистов, которые используют свой Rectangle.

контракт является то, что вы можете сделать setWidth(15), а потом getWidth() вернется 15, пока вы не сделаете еще setWidth с другим значением.
С точки зрения setHeight, это означает, что он не должен меняться height. Продолжайте эту мысль, а контракт для сеттера - «обновите это свойство до значения параметра, а оставьте все остальные свойства без изменений».

Сейчас в Square, новая инвариантной getWidth() == getHeight() силы setWidth также установить высоту, и вуаля: договор setHeight нарушается.

Конечно, вы можете явно указать в контракте Rectangle.setWidth() (то есть в документации по методу), что ширина может измениться, если вызывается setHeight().
Но теперь ваш контракт на setWidth довольно бесполезен: он установит ширину, которая может оставаться или не оставаться постоянной после вызова setHeight, в зависимости от того, что может решить подкласс.

Все может ухудшиться. Предположим, вы разворачиваете свой Rectangle, люди жалуются на необычный контракт на сеттеры, но в остальном все в порядке.
Но теперь кто-то приходит и хочет добавить новый подкласс, OriginCenteredRectangle. Теперь изменение ширины также необходимо обновить x и y. Веселитесь, объясняя своим пользователям, что вам нужно было изменить контракт базового класса, чтобы можно было добавить другой подкласс (который требовал только 10% ваших пользователей).

Практика показала, что проблема OriginCenteredRectangle является гораздо более распространенной, чем глупость этого примера.
Кроме того, практика показала, что программисты обычно не знают о полном контракте и начинают писать обновляемые подклассы, которые летают перед лицом ожиданий, вызывая тонкие ошибки.
Так что большинство сообществ языков программирования, наконец, решает, что вам нужны классы значений; из того, что я видел на C++ и Java, этот процесс занимает десять или два.

Теперь с непреложными классами ваши сеттеры внезапно выглядят по-другому: void setWidth(width) становится Rectangle setWidth(width). То есть вы не записываете сеттеры, вы пишете функции, которые возвращают новый объект с другой шириной.
И это совершенно приемлемо в Square: setWidth остается неизменным и по-прежнему возвращает Rectangle, как и должно быть. Конечно, вам нужна функция, чтобы вернуть другой квадрат, поэтому Square добавляет функцию Square Square.setSize(size).

Вы по-прежнему хотите класс MutableRectangle просто построить Rectangles без необходимости создавать новую копию - у вас будет Rectangle toRectangle(), который вызывает вызов конструктора. (Другое имя для MutableRectangle будет RectangleBuilder, в котором описывается несколько более ограниченное рекомендуемое использование класса. Выберите свой стиль - лично, я думаю, MutableRectangle в порядке, это просто, что большинство попыток подкласса не удастся, поэтому я бы подумал об этом final.)