Нарушают ли методы Collections.unmodifiableXXX LSP?

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

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

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

Я прочитал некоторые другие статьи и немного потерялся, думая об этом вопросе. . Методы 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 или я совершенно не так понял?


person Narendra Pathai    schedule 26.02.2014    source источник
comment
Сильно связанные: stackoverflow.com/q/20290834/319403   -  person cHao    schedule 27.02.2014
comment
Возможно, стоит указать на первую запись часто задаваемых вопросов по дизайну API коллекций здесь: Почему бы вам не поддерживать неизменяемость непосредственно в интерфейсах основной коллекции, чтобы можно было отказаться от необязательных операций (и исключения UnsupportedOperationException)?.   -  person Marco13    schedule 14.08.2016
comment
Отличная ссылка. Спасибо @Marco13   -  person Narendra Pathai    schedule 15.08.2016


Ответы (4)


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

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

Что такое генеральный контракт add()? Согласно документации API это:

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

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

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

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

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

Кстати, хороший вопрос.

person Matt    schedule 26.02.2014
comment
кроме того, в документации определены исключения, которые должны генерироваться в определенных случаях при отказе в добавлении, в том числе UnsupportedOperationException - person Silly Freak; 26.02.2014
comment
Это также причина, по которой вам никогда не следует изменять коллекции, полученные из API, которых вы не знаете. Если не указано, что коллекция является изменяемой, всегда следует создавать новую. - person Kirill Rakhman; 28.02.2014
comment
@cypressious: Это лишь малая часть причины. Гораздо более важная причина заключается в том, что даже если бы кто-то знал, что коллекция допускает мутацию, он не знал бы, была ли ему дана копия коллекции или представление о коллекции, которое нельзя было изменить. - person supercat; 18.11.2014

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

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

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

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

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

Это, конечно, о многом бы сказало.

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

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

person David Conrad    schedule 26.02.2014
comment
однако методы манипулирования List указывают, что существует несколько способов их поведения. В контракте указано только, что один из них должен быть выбран, и это относится как к изменяемым, так и к неизменяемым спискам. Строго говоря, каждый, кто предполагает, что метод добавления списка не будет генерировать исключение UnsupportedOperationException, нарушает подстановку, а не классы, реализующие List. - person Silly Freak; 26.02.2014
comment
Я думаю, что технически вы правы, но есть теория, а есть контракт на практике. Если бы существовало два типа, ImmutableList и List, и метод add существовал бы только для второго, мы бы никогда не оказались в такой ситуации. - person David Conrad; 26.02.2014
comment
Иными словами, помещая метод в ваш API, а затем объявляя, что иногда он будет что-то делать, а иногда выдавать ошибки, вы просто создаете проблемы. Это ошибка, ожидающая своего появления, и она подрывает то, от чего LSP должен защищать вас. - person David Conrad; 26.02.2014
comment
Конечно, есть и другие возможности дизайна, где существует различие типов между изменяемыми и неизменяемыми списками, но я бы сказал, что это не обязательно. Важно то, что утверждения передаются между вызывающими и вызываемыми объектами — это делают типы. Но даже без различия типов метод может указать в своей документации, что возвращаемое значение является неизменяемым списком, и даже если это не имеет значения для компилятора, программист может сказать, что он не может изменить или заменить его там, где ожидается изменяемый список. - person Silly Freak; 27.02.2014
comment
строго говоря, то же самое относится и к изменяемым спискам, поэтому люди должны указать, что списки, которые они возвращают, и т. д. действительно изменяемы, но я бы сказал, что это ожидается в соответствии с принципом наименьшего удивления, поэтому на самом деле гораздо важнее документировать неизменяемые списки. . - person Silly Freak; 27.02.2014

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

person Ian Roberts    schedule 26.02.2014
comment
Мне не совсем понятно, что означает необязательный в отношении LSP. Код клиента никогда не должен использовать его, поскольку он может не иметь его. - person djechlin; 26.02.2014
comment
@djechlin Да, я так думаю. Неподдерживаемые операции не должны быть частью интерфейса Immubtable, если они не поддерживаются. - person Narendra Pathai; 26.02.2014
comment
@NarendraPathai: если многие реализации интерфейса Wozzler также реализуют метод Wuzzle, и многие потребители Wozzler будут ожидать использования метода Wuzzle, если он доступен, и в противном случае делать что-то еще, то, ИМХО, в целом было бы намного лучше, чтобы Wizzler включал Wuzzle, а также средство определения того, поддерживает ли его данный экземпляр, чем иметь Wuzzle только в интерфейсе Wuzzler, но не в Wizzler. Если коллекции фиксированного размера, которые поддерживают запись по индексу, и коллекции переменного размера, которые поддерживают add, но не поддерживают запись по индексу, имеют общий интерфейс - person supercat; 26.12.2014
comment
... тогда один тип WrappedObservableCollection сможет работать с обоими (позволяя клиентам использовать любые функции, поддерживаемые базовой коллекцией). Разделение возможностей интерфейса в системе типов, а не с помощью методов, сообщающих о возможностях, означает, что для каждой комбинации возможностей, которые может иметь коллекция, потребуется другой класс-оболочка. - person supercat; 26.12.2014

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

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

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

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

person Jim    schedule 26.02.2014
comment
Для этой цели интерфейсы можно рассматривать как суперклассы. (На самом деле, они в значительной степени просто несовершенный обходной путь из-за отсутствия множественного наследования, а в некоторых языках с MI, таких как C++, интерфейсы являются просто классами только с чисто виртуальными членами. ) Интерфейс редко просто предоставляет список имен функций; обычно он также определяет, что делают функции, и все, что соответствует интерфейсу, лучше делает эти вещи. - person cHao; 27.02.2014
comment
@cHao: я не согласен. Интерфейсы и суперклассы - несвязанные концепции. LSP предназначен только для подтипирования. Механизм аналогичен, имеет аргумент в методе IntefaceA и передает InterfaceImpl или InterfaceImpl2, но мы все еще не подтипируем так, как говорит LSP. . Цитата из оракула: If your class claims to implement an interface, all methods defined by that interface must appear in its source code before the class will successfully compile. docs.oracle.com/javase/tutorial/java /concepts/interface.html - person Jim; 27.02.2014
comment
Oracle фокусируется на самом языке. Java не заботится о SOLID и не может обеспечить его соблюдение в какой-либо полезной степени. Но LSP по-прежнему применяется. Более того, на самом деле вы зависите от его применения. Доказательство: скажем, я создаю List<T>, где каждая функция выдает RuntimeException. Это разрешено компилятором и является нарушением LSP, если и только сам интерфейс имеет известные свойства, которые ожидаются, но отсутствуют в реализации. Итак, я передаю свой список, вы пытаетесь его использовать, и бум! Теперь, кто виноват в результате аварии? Если вы вините меня, то вы полагались на LSP. - person cHao; 05.03.2014
comment
Я согласен с @cHao, LSP применяется к контракту, независимо от того, как это указано. - person Marco Lackovic; 31.08.2020