Не объявляйте интерфейсы для неизменяемых объектов

27

Не объявляйте интерфейсы для неизменяемых объектов

[РЕДАКТИРОВАТЬ] Где рассматриваемые объекты представляют объекты передачи данных (DTO) или простые старые данные (POD)

Это разумное руководство?

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

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

Из-за этой проблемы я рассматриваю возможность никогда не объявлять интерфейсы для неизменяемых объектов.

Это может иметь последствия в отношении модульного тестирования, но кроме этого, кажется ли это разумным руководством?

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

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

[РЕДАКТИРОВАТЬ]

Чтобы предоставить мне гораздо больше смысла в желании сделать объекты неизменяемыми, посмотрите это сообщение в блоге Эрика Липперта:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

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

Также Джошуа Блох рекомендует использовать неизменяемые объекты в своей книге «Эффективная Java» .


Следовать за

Спасибо за отзыв, все. Я решил пойти дальше и использовать это руководство для DTO и им подобных. Пока все работает хорошо, но прошла всего неделя ... Тем не менее, выглядит хорошо.

Есть некоторые другие вопросы, связанные с этим, о которых я хочу спросить; особенно то, что я называю «Глубокая или неглубокая неизменность» (номенклатура, которую я украл из Глубокого и неглубокого клонирования) - но это вопрос в другой раз.

Мэтью Уотсон
источник
3
+1 отличный вопрос. Пожалуйста, объясните, почему для вас так важно, чтобы объект принадлежал именно этому неизменяемому классу, а не тому, что просто реализует тот же интерфейс. Возможно, ваши проблемы кроются в другом месте. Внедрение зависимостей чрезвычайно важно для модульного тестирования, но имеет много других важных преимуществ, которые исчезают, если вы заставляете свой код требовать определенного неизменяемого класса.
Стивен Доггарт
1
Я немного смущен тем, что вы используете термин « неизменный» . Просто для ясности, вы имеете в виду запечатанный класс, который не может быть унаследован и переопределен, или вы имеете в виду, что предоставляемые им данные доступны только для чтения и не могут быть изменены после создания объекта. Другими словами, хотите ли вы гарантировать, что объект имеет определенный тип или что его значение никогда не изменится. В моем первом комментарии я предположил, что имел в виду первое, но теперь с вашим редактированием это больше похоже на второе. Или вы обеспокоены обоими?
Стивен Доггарт
3
Я запутался, почему бы просто не оставить сеттеры за пределами интерфейса? Но с другой стороны, я устаю видеть интерфейсы для объектов, которые действительно являются объектами домена и / или DTO, требующими фабрики для этих объектов ... вызывает когнитивный диссонанс для меня.
Майкл Браун
2
Как насчет интерфейсов для классов, которые все неизменны? Например, в Java Номер класса позволяет , чтобы определить , List<Number>который может содержать Integer, Float, Long, BigDecimalи т.д. ... Все это неизменны сами.
1
@MikeBrown Я думаю, что только потому, что интерфейс подразумевает неизменность, не означает, что реализация обеспечивает его. Вы могли бы просто оставить это до соглашения (то есть документировать интерфейс, который требует неизменности), но вы можете столкнуться с некоторыми действительно неприятными проблемами, если кто-то нарушит соглашение.
vaughandroid

Ответы:

17

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

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

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

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

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

Стивен Доггарт
источник
Код, который специально предназначен для работы с DTO или объектом с неизменяемыми значениями, должен использовать функции этого типа, а не функции интерфейса, но это не означает, что не будет вариантов использования для интерфейса, который реализуется таким классом, а также другими классами, с методами, которые обещают возвращать значения, которые будут действительны в течение некоторого минимального времени (например, если интерфейс передается функции, методы должны возвращать действительные значения, пока эта функция не вернется). Такой интерфейс может быть полезен, если есть несколько типов, которые инкапсулируют сходные данные, и кто-то желает ...
суперкат
... чтобы иметь возможность копировать данные из одного в другой. Если все типы, которые включают в себя определенные данные, реализуют один и тот же интерфейс для его чтения, то такие данные могут быть легко скопированы среди всех типов, не требуя, чтобы каждый знал, как импортировать из каждого из других.
суперкат
В этот момент вы можете также исключить ваши методы получения (и установки, если ваши DTO изменчивы по какой-то глупой причине) и сделать поля открытыми. Вы никогда не собираетесь помещать туда какую-либо логику, верно?
Кевин
@Kevin Полагаю, но с современным удобством свойств авто так легко сделать их свойства, почему бы и нет? Я полагаю, если производительность и эффективность имеют первостепенное значение, то, возможно, это имеет значение. В любом случае, вопрос в том, какой уровень «логики» вы допускаете в DTO? Даже если вы добавите некоторую логику проверки в его свойствах, не будет проблем с модульным тестированием, если у него нет зависимостей, которые потребовали бы проверки. До тех пор, пока DTO не использует бизнес-объекты зависимостей, в них можно встроить небольшую логику, потому что они по-прежнему будут тестируемыми.
Стивен Доггарт
Лично я предпочитаю держать их в чистоте от подобных вещей, насколько это возможно, но всегда есть моменты, когда необходимо изменить правила, и поэтому лучше оставить дверь открытой на всякий случай.
Стивен Доггарт
7

Раньше я суетился из-за того, что мой код неуязвим для неправильного использования. Я сделал интерфейсы только для чтения для сокрытия мутирующих членов, добавил много ограничений к моим общим сигнатурам и т. Д. И т. Д. Оказалось, что большую часть времени я принимал дизайнерские решения, потому что не доверял своим воображаемым коллегам. «Возможно, когда-нибудь они наймут нового парня начального уровня, и он не узнает, что класс XYZ не может обновить DTO ABC. О, нет!» В другой раз я сосредоточился на неправильной проблеме - игнорируя очевидное решение - не видя леса сквозь деревья.

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

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

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

Это похоже на нормальное руководство, но по странным причинам. У меня было несколько мест, где интерфейс (или абстрактный базовый класс) обеспечивает равномерный доступ к серии неизменяемых объектов. Стратегии имеют тенденцию падать здесь. Государственные объекты имеют тенденцию падать здесь. Я не думаю, что слишком неразумно формировать интерфейс, чтобы он казался неизменным, и документировать его как таковой в вашем API.

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

Telastyn
источник
2

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

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

Однако, на мой взгляд, нет такой вещи, как неизменный интерфейс - контракт кода, указанный интерфейсом, применяется только к открытым методам объекта и ничего не относится к внутренним объектам объекта.

Гораздо более распространено видеть неизменность реализованной как декоратор, а не как интерфейс, но выполнимость этого решения действительно зависит от структуры вашего объекта и сложности вашей реализации.

Джонатан Рич
источник
1
Не могли бы вы привести пример реализации неизменяемости в качестве декоратора?
vaughandroid
Не тривиально, я не думаю, но это относительно простое мысленное упражнение - если MutableObjectесть nметоды, которые изменяют состояние, и mметоды, которые возвращают состояние, ImmutableDecoratorмогут продолжать предоставлять методы, которые возвращают state ( m), и, в зависимости от среды, утверждать или генерировать исключение при вызове одного из изменяемых методов.
Джонатан Рич
Мне действительно не нравится преобразовывать определенность времени компиляции в возможность исключений во время выполнения ...
Мэтью Уотсон
Хорошо, но как ImmutableDecorator может узнать, изменяет ли данный метод состояние или нет?
vaughandroid
Вы ни в коем случае не можете быть уверены, что класс, реализующий ваш интерфейс, неизменен относительно методов, определенных вашим интерфейсом. @Baqueta Декоратор должен знать о реализации базового класса.
Джонатан Рич
0

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

В C # метод, который ожидает объект «неизменяемого» типа, не должен иметь параметр типа интерфейса, потому что интерфейсы C # не могут указывать контракт неизменности. Таким образом, руководство, которое вы предлагаете, само по себе не имеет смысла в C #, потому что вы не можете сделать это в первую очередь (и будущие версии языков вряд ли позволят вам это сделать).

Ваш вопрос проистекает из тонкого недопонимания статей Эрика Липперта об неизменности. Эрик не определял интерфейсы IStack<T>и не IQueue<T>определял контракты для неизменных стеков и очередей. Они не Он определил их для удобства. Эти интерфейсы позволили ему определять различные типы для пустых стеков и очередей. Мы можем предложить другой дизайн и реализацию неизменяемого стека с использованием одного типа, не требуя интерфейса или отдельного типа для представления пустого стека, но полученный код не будет выглядеть таким чистым и будет немного менее эффективным.

Теперь давайте придерживаться дизайна Эрика. Метод, который требует неизменного стека, должен иметь параметр типа, Stack<T>а не общий интерфейс, IStack<T>который представляет абстрактный тип данных стека в общем смысле. Не очевидно, как это сделать при использовании неизменного стека Эрика, и он не обсуждал это в своих статьях, но это возможно. Проблема с типом пустого стека. Вы можете решить эту проблему, убедившись, что вы никогда не получите пустой стек. Это может быть обеспечено путем добавления фиктивного значения в качестве первого значения в стеке и никогда не выталкивать его. Таким образом, вы можете смело приводить результаты Pushи Popк Stack<T>.

Наличие Stack<T>инструментов IStack<T>может быть полезным. Вы можете определить методы, которые требуют стек, любой стек, не обязательно неизменный стек. Эти методы могут иметь параметр типа IStack<T>. Это позволяет вам передавать ему неизменные стеки. В идеале, IStack<T>было бы частью самой стандартной библиотеки. В .NET его нет IStack<T>, но есть и другие стандартные интерфейсы, которые может реализовать неизменяемый стек, что делает тип более полезным.

Альтернативный дизайн и реализация неизменяемого стека, о котором я упоминал ранее, использует интерфейс под названием IImmutableStack<T>. Конечно, вставка «неизменяемого» в имя интерфейса не делает каждый тип, который его реализует, неизменным. Однако в этом интерфейсе договор неизменности является просто устным. Хороший разработчик должен уважать это.

Если вы разрабатываете небольшую внутреннюю библиотеку, вы можете договориться со всеми в команде о соблюдении этого контракта и можете использовать ее IImmutableStack<T>в качестве типа параметров. В противном случае вы не должны использовать тип интерфейса.

Я хотел бы добавить, поскольку вы пометили вопрос C #, что в спецификации C # нет такой вещи, как DTO и POD. Поэтому их отбрасывание или точное определение улучшает вопрос.

Хади Браис
источник