Объявление интерфейса в том же файле, что и базовый класс, это хорошая практика?

29

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

public class FooService: IFooService 
{ ... }

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

Я думал, что запись IFooService в тот же файл, что и FooService уменьшит вышеупомянутую странность. В конце концов, IFooService и FooService очень связаны. Это хорошая практика? Есть ли веская причина, по которой IFooService должен находиться в собственном файле?

Луис Рис
источник
3
Если у вас есть только одна реализация конкретного интерфейса, то нет реальной логической необходимости в интерфейсе. Почему бы не использовать класс напрямую?
Йорис Тиммерманс
11
@MadKeithV для слабой связи и тестируемости?
Луи Рис
10
@MadKeithV: вы правы. Но когда вы пишете модульные тесты для кода, который опирается на IFooService, вы обычно предоставляете MockFooService, который является второй реализацией этого интерфейса.
Док Браун
5
@DocBrown - если у вас есть несколько реализаций, то, очевидно, комментарий исчезает, но также полезна возможность перехода непосредственно к «единственной реализации» (потому что есть как минимум две). Тогда возникает вопрос: какой информации в конкретной реализации не должно быть в описании / документации интерфейса в той точке, где вы используете интерфейс?
Йорис Тиммерманс
1
Явная самореклама: stackoverflow.com/questions/5840219/…
Марьян Венема,

Ответы:

24

Это не должно быть в своем собственном файле, но ваша команда должна выбрать стандарт и придерживаться его.

Кроме того, вы правы, что «Перейти к определению» приводит вас к интерфейсу, но если у вас установлен Resharper , то только один клик откроет список производных классов / интерфейсов этого интерфейса, так что это не имеет большого значения. Вот почему я держу интерфейс в отдельном файле.

 

Скотт Уитлок
источник
5
Я согласен с этим ответом, ведь IDE должен адаптироваться к нашему дизайну, а не наоборот, верно?
Марктани
1
Кроме того, без Resharper F12 и Shift + F12 делают одно и то же. Даже не щелкнув правой кнопкой мыши :) (если честно, вам понадобится только прямое использование интерфейса, а не уверенность в том, что делает версия Resharper)
Daniel B
@DanielB: в Resharper Alt-End переходит к реализации (или предоставляет вам список возможных реализаций, к которым можно обратиться).
StriplingWarrior
@StriplingWarrior тогда кажется, что это именно то, что делает vanilla VS, тоже. F12 = перейти к определению, Shift + F12 = перейти к реализации (я уверен, что это работает без Resharper, хотя он у меня установлен, так что это сложно подтвердить). Это один из самых полезных ярлыков навигации, я просто распространяю слово.
Даниэль Б
@DanielB: по умолчанию для Shift-F12 "найти использования". jetbrains.com/resharper/webhelp/...
StriplingWarrior
15

Я думаю, вы должны хранить их в отдельных файлах. Как вы сказали, идея в том, чтобы оставаться тестируемым и взаимозаменяемым. Помещая интерфейс в тот же файл, что и ваша реализация, вы связываете интерфейс с конкретной реализацией. Если вы решите создать фиктивный объект или другую реализацию, интерфейс не будет логически отделен от FooService.

Брэд С
источник
10

Согласно SOLID, вы не только должны создавать интерфейс, и не только он должен находиться в другом файле, но и в другой сборке.

Зачем? Потому что любое изменение исходного файла, который компилируется в сборку, требует перекомпиляции сборки, а любое изменение сборки требует перекомпиляции любой зависимой сборки. Итак, если ваша цель, основанная на SOLID, состоит в том, чтобы иметь возможность заменить реализацию A реализацией B, в то время как класс C, зависящий от интерфейса, мне не нужно знать разницу, вы должны убедиться, что сборка с I в нем не меняется, тем самым защищая обычаи.

«Но это просто перекомпиляция», я слышу ваш протест. Ну, это может быть, но в вашем приложении для смартфона, которое облегчает пропускную способность данных ваших пользователей; загрузить один двоичный файл, который изменился, или загрузить этот двоичный файл и пять других с кодом, который зависит от него? Не каждая программа написана для использования на настольных компьютерах в локальной сети. Даже в том случае, когда пропускная способность и память дешевы, более мелкие выпуски исправлений могут иметь значение, поскольку их несложно распространить на всю локальную сеть через Active Directory или аналогичные уровни управления доменом; Ваши пользователи будут ждать только несколько секунд, пока он будет применен при следующем входе в систему, а не несколько минут, пока все будет переустановлено. Не говоря уже о том, что чем меньше сборок нужно перекомпилировать при создании проекта, тем быстрее он будет построен,

Теперь, отказ от ответственности: это не всегда возможно или возможно сделать. Самый простой способ сделать это - создать централизованный проект «интерфейсы». Это имеет свои недостатки; код становится менее пригодным для повторного использования, потому что на интерфейсный проект И на проект реализации нужно ссылаться в других приложениях, повторно использующих слой постоянства или другие ключевые компоненты вашего приложения. Вы можете преодолеть эту проблему, разделив интерфейсы на более тесно связанные сборки, но в вашем приложении будет больше проектов, что делает полную сборку очень болезненной. Ключ - баланс и поддержание слабосвязанной конструкции; обычно вы можете перемещать файлы по мере необходимости, поэтому, когда вы увидите, что классу потребуется много изменений или что регулярно потребуются новые реализации интерфейса (возможно, для взаимодействия с недавно поддерживаемыми версиями другого программного обеспечения,

Keiths
источник
8

Почему наличие отдельных файлов вызывает дискомфорт? Для меня это намного аккуратнее и чище. Обычно создается подпапка с именем «Interfaces» и помещается туда ваши файлы IFooServer.cs, если вы хотите видеть меньше файлов в обозревателе решений.

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

Авнер Шахар-Каштан
источник
6

Часто рекомендуется, чтобы файлы кода содержали только один класс или один интерфейс. Но эти методы кодирования являются средством для достижения цели - чтобы лучше структурировать ваш код, облегчая работу с ним. Если вам и вашей команде будет легче работать, если классы хранятся вместе с их интерфейсами, делайте это во что бы то ни стало.

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

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

Пит
источник
3
+1 за указание на то, что практика команды должна определять файловую структуру, и за то, что она противоречит норме.
3

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

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

StuartLC
источник
2

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

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

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


1 Примечательным исключением является случай, когда вам нужен интерфейс для тестирования, потому что ваш выбор среды моделирования накладывал это дополнительное требование на ваш код.

dasblinkenlight
источник
3
На самом деле вполне нормально иметь один интерфейс для одного класса в .NET, так как он позволяет модульным тестам заменять зависимости на mock, заглушки, spys или другие двойники тестов.
Пит
2
@ Пит Я отредактировал свой ответ, чтобы включить это как примечание. Я бы не назвал этот шаблон «нормальным», потому что он позволяет вашей проблеме тестируемости «просачиваться» в основную базу кода. Современные фальшивые фреймворки в значительной степени помогают вам преодолеть эту проблему, но наличие интерфейса определенно упрощает ситуацию.
dasblinkenlight
2
@KeithS Класс без интерфейса должен присутствовать в вашем проекте только тогда, когда вы абсолютно уверены, что нет смысла его подклассировать, и вы создаете этот класс sealed. Если вы подозреваете, что в будущем вы сможете добавить второй подкласс, вы сразу же включите интерфейс. PS Достаточно качественный помощник по рефакторингу может быть вашим по скромной цене одной лицензии ReSharper :)
dasblinkenlight
1
@KeithS И да, все ваши классы, которые специально не предназначены для создания подклассов, должны быть запечатаны. Фактически, классы желаний были запечатаны по умолчанию, что заставляло вас явно указывать классы для наследования, так же, как язык в настоящее время заставляет вас назначать функции для переопределения, помечая их virtual.
dasblinkenlight
2
@KeithS: Что происходит, когда одна ваша реализация становится двумя? Так же, как когда ваша реализация меняется; Вы меняете код, чтобы отразить это изменение. ЯГНИ применяется здесь. Вы никогда не узнаете, что изменится в будущем, так что сделайте самое простое возможное. (К сожалению, тестирование мешает с этим
принципом
2

Интерфейсы принадлежат их клиентам, а не их реализациям, как определено в гибких принципах, практиках и шаблонах Роберта К. Мартина. Таким образом, объединение интерфейса и реализации в одном месте противоречит его принципу.

Код клиента зависит от интерфейса. Это дает возможность компиляции и развертывания клиентского кода и интерфейса без реализации. Чем у вас могут быть разные реализации, работающие как плагины для клиентского кода.

ОБНОВЛЕНИЕ: Это не гибкий принцип здесь. «Бригада четырех в шаблонах проектирования» еще в 1994 году уже говорила о клиентах, придерживающихся интерфейсов и программирующих с интерфейсами. Их мнение схоже.

Паткос Чаба
источник
1
Использование интерфейсов выходит далеко за пределы области, которой обладает Agile. Agile - это методология разработки, которая использует языковые конструкции особым образом. Интерфейс - это языковая конструкция.
1
Да, но интерфейс по-прежнему принадлежит клиенту, а не реализации, независимо от того, идет речь о гибкой или нет.
Паткос Чаба
Я с тобой на этом.
Марьян Венема
0

Мне лично не нравится "я" перед интерфейсом. Как пользователь такого типа, я бы не хотел видеть, что это интерфейс или нет. Тип это интересная вещь. С точки зрения зависимостей ваш FooService является ОДНОЙ возможной реализацией IFooService. Интерфейс должен находиться рядом с местом, где он используется. С другой стороны, реализация должна быть в том месте, где ее можно легко изменить, не влияя на клиента. Итак, я бы предпочел два файла - нормально.

ollins
источник
2
Как пользователь, я хочу знать, является ли тип интерфейсом, потому что, например, это означает, что я не могу использовать newего, но, с другой стороны, я могу использовать его совместно или наоборот. И Iэто установленное соглашение об именах в .Net, так что это хорошая причина придерживаться его, хотя бы для согласованности с другими типами.
svick
Начиная с Iв интерфейсе только введение в мои рассуждения о разделении в двух файлах. Код лучше читается и делает инверсию зависимостей более понятной.
ollins
4
Нравится вам это или нет, но использование «I» перед именем интерфейса является стандартом де-факто в .NET. Несоблюдение стандарта в проекте .NET, на мой взгляд, будет нарушением «принципа наименьшего удивления».
Пит
Мой главный вопрос: разделить на два файла. Один для абстракции и один для реализации. Если использование Iв вашей среде является нормальным: используйте его! (Но мне это не нравится :))
ollins
А в случае P.SE префикс I перед интерфейсом делает более очевидным, что автор говорит об интерфейсе, а не о наследовании от другого класса.
0

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

Дракон Шиван
источник
наличие только одной реализации в вашем приложении интерфейса не обязательно плохо; можно было бы защитить приложение от технологических изменений. Например, интерфейс для технологии сохранения данных. У вас будет только одна реализация для текущей технологии сохранения данных при защите приложения, если это изменится. Так что в то время был только один чертенок.
Цмит
0

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

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

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

Holf
источник
Это не просто «организационное удобство» ... это также вопрос управления рисками, управления развертыванием, отслеживания кода, контроля изменений, помех контроля исходного кода, управления безопасностью. Есть много точек зрения на этот вопрос.
Цмит