Не нарушают ли частные случаи с запасными вариантами принцип подстановки Лискова?

20

Допустим, у меня есть интерфейс, FooInterfaceкоторый имеет следующую подпись:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

И конкретный класс, ConcreteFooкоторый реализует этот интерфейс:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Я хотел бы ConcreteFoo::doSomething()сделать что-то уникальное, если ему передан специальный тип SomethingInterfaceобъекта (скажем, он называется SpecialSomething).

Это определенно нарушение LSP, если я усиливаю предварительные условия метода или выбрасываю новое исключение, но будет ли это все еще нарушением LSP, если я создаю специальные SpecialSomethingобъекты, предоставляя запасной вариант для универсальных SomethingInterfaceобъектов? Что-то вроде:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}
Evan
источник

Ответы:

19

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

Например, если часть договора doSomethingзаключается в том, что он будет вызывать something.commitUpdates()хотя бы один раз перед возвратом, а для особого случая он вызывает commitSpecialUpdates()вместо этого, то это является нарушением LSP. Даже если SpecialSomething«s commitSpecialUpdates()метод был сознательно разработан , чтобы сделать все из того же материала , как commitUpdates(), что это просто превентивно взлому вокруг нарушения LSP, и именно своего рода повозка , запряженная волами не придется делать , если один следовал LSP последовательно. Относится ли что-то подобное к вашему делу, вы должны выяснить, проверив свой контракт на этот метод (явный или неявный).

Причина, по которой этот код пахнет, заключается в том, что проверка конкретного типа одного из ваших аргументов в первую очередь не позволяет определить для него интерфейсный / абстрактный тип, и потому что в принципе вы больше не можете гарантировать, что метод даже работает (представьте себе, если кто-то пишет подкласс SpecialSomethingс предположением, что commitUpdates()будет вызван). Прежде всего, постарайтесь, чтобы эти специальные обновления работали в рамках существующихSomethingInterface; это наилучший возможный результат. Если вы действительно уверены, что не можете этого сделать, вам нужно обновить интерфейс. Если вы не управляете интерфейсом, вам может потребоваться написать собственный интерфейс, который делает то, что вы хотите. Если вы даже не можете придумать интерфейс, который работает для всех них, возможно, вам следует полностью отказаться от интерфейса и использовать несколько методов, использующих разные конкретные типы, или, возможно, потребуется еще больший рефакторинг. Нам нужно больше узнать о магии, которую вы прокомментировали, чтобы сказать, какая из них подходит.

Ixrec
источник
Благодарность! Это помогает. Гипотетически, целью doSomething()метода было бы преобразование типа в SpecialSomething: если он получит, SpecialSomethingон просто вернет объект без изменений, тогда как если он получит универсальный SomethingInterfaceобъект, он запустит алгоритм для преобразования его в SpecialSomethingобъект. Поскольку предварительные условия и постусловия остаются прежними, я не верю, что контракт был нарушен.
Эван
1
@ Эван Вау ... это интересный случай. Это может быть на самом деле совершенно без проблем. Единственное, о чем я могу думать, это то, что если вы возвращаете существующий объект вместо создания нового объекта, возможно, кто-то зависит от этого метода, возвращающего совершенно новый объект ... но может ли такая вещь сломать людей, может зависеть от язык. Можно ли для кого - то назвать y = doSomething(x), то x.setFoo(3), а затем обнаружить , что y.getFoo()возвращается 3?
Ixrec
Это проблематично в зависимости от языка, хотя его легко можно обойти, просто SpecialSomethingвзамен возврата копии объекта. Хотя ради чистоты я также мог бы отказаться от оптимизации в особом случае, когда передается объект, SpecialSomethingи просто запустить его с помощью более крупного алгоритма преобразования, поскольку он все еще должен работать, поскольку он также является SomethingInterfaceобъектом.
Эван
1

Это не нарушение LSP. Однако это все еще является нарушением правила «не делать проверки типов». Было бы лучше спроектировать код таким образом, чтобы спецэффект возник естественным образом; возможно, SomethingInterfaceнужен другой член, который мог бы выполнить это, или, может быть, вам нужно внедрить абстрактную фабрику куда-нибудь.

Однако это не сложное и быстрое правило, поэтому вам нужно решить, стоит ли компромисс. Прямо сейчас у вас есть запах кода и возможное препятствие для будущих улучшений. Избавление от этого может означать значительно более запутанную архитектуру. Без дополнительной информации я не могу сказать, какая из них лучше.

Себастьян Редл
источник
1

Нет, использование того факта, что данный аргумент не только предоставляет интерфейс A, но и A2, не нарушает LSP.

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

Шаблоны C ++ часто делают это, чтобы обеспечить лучшую производительность, например, требуя InputIterators, но давая дополнительные гарантии при вызове с RandomAccessIterators.

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

Использование особого случая часто идет против СУХОГО (не повторяйте себя), поскольку вам, возможно, придется дублировать код, и против ПОЦЕЛУЯ (Keep it Simple), поскольку это более сложно.

Deduplicator
источник
0

Существует компромисс между принципом «не делать проверки типов» и «разделять ваши интерфейсы». Если многие классы предоставляют работоспособное, но, возможно, неэффективное средство для выполнения некоторых задач, и некоторые из них также могут предложить более эффективные средства, и существует потребность в коде, который может принимать любую из более широкой категории элементов, которые могут выполнить задачу (возможно, неэффективно), но затем выполнить задачу настолько эффективно, насколько это возможно, необходимо, чтобы все объекты реализовали интерфейс, включающий член, чтобы сказать, поддерживается ли более эффективный метод, и другой, чтобы использовать его, если он есть, или иметь код, который получает объект, проверить, поддерживает ли он расширенный интерфейс, и привести его, если так.

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

Supercat
источник
0

Принцип подстановки Лискова касается подтипов, действующих в соответствии с контрактом их супертипа. Итак, как писал Ixrec, недостаточно информации, чтобы ответить, является ли это нарушением LSP.

То, что здесь нарушено, - это принцип открытого закрытого типа. Если у вас есть новое требование - магия SpecialSomething - и вам нужно изменить существующий код, то вы определенно нарушаете OCP .

Западло
источник