Подтвердить что ты не робот

Методы Collections.unmodifiableXXX нарушают LSP?

Принцип замены Лискова является одним из принципов SOLID. Я читал этот принцип несколько раз и стараюсь его понять.

Вот что я из него сделаю,

Этот принцип связан с сильным поведенческим контрактом между иерархия классов. Подтипы должны быть заменены на супертип без нарушения договора.

Я прочитал еще один articles, и я немного потерял мысль об этом вопросе. Методы Collections.unmodifiableXXX() не нарушают LSP?

Выдержка из статьи, приведенной выше:

Другими словами, при использовании объекта через интерфейс базового класса, пользователь знает только предварительные условия и постусловия базы класс. Таким образом, производные объекты не должны ожидать, что такие пользователи будут подчиняться предварительные условия, которые сильнее, чем те, которые требуются базовому классу

Почему я так думаю?

Перед

class SomeClass{
      public List<Integer> list(){
           return new ArrayList<Integer>(); //this is dumb but works
      }
}

После

class SomeClass{
     public List<Integer> list(){
           return Collections.unmodifiableList(new ArrayList<Integer>()); //change in implementation
     }
}

Я не могу изменить имплантацию SomeClass, чтобы возвращать немодифицируемый список в будущем. Компиляция будет работать, но если клиент каким-то образом попытается изменить возвращаемый List, тогда он будет терпеть неудачу во время выполнения.

Именно поэтому Guava создал отдельный интерфейс ImmutableXXX для коллекций?

Разве это не прямое нарушение LSP, или я полностью понял это неправильно?

4b9b3361

Ответ 1

LSP говорит, что каждый подкласс должен подчиняться тем же контрактам, что и суперкласс. Wether или нет, это имеет место для Collections.unmodifiableXXX(), поэтому зависит от того, как этот контракт читается.

Объекты, возвращаемые Collections.unmodifiableXXX(), вызывают исключение, если вы пытаетесь вызвать на них любой модифицирующий метод. Например, если вызывается add(), выдается UnsupportedOperationException.

Что такое общий договор add()? В соответствии с API-документацией:

Обеспечивает, чтобы эта коллекция содержала указанный элемент (необязательно операция). Возвращает true, если эта коллекция изменилась в результате вызов. (Возвращает false, если эта коллекция не позволяет дублировать и уже содержит указанный элемент.)

Если это был полный контракт, то действительно немодифицируемый вариант не мог использоваться во всех местах, где коллекция может использоваться. Однако спецификация продолжается, а также говорит, что:

Если коллекция отказывается добавлять какой-либо элемент по какой-либо причине кроме того, что он уже содержит элемент, он должен исключение (вместо того, чтобы возвращать false). Это сохраняет инвариант что коллекция всегда содержит указанный элемент после этого возврат вызовов.

Это явно позволяет реализации иметь код, который не добавляет аргумент add в коллекцию, но приводит к исключению. Разумеется, это включает в себя обязательство для клиента коллекции, которое они учитывают (юридическую) возможность.

Таким образом, поведенческий подтипирование (или LSP) все еще выполняется. Но это показывает, что если вы планируете иметь разные типы поведения в подклассах, которые также должны быть предусмотрены в спецификации класса верхнего уровня.

Хороший вопрос, кстати.

Ответ 2

Да, я считаю, что у вас все правильно. По сути, для выполнения LSP вы должны иметь возможность делать что-либо с подтипом, который вы можете сделать с супертипом. Именно поэтому проблема Ellipse/Circle возникает с LSP. Если Ellipse имеет метод setEccentricity, а Circle - подкласс Ellipse, и объекты должны быть изменяемыми, то Circle не может реализовать метод setEccentricity. Таким образом, вы можете сделать что-то, что вы можете сделать с эллипсом, который вы не можете сделать с кругом, поэтому LSP нарушен. & Dagger; Аналогично, вы можете сделать что-то, что вы можете сделать с помощью обычного List, который вы не можете сделать, с обернутым Collections.unmodifiableList, чтобы нарушение LSP.

Проблема в том, что мы хотим что-то здесь (неизменный, неизменяемый, только для чтения список), который не захватывается системой типов. В С# вы можете использовать IEnumerable, который фиксирует идею последовательности, с которой вы можете перебирать и читать, а не писать. Но в Java существует только List, который часто используется для изменяемого списка, но который мы иногда хотели бы использовать для неизменяемого списка.

Теперь некоторые могут сказать, что Circle может реализовать setEccentricity и просто выбросить исключение, а также немодифицируемый список (или неизменный из Guava) генерирует исключение, когда вы пытаетесь его модифицировать. Но это на самом деле не означает, что это - Список с точки зрения LSP. Прежде всего, это по крайней мере нарушает принцип наименьшего удивления. Если вызывающий объект получает неожиданное исключение при попытке добавить элемент в список, это довольно удивительно. И если вызывающий код должен предпринять шаги, чтобы отличить список, который он может изменить, и один он не может (или форма, чья эксцентричность может быть установлена, а другая - невозможна), то одно не может быть заменено другим.

Было бы лучше, если бы система типа Java имела тип для последовательности или коллекции, которая разрешала итерацию, и другую, которая допускала модификацию. Возможно, Iterable может быть использован для этого, но я подозреваю, что ему не хватает некоторых функций (например, size()), которые действительно хотелось бы. К сожалению, я считаю, что это ограничение существующего API коллекций Java.

Несколько человек отметили, что документация для Collection позволяет реализации генерировать исключение из метода add. Я полагаю, что это означает, что Список, который не может быть изменен, подчиняется букве закона, когда речь заходит о контракте на add, но я думаю, что нужно изучить один код и посмотреть, сколько мест там защищает вызовы мутируя методы List (add, addAll, remove, clear) с блоками try/catch, прежде чем утверждать, что LSP не нарушен. Возможно, это не так, но это означает, что весь код, вызывающий List.add в List, который он получил как параметр, сломан.

Это, безусловно, будет говорить много.

(Аналогичные рассуждения могут показать, что идея, что null является членом каждого типа, также является нарушением принципа замены Лискова.)

& крестик; Я знаю, что есть другие способы решения проблемы эллипса/круга, например, сделать их неизменными или удалить метод setEcentricity. Я говорю здесь только о наиболее распространенном случае, как о аналогии.

Ответ 3

Я не считаю это нарушением, потому что контракт (т.е. интерфейс List) говорит о том, что операции мутации являются необязательными.

Ответ 4

Я думаю, вы здесь не смешиваете.
Из LSP:

Лисковское понятие поведенческого подтипа определяет понятие заместительность для изменяемых объектов; т.е. если S является подтипом T, то объекты типа T в программе могут быть заменены объектами типа S без изменения каких-либо желательных свойств этого (например, правильность).

LSP относится к подклассам.

Список - это интерфейс, а не суперкласс. Он определяет список методов, предоставляемых классом. Но отношения не связаны как с родительским классом. Тот факт, что класс A и класс B реализуют один и тот же интерфейс, не гарантирует ничего о поведении этих классов. Одна реализация всегда может возвращать true, а другая - исключать или всегда возвращать false или что-то другое, но оба придерживаются интерфейса, поскольку они реализуют методы интерфейса, чтобы вызывающий мог вызвать метод на объекте.