Примите самые слабые, верните самые сильные. Но почему? - программирование
Подтвердить что ты не робот

Примите самые слабые, верните самые сильные. Но почему?

Спрашивая себя снова и снова, какой хороший код, я прочитал совет "Примите самых слабых, верните самых сильных".

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

"Возвращение сильнейшего" для меня не так понятно. Почему я должен вернуть самый сильный интерфейс? Какой самый сильный интерфейс? Как вы оцениваете силовость?

Предположим, что существует метод, возвращающий последовательность элементов. Самый слабый интерфейс будет иметь тип IEnumerable. Следуя рекомендациям, мы должны вернуться, например, IList. Но почему?

Я хотел бы попросить объяснение, почему нужно вернуть самый сильный интерфейс.

4b9b3361

Ответ 1

Это правило имеет несколько форм, вот и другое:

  • Ввод должен быть самым общим/общим (не в стиле .NET/С#) типом
  • Вывод должен быть наиболее конкретным типом

Обоснование этого заключается в том, чтобы сделать это:

  • Легко использовать все типы типов, поддерживающие общий интерфейс
  • Выясните, какая структура данных, которая фактически возвращается, дает абоненту большую гибкость и, возможно, даже большую производительность.

Возьмем пример.

Предположим, вы хотите создать метод, который берет набор целых чисел, обрабатывает их, а затем возвращает новый набор целых чисел.

Совершенно хорошей реализацией этого может быть:

public IEnumerable<int> Process(IEnumerable<int> input) { ... }

Обратите внимание, что я не обязательно следовал второму пункту здесь, но это зависит от того, что делает этот метод. Если метод обрабатывает по одному элементу за раз, вышеуказанное объявление является довольно хорошим, оно сигнализирует, что вы не обязательно возвращаете список, или массив, или еще что-то, просто "коллекцию".

Однако предположим, что метод должен сначала собрать все значения, затем обработать их, а затем вернуть их. Лучшее объявление в этом случае может быть следующим:

public List<int> Process(IEnumerable<int> input) { ... }

Почему это лучше? Ну, во-первых, нередко вызывающий хочет все равно собирать все значения, и теперь ему это не нужно, оно уже возвращается как единый сборник, содержащий все значения. Кроме того, если вызывающий абонент просто хочет продолжать использовать значения как IEnumerable<int>, это возможно также.

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

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

Ответ 2

Я не думаю, что возвращение "самого сильного" (или, скорее, самого конкретного или производного) типа обязательно является хорошей идеей, потому что если ваш интерфейс или контракт указывает, что вы возвращаете List<T>, вы будете застревать, если хотите изменить это на Collection<T> в будущем, потому что обе из них представляют собой различные реализации IList<T> без общего предка. Вам нужно будет изменить интерфейс.

Мантра, которую вы цитировали, кажется очень догматичной - всегда есть исключения, и я бы сказал, что вы должны придерживаться того, что подходит для данной работы. Это звучит как укоренение принципа программирования в сети /IO "Будьте либеральны в том, что вы принимаете в качестве входных данных, но строго в том, что вы выводите", - но программирование в сети /IO очень отличается от определения типов интерфейса OO или реализации, поскольку ABI - совсем другой зверь по сравнению с удобочитаемой спецификацией (например, RFC для HTTP).

Ответ 3

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

IList является "более сильным", поскольку он дает больше гарантий, чем IEnumerable. IList поддерживает добавление, удаление, перечисление и т.д., Тогда как IEnumerable поддерживает только перечисление. Конечно, List еще сильнее, будучи конкретным типом.

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

С положительной стороны возврат "самого сильного" типа позволяет вызывающему абоненту выполнять все возможные операции без расторжения контракта (например, путем литья) или выполнения дополнительной работы (например, копирование IEnumerable в List), Как правило, помощь в приложении и, поскольку она в рамках одного приложения, любые изменения прерывания будут обнаружены во время компиляции и могут быть легко исправлены.

Однако для API, которые должны быть связаны с обратной совместимостью (например, используемой более чем одним приложением или развернутым независимо), тип возврата является частью контракта. В этом случае вам действительно нужно взвешивать значение навсегда, поддерживая возврат IList вместо более слабого IEnumerable. Специфика там должна быть продиктована намерением API, а не придерживаться этого правила.

FWIW, мое личное содержательное утверждение было бы чем-то вроде "вернуть то, что полезно для вызывающего, и не более". Я рассматриваю это как ярлык для определения "полезного для вызывающего" в контексте одного приложения, но активно вредного для внешнего API.

Ответ 4

"Самый сильный" - это немного неопределенный термин и может означать как минимум две вещи:

  • Вернуть интерфейс "Самый производный", например. return IList<T> или List<T> вместо ICollection<T> или IEnumerable<T>
  • Верните диапазон значений с наибольшим ограничением. Например, возвращаемые числа в диапазоне 10..20, а не в диапазоне 5..25

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

Рассмотрим случай Linq. Представьте, что интерфейс Linq последовал этому совету и вернул List<T> везде, а не IEnumerable<T>. Очевидно, что это было бы невозможно.

Для конкретного примера рассмотрим Enumerable<T>.Range(int start, int count). Это может вернуть List<T>, но в большинстве случаев это будет очень неэффективно.

Также представьте, что вы хотите вернуть последовательность элементов из класса, который вы не хотите, чтобы вызывающий вызывал изменение. Вы должны вернуть ReadOnlyCollection<T>, а не List<T>. На самом деле в некоторых случаях я думаю, что вы должны вернуть IEnumerable<T>.

Кодовые контракты и методы переопределения в производных классах

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

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

Аналогично, вам разрешено усилить постусловие. Это может применяться как к типам, так и к выходным диапазонам. Например, если метод базового класса возвращает IEnumerable<T>, вы можете вернуть IList<T> (хотя он, конечно, будет возвращен как IEnumerable<T>).

И если базовый класс сказал, что возвращаемые значения должны находиться в диапазоне 0..100, вы можете вернуть значения в диапазоне 40..60 без нарушения кодового контракта.

Ответ 5

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

Просто посмотрите примеры java- script, где возвращаемое значение является объектом. Вы НЕ МОЖЕТЕ работать с ним, не используя какую-либо документацию для значения. (Или запустить код, чтобы увидеть, что это значит).

Надеюсь, что это поможет.

Ответ 6

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