Не так давно на форуме rsdn.ru был поднят вопрос о выборе типа возвращаемого значения для некоторого метода, возвращающего коллекцию объектов. Какой тип выбрать: более абстрактный (например, для коллекциях в C# это может быть IEnumerable<T>), более конкретный (например, List<T>) или остановиться на каком-либо промежуточном варианте (например, IList<T>)?
К этому вопросу можно подойти с двух сторон. С чисто теоретической точки зрения и с прагматично-практической.
Начнем по-порядку.
Теоретические обоснования выбора типа возвращаемого значения
Теоретическое обоснование выбора типа возвращаемого значения восходит к формальной теории верификации программ и проектированию по контракту Бертрана Мейера.
Существует формальная математическая нотация определяющая корректность некоторых программ, которая представлена формулой корректности.
{P} A {Q} |
Определение этой формулы звучит так:
Любое выполнение A, начинающееся в состоянии, где P истинно, завершится и в заключительном состоянии будет истинно Q.
Эта формула также называется триадой Хоара, а P и Q представляет собой утверждения, называемые предусловие и постусловие соответственно.
Рассмотрим пример триады Хоара для некоторой функции, например, возведения в квадрат.
{x = 5} x = x ^ 2 {x > 0} |
Эта триада корректна, т.к. если перед выполнением операции x^2, предусловие выполняется и значение x равно 5, то после выполнения этой операции, постусловие (x больше нуля) будет гарантировано выполняться (при условии корректной реализации целочисленной арифметики). Из этого примера видно, что приведенное постусловие не является самым сильным. В приведенном примере самым сильным постусловием при заданном предусловии является {x = 25}, а самым слабым предусловием при заданном постусловии является {x > 0}. Из выполняемой формулы корректности всегда можно породить новые выполняемые формулы, путем ослабления постусловия или усиления предусловия.
Бертран Мейер в своей книге "Объектно-ориентированное конструирование программных систем" описал значение сильных и слабых условий на примере контракта человека.
Давайте взглянем на формулу корректности с позиции человека, собирающегося наняться на работу по выполнению операции A. Каковы с его точки зрения наилучшие предусловие P и постусловие Q, если у него есть возможность выбора? Возможность усиления предусловия означает, что можно предъявлять более жесткие требования к работодателю, что можно уменьшить число ситуаций, в которых следует приступать к выполнению работы. Так что сильное предусловие это "хорошие новости" для работника. Наилучшей для него работой — синекурой является работа, чья спецификация выражается формулой:
{False} A {...} Постусловие здесь не специфицировано, поскольку не имеет значения каково оно. К выполнению работы можно вообще не приступать, поскольку нет ни одного начального состояния, в котором предусловие было бы истинным. Так что если вам предложат такую синекуру, немедленно соглашайтесь, не глядя на постусловие — требования, предъявляемые к выполненной работе.
Для постусловия ситуация меняется на противоположную. Лучшими для работника являются более слабые условия — это "хорошие новости"; в этом случае хорошо нужно уметь делать очень немногое. Наилучшей работой — второй синекурой является работа, заданная спецификацией:
{...} A {True} Как бы не была выполнена работа, постусловие в этом случае будет истинным по определению.
Обсуждение того, будет ли усиление или ослабление утверждений "хорошей" или "плохой" новостью, шло с позиции работника, нанимающегося для выполнения работы. Обратим ситуацию, и рассмотрим ее с позиции работодателя. В этом случае слабое предусловие станет "хорошей" новостью, поскольку означает выполнение работы для большего множества входных случаев; более предпочтительным теперь является сильное постусловие, поскольку оно расширяет получение важных результатов.
Понятно, что тип возвращаемого значения на прямую связан с силой постусловия (т.к. возвращаемое значение является результатом выполнения некоторой операции). Более конкретный тип возвращаемого значения усиливает постусловие, а более базовый тип — наоборот ослабляет.
Теперь, чтобы ответить на вопрос о том, какой тип возвращаемого значения выбрать, нужно знать, на какой стороне (с точки зрения операции) вы находитесь или какая из двух сторон важнее? Например, если важно получить максимальную функциональность на стороне клиента (потребителя услуги), то необходимо сильное постусловие и возврат наиболее конкретного типа возвращаемого значения (если речь идет о коллекциях, то List<T> или другой конкретный тип). Если же необходимо обеспечить минимальную стоимость эволюции и сопровождения поставщика услуги, то выгоднее использовать слабое постусловие и возвращать базовые типы в качестве типа возвращаемого значения (например, IEnumerable<T>). Если же речь идет о разумном компромиссе, между минимизацией затрат на эволюцию и сопровождение кода и функциональностью, то нужно рассматривать контекст использования возвращаемого значения и выбирать некоторый промежуточный вариант.
Прагматично-практический способ выбора типа возвращаемого значения
Прагматичный подход к вопросу выбора типа возвращаемого значения зависит прежде всего от способа использования метода, а точнее от того, является ли класс, реализующий этот метод, библиотечным или же имеется ввиду сущность бизнес-приложения. Если речь идет о библиотеке, которую будут использовать сотни тысяч пользователей, то цена ошибки в открытом интерфейсе класса возрастает многократно, что требует значительно более высокой квалификации проектировщика и серьезное смещение акцентов в сторону удобства использования пользователями даже в ущерб принципам декомпозиции или стоимости сопровождения. Чтобы оценить сложность проектирования крупных библиотек (framework-ов), стоит обратить внимание на количество недочетов у таких монстров как Microsoft или Sun и почитать замечательную книгу Framework Design Guidelines, 2nd edition by Krzysztof Cwalina and Brad Abrams. Кроме того, большинство пользователей все же сталкиваются с вопросами выбора типа возвращаемого значения при проектировании бизнес-приложений, поэтому далее я буду рассчитывать, что речь идет не о библиотеках, а о коде бизнес-логики логики.
Для любого кода в приложении характерны два важных показателя: сцепление (cohesion) и связанность (coupling). При проектировании очень важно максимизировать связи внутри компонентов (высокое сцепление, high cohesion) и минимизировать связи между компонентами (низкая связанность, low coupling). Если проектировщику удастся обеспечить эти показатели, то количество изменений, которые придется внести в код при ошибочном выборе типа возвращаемого значения (при изменении интерфейса, если рассматривать более общий случай) будут минимальны. В случае, если один человек отвечает и за поставщика услуг (за сам компонент), и за клиента (который этими услугами пользуется), то подобное изменение сложно назвать катастрофическим. Если же за разные компоненты отвечают разные люди, то изменение интерфейса взаимодействия будет неприятным, но не смертельным (кроме того, всегда можно добавить новый метод не удаляя старый, при этом старый пометить как устаревший (Obsolete), если ваша среда это позволяет).
Хотя часто изменения интерфейса бизнес-объектов не являются чем-то катастрофическим, каждый проектировщик стремится минимизировать подобные проблемы. Для этого в ходе анализа предметной области необходимо оценить то, какие задачи выполняет этот класс и какое наиболее вероятное (и, возможно, удобное) применение результатов выполнения операции. Если речь идет о возврате коллекции, то нужно подумать над следующими вопросами: следует ли возвращать копию коллекции, или можно вернуть ссылку на внутреннее поле или свойство; какое предполагаемое количество элементов будет в коллекции; как часто будет вызываться этот метод; будет ли он вызываться несколько раз подряд; нужно ли будет изменять полученную коллекцию; нужен ли доступ по индексу и т.д. Так, в случае возможности применения ленивых вычислений стоит обратить внимание на IEnumerable<T>, в случае необходимости доступа к элементам по индексу - IList<T>. Совершенно ясно, что на все эти вопросы невозможно дать ответ на этапе проектирования, а также вполне вероятно, что ответы могут меняться в процессе разработки или эволюции программы, но нужно же как-то получить отправную точку. При этому задача проектировщика заключается в том, чтобы выбрать такой тип возвращаемого значения, который будет минимальным образом отвечать потребностям вызывающей стороны.
Вместо заключения
Выбор правильного типа возвращаемого значения - задача непростая, а на ранних этапах разработки программного обеспечения и не всегда выполнимая. Но даже в этом случае принципы, заложенные в проектировании по контракту могут быть весьма полезными для формализации отношений между различными классами, что позволит использовать эту информацию в качестве документации, а также во время статического анализа кода. В любом случае главное помнить следующее: ваша задача - получить удовлетворительное решение за конечное время, а не идеальное решение, но за бесконечное время.