Всегда ли плохо использовать «новый» в конструкторе?

37

Я читал, что использование «нового» в конструкторе (для любых других объектов, кроме простых значений) является плохой практикой, так как делает невозможным модульное тестирование (так как тогда эти коллаборационисты тоже должны быть созданы и не могут быть смоделированы). Поскольку я не очень опытен в модульном тестировании, я пытаюсь собрать некоторые правила, которые я изучу в первую очередь. Кроме того, действительно ли это правило действует независимо от используемого языка?

Эзоэла Вакка
источник
9
Это не делает тестирование невозможным. Однако это усложняет поддержку и тестирование вашего кода. Прочитайте новый клей, например.
Дэвид Арно
38
«всегда» всегда неверно. :) Есть лучшие практики, но исключений предостаточно.
Павел
63
Что это за язык? newозначает разные вещи на разных языках.
user2357112 поддерживает Monica
13
Этот вопрос немного зависит от языка, не так ли? Не на всех языках даже есть newключевое слово. На каких языках вы спрашиваете?
Брайан Оукли
7
Глупое правило. Неспособность использовать внедрение зависимостей, когда вы должны иметь очень мало общего с ключевым словом «new». Вместо того чтобы говорить, что «использование нового является проблемой, если вы используете его, чтобы сломать инверсию зависимости», просто скажите «не ломайте инверсию зависимости».
Мэтт Тиммерманс

Ответы:

36

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

Использование new в конструкторе нарушает D в SOLID (принцип инверсии зависимостей). Это затрудняет тестирование вашего кода, потому что модульное тестирование - это изоляция; трудно выделить класс, если у него есть конкретные ссылки.

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

Отсутствие нового в конструкторе делает ваш код более гибким. Это также относится к языкам, которые могут использовать конструкции, отличные от newинициализации объекта.

Тем не менее, ясно, что вам нужно использовать здравый смысл. Есть много раз, когда это хорошо использовать new, или где было бы лучше не делать, но у вас не будет негативных последствий. В какой-то момент, где-то, newдолжен быть вызван. Просто будьте очень осторожны при вызове newвнутри класса, от которого зависит множество других классов.

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

Чем больше ссылок на него имеет класс, тем осторожнее вы не должны вызывать newего изнутри.

TheCatWhisperer
источник
12
Я действительно не вижу никакой ценности в этом правиле. Существуют правила и рекомендации о том, как избежать тесной связи в коде. Это правило, похоже, имеет дело с точно такой же проблемой, за исключением того, что оно невероятно ограничено, но не дает нам никакой дополнительной ценности. Так какой смысл? Зачем создавать фиктивный объект заголовка для моей реализации LinkedList вместо того, чтобы заниматься всеми теми особыми случаями, которые возникают из- head = nullза какого-либо улучшения кода? Почему лучше оставить включенные коллекции как нулевые и создавать их по требованию, чем в конструкторе?
Во
22
Вы упускаете суть. Да, модуль персистентности - это абсолютно то, от чего не должен зависеть модуль более высокого уровня. Но «никогда не используй новое» не следует из «некоторых вещей, которые нужно вводить и не связывать напрямую». Если вы возьмете верную копию Agile Software Development, вы увидите, что Мартин обычно говорит о модулях, а не об отдельных классах: «Модули высокого уровня имеют дело с политиками высокого уровня приложения. Эти политики обычно мало заботятся о деталях, которые реализуют их."
Во
11
Итак, дело в том, что вам следует избегать зависимостей между модулями, особенно между модулями разных уровней абстракции. Но модуль может быть более чем одним классом, и просто нет смысла устанавливать интерфейсы между тесно связанным кодом одного и того же уровня абстракции. В хорошо спроектированном приложении, которое следует принципам SOLID, не каждый класс должен реализовывать интерфейс, и не каждый конструктор всегда должен принимать интерфейсы только в качестве параметров.
Во
18
Я понизил голос, потому что это много модного супа и не много дискуссий о практических соображениях. Есть кое-что о репо с двумя БД, но, честно говоря, это не реальный пример.
jpmc26
5
@ jpmc26, BJoBnh Не вдаваясь в вопрос о том, оправдываю ли я этот ответ или нет, этот вопрос выражен очень четко. Если вы думаете, что «принцип инверсии зависимостей», «конкретная ссылка» или «инициализация объекта» - это просто модные слова, то на самом деле вы просто не знаете, что они означают. Это не вина ответа.
Р. Шмитц
50

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

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


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

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

Эрик Эйдт
источник
4
+1. Это совершенно верно. Вы должны определить, какое поведение класса предназначено, и это определяет, какие детали реализации должны быть раскрыты, а какие нет. Есть некая тонкая интуитивная игра, в которую вы должны играть, чтобы определить, что такое «ответственность» класса.
jpmc26
27

Не все соавторы достаточно интересны для отдельного модульного тестирования, вы можете (косвенно) протестировать их через класс хостинга / создания экземпляров. Это может не совпадать с идеей некоторых людей о необходимости тестирования каждого класса, каждого открытого метода и т. Д., Особенно при выполнении теста после. При использовании TDD вы можете реорганизовать этого «соавтора», извлекая класс, в котором он уже полностью тестируется из вашего первого процесса тестирования.

Joppe
источник
14
Not all collaborators are interesting enough to unit-test separatelyконец истории :-), этот случай возможен, и никто не осмелился упомянуть. @Joppe Я призываю вас немного уточнить ответ. Например, вы можете добавить несколько примеров классов, которые являются простыми деталями реализации (не пригодными для замены) и как их можно извлечь последним, если мы сочтем это необходимым.
Laiv
Модели доменов @Laiv, как правило, являются конкретными, не абстрактными, вы не будете вставлять туда вложенные объекты. Простой объект значения / сложный объект, не имеющий никакой логики, также являются кандидатами.
Joppe
4
+1, абсолютно. Если язык настроен так, что вам нужно вызывать new File()что-либо связанное с файлом, нет смысла запрещать этот вызов. Что вы собираетесь делать, писать регрессионные тесты для Fileмодуля stdlib ? Скорее всего, не. С другой стороны, призыв newк самодовольному классу более сомнителен.
Килиан Фот
7
@KilianFoth: Удачи, юнит тестирует все, что напрямую вызывает new File(), хотя.
Фоши
1
Это догадки . Мы можем найти случаи, когда это не имеет смысла, случаи, когда это бесполезно, и случаи, когда это имеет смысл и полезно. Это вопрос потребностей и предпочтений.
августа
13

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

Внимательно изучайте «правила» для проблем, с которыми вы никогда не сталкивались. Если вы сталкиваетесь с каким-то «правилом» или «наилучшей практикой», я бы предложил найти простой игрушечный пример того, как «правило» следует использовать, и попытаться решить эту проблему самостоятельно , игнорируя то, что говорит «правило».

В этом случае вы можете попытаться создать 2 или 3 простых класса и некоторые варианты поведения, которые они должны реализовать. Реализуйте классы любым удобным для вас способом и напишите модульный тест для каждого поведения. Составьте список всех проблем, с которыми вы столкнулись, например, если вы начали с того, что все работает в одну сторону, то вам нужно было вернуться и изменить это позже; если вы запутались в том, как вещи должны соответствовать друг другу; если вас раздражает написание шаблона; и т.п.

Затем попробуйте решить ту же проблему, следуя «правилу». Опять же, составьте список проблем, с которыми вы столкнулись. Сравните списки и подумайте, какие ситуации могут быть лучше при соблюдении правила, а какие нет.


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

Основная логика заключается в том, чтобы вычислять вещи «внутри» приложения на основе проблемной области. Он может содержать классы , как User, Document, Order, Invoice, штраф и т.д. Это иметь базовые классы называть newдля других основных классов, так как они «внутренней» детали реализации. Например, создание Orderможет также создать Invoiceи Documentдетализацию того, что было заказано. Во время тестов нет нужды насмехаться над этим, потому что это именно то, что мы хотим протестировать!

Порты и адаптеры - это то, как ядро ​​логики взаимодействует с внешним миром. Это где вещи , как Database, ConfigFile, EmailSenderи т.д. жить. Это то, что усложняет тестирование, поэтому желательно создавать их вне базовой логики и передавать их по мере необходимости (либо с помощью внедрения зависимости, либо в качестве аргументов метода и т. Д.).

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

Порты и адаптеры можно тестировать отдельно, используя макеты для базы данных, файловой системы и т. Д., Не заботясь о бизнес-логике. Мы можем просто передать некоторые примеры значений и убедиться, что они хранятся / читаются / отправляются / и т.д. соответственно.

Warbo
источник
6

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

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

Использование new в конструкторе нарушает D в SOLID (принцип обращения зависимости). Это затрудняет тестирование вашего кода, потому что модульное тестирование - это изоляция; трудно выделить класс, если у него есть конкретные ссылки.

-TheCatWhisperer-

Да, использование newвнутренних конструкторов часто приводит к недостаткам конструкции (например, к жесткой связи), что делает нашу конструкцию жесткой. Трудно проверить да, но не невозможно. Собственность в игре здесь - устойчивость (терпимость к изменениям) 1 .

Тем не менее приведенная цитата не всегда верна. В некоторых случаях могут быть классы , которые должны быть тесно связаны . Дэвид Арно прокомментировал пару.

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

Давид Арно

В точку. Некоторые классы (например, inner-классы) могут быть просто деталями реализации основного класса. Они предназначены для тестирования вместе с основным классом, и они не обязательно заменяемы или расширяемы.

Более того, если наш культ SOLID заставляет нас извлекать эти классы , мы можем нарушать еще один хороший принцип. Так называемый Закон Деметры . Что, с другой стороны, мне кажется очень важным с точки зрения дизайна.

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

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


1: основная цель SOLID - не сделать наш код более тестируемым. Это делает наш код более терпимым к изменениям. Более гибкий и, следовательно, легче тестировать

Примечание: Дэвид Арно, TheWhisperCat, надеюсь, вы не возражаете, я вас процитировал.

LAIV
источник
3

В качестве простого примера рассмотрим следующий псевдокод

class Foo {
  private:
     class Bar {}
     class Baz inherits Bar {}
     Bar myBar
  public:
     Foo(bool useBaz) { if (useBaz) myBar = new Baz else myBar = new Bar; }
}

Поскольку newэто чистая реализация деталей Foo, и то и другое Foo::Barи Foo::Bazявляются частью Foo, при модульном тестировании Fooнет смысла в насмешливых частях Foo. Вы только издеваетесь над деталями снаружи Foo при юнит-тестировании Foo.

MSalters
источник
-3

Да, использование 'new' в корневых классах вашего приложения - это запах кода. Это означает, что вы привязываете класс к использованию определенной реализации и не сможете заменить другую. Всегда выбирайте внедрение зависимости в конструктор. Таким образом, вы не только сможете легко внедрить ложные зависимости во время тестирования, но это сделает ваше приложение гораздо более гибким, позволяя вам при необходимости быстро заменять различные реализации.

РЕДАКТИРОВАТЬ: Для downvoters - вот ссылка на книгу разработки программного обеспечения, помечающую «новый» как возможный запах кода: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = OnePage & д = новый% 20keyword% 20code% 20smell & F = ложь

Eternal21
источник
15
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.Почему это проблема? Не каждая отдельная зависимость в дереве зависимостей должна быть открыта для замены
Laiv
4
@Paul: Наличие реализации по умолчанию означает, что вы берете тесно связанную ссылку на конкретный класс, указанный в качестве значения по умолчанию. Это не делает его так называемым «запахом кода».
Роберт Харви
8
@EzoelaVacca: Я бы с осторожностью использовал слова «запах кода» в любом контексте. Это как сказать «республиканец» или «демократ»; что эти слова вообще значат? Как только вы даете что-то подобное, вы перестаете думать о реальных проблемах, и обучение прекращается.
Роберт Харви
4
«Более гибкий» не означает автоматически «лучший».
whatsisname
4
@EzoelaVacca: Использование newключевого слова не плохая практика, и никогда не было. Важно то, как вы используете инструмент. Вы не используете кувалду, например, там, где достаточно шарикового молотка.
Роберт Харви