Являются ли методы init () запахом кода?

20

Есть ли цель объявить init()метод для типа?

Я не спрашиваю, стоит ли нам отдавать предпочтение init()конструктору или как избежать объявленияinit() .

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


Эта init()идиома довольно распространена, но я пока не вижу реальной выгоды.

Я говорю о типах, которые поощряют инициализацию с помощью метода:

class Demo {
    public void init() {
        //...
    }
}

Когда это когда-нибудь пригодится в производственном коде?


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

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

Винс Эмиг
источник
1
Чтобы "... видеть, как это часто встречается ...": это часто? Можете привести несколько примеров? Возможно, вы имеете дело с каркасом, который требует разделения инициализации и конструкции.
Приходите с
Находится ли метод в базовом или производном классе, или в обоих? (Или: найден ли метод в классе, который принадлежит иерархии наследования? Вызывает ли базовый класс init()производный класс, или наоборот?) Если это так, это пример того, как базовый класс может выполнить «пост-конструктор» "который может быть выполнен только после того, как самый производный класс закончил конструкцию. Это пример многофазной инициализации.
Rwong
Если вы не хотите инициализировать в момент создания экземпляра, имеет смысл разделить их.
JᴀʏM16
вам может быть интересно Хорошая практика
запускай, беги или исполняй

Ответы:

39

Да, это запах кода. Запах кода не обязательно должен быть удален. Это то, что заставляет вас взглянуть второй раз.

Здесь у вас есть объект в двух принципиально разных состояниях: pre-init и post-init. Эти государства имеют разные обязанности, разные методы, которые могут быть вызваны, и разное поведение. Это фактически два разных класса.

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

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

Карл Билефельдт
источник
6
Ваше предложение попробовать модель с двумя классами хорошо. Предложение конкретного шага для устранения запаха кода полезно.
Иван
1
Это лучший ответ на данный момент. Однако меня беспокоит одна вещь: «В этих штатах разные обязанности, разные методы, которые можно вызывать, и разное поведение » - не разделение этих обязанностей нарушает ПСП. Если бы целью программного обеспечения было копирование сценария реального мира во всех аспектах, это имело бы смысл. Но в процессе разработки разработчики в основном пишут код, который способствует простоте управления, пересматривая модель, если это необходимо, чтобы лучше соответствовать программной среде (продолжение в следующем комментарии)
Винс Эми
1
Реальный мир - это другая среда. Программы, пытающиеся реплицировать его, кажутся специфичными для домена, так как в большинстве случаев вы не будете / не должны учитывать это. Многие вещи, которые осуждаются, принимаются, когда дело касается проектов, специфичных для предметной области, поэтому я стараюсь сделать это как можно более общим (например, то, как вызов thisв конструкторе является запахом кода и может привести к подверженному ошибкам коду, и избегать его рекомендуется независимо от того, к какому домену относится ваш проект).
Винс Эми
14

По-разному.

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

Быстрый поиск в Google дал мне этот пример. Я легко могу представить себе больше случаев, когда код, выполняемый во время размещения объекта (конструктор), может быть лучше отделен от самой инициализации. Возможно, у вас есть выровненная система, и распределение / построение происходит на уровне X, но инициализация только на уровне Y, потому что только Y может обеспечить необходимые параметры. Возможно, init является дорогостоящим и должен выполняться только для подмножества выделенных объектов, и определение этого подмножества может быть выполнено только на уровне Y. Или вы хотите переопределить (виртуальный) метод init в производном класс, который нельзя сделать с помощью конструктора. Возможно, уровень X предоставляет вам выделенные объекты из дерева наследования, но уровень Y не знает о конкретном выводе, только об общем интерфейсе (гдеinit может быть определен).

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

Док Браун
источник
2
Ответ в этой ссылке говорит, что это полезно для уменьшения объема работы в конструкторе, но поощряет частично созданные объекты. Меньшие конструкторы могут быть получены с помощью декомпозиции, поэтому мне кажется, что ответы создают новую проблему (возможность забыть вызвать все необходимые методы инициализации, оставляя объект подверженным ошибкам) ​​и попадают под категорию запаха кода
Vince Emigh
@VinceEmigh: хорошо, это был первый пример , который я мог бы найти здесь на platfform SE, может быть , не лучший, но есть законные прецеденты для отдельного initметода. Однако всякий раз, когда вы видите такой метод, не стесняйтесь ставить под сомнение его необходимость.
Док Браун
Я подвергаю сомнению / оспариваю каждый вариант использования для этого, поскольку я чувствую, что нет никакой ситуации, где это было бы необходимо. Для меня это плохое время создания объекта, и его следует избегать, так как он является кандидатом на ошибки, которых можно избежать с помощью правильного проектирования. Если бы было правильное использование init()метода, я уверен, что мне было бы полезно узнать о его назначении. Извините за мое невежество, я просто удивлен тем, как тяжело мне время от времени находить для него применение, не позволяя мне подумать об этом, чего следует избегать
Винс Эми
1
@ VinceEmigh: когда вы не можете думать о такой ситуации, вам нужно работать над своим воображением ;-). Или прочитайте мой ответ еще раз, не сводите его просто к «распределению». Или работать с большим количеством фреймворков от разных поставщиков.
Док Браун
1
@DocBrown Использование воображения, а не проверенной практики, приводит к некоторому прикольному коду, такому как инициализация двойной скобкой: умная, но неэффективная, и ее следует избегать. Я знаю, что поставщики используют его, но это не означает, что использование оправдано. Если вы чувствуете, что это так, пожалуйста, дайте мне знать, почему, поскольку это то, что я пытался выяснить. Вы, казалось, зашли в тупик, имея НЕКОТОРЫЕ полезные цели, но похоже, что вам трудно привести пример, который поощряет хороший дизайн. Я знаю, при каких обстоятельствах его можно использовать, но стоит ли его использовать?
Винс Эми
5

Мой опыт разбивается на две группы:

  1. Код, где фактически требуется init (). Это может произойти, когда суперкласс или фреймворк не позволяют конструктору вашего класса получить все его зависимости во время построения.
  2. Код, в котором используется init (), но его можно было избежать.

В моем личном опыте я видел только несколько случаев (1), но намного больше случаев (2). Следовательно, я обычно предполагаю, что init () - это запах кода, но это не всегда так. Иногда вы просто не можете обойти это.

Я обнаружил, что использование шаблона Builder часто помогает устранить необходимость / желание иметь init ().

Иван
источник
1
Если суперкласс или фреймворк не позволяют типу получать зависимости, в которых он нуждается через конструктор, как его может init()решить добавление метода? init()Метод должен либо параметры принять зависимости, или вы должны создать экземпляр зависимостей в пределах от init()метода, который вы могли бы также сделать с помощью конструктора. Не могли бы вы привести пример?
Винс Эми
1
@VinceEmigh: init () иногда можно использовать для загрузки файла конфигурации из внешнего источника, открытия соединения с базой данных или чего-то подобного. Для этого используется метод DoFn.initialize () (из инфраструктуры Apache Crunch). Он также может использоваться для загрузки несериализуемых внутренних полей (DoFns должен быть сериализуемым). Две проблемы здесь заключаются в том, что (1) что-то должно гарантировать, что вызывается метод инициализации, и (2) объект должен знать, где он собирается (или как он собирается строить) эти ресурсы.
Иван
1

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

Подводя итог: как для большинства дилемм, является ли это запахом кода или нет, зависит от ситуации и обстоятельств.

Владимир Стокич
источник
Если обновления конфигурации, и для этого требуется объект для сброса / изменить ее состояние в зависимости от конфигурации, вы не думаете , что было бы лучше иметь объект выступать в качестве наблюдателя к Config?
Винс Эми
@ Винс Эми Не обязательно. Обозреватель работал бы, если бы я знал точный момент, когда изменяется конфигурация. Однако, если данные конфигурации хранятся в файле, который может быть изменен вне приложения, тогда не существует действительно элегантного подхода. Например, если у меня есть программа, которая анализирует некоторые файлы и преобразует данные в некоторую внутреннюю модель, а отдельный файл конфигурации содержит значения по умолчанию для отсутствующих данных, если я изменю значения по умолчанию, я буду читать их снова при следующем запуске разбор. В этом случае было бы очень удобно иметь метод Init в моем приложении.
Владимир Стокич
Если файл конфигурации внешне изменяется во время выполнения, нет способа перезагрузить эти переменные без какого- либо уведомления, уведомляющего ваше приложение о том, что оно должно вызвать init ( update/ reload, вероятно, было бы более описательным для такого рода поведения), чтобы фактически зарегистрировать эти изменения. , В этом случае это уведомление приведет к внутреннему изменению значений конфига в вашем приложении, что, как я полагаю, можно наблюдать, наблюдая за вашим конфигом, уведомляя наблюдателей, когда конфиг получает указание изменить одно из его значений. Или я неправильно понимаю ваш пример?
Винс Эми
Приложение берет некоторые внешние файлы (экспортированные из некоторой ГИС или другой системы), анализирует эти файлы и преобразует их в некоторую внутреннюю модель, которую используют системы, частью которых является приложение. В ходе этого процесса можно найти некоторые пробелы в данных, и эти пробелы в данных можно заполнить, установив значения по умолчанию. Эти значения по умолчанию можно сохранить в конфигурационном файле, который можно прочитать в начале процесса преобразования, который вызывается всякий раз, когда появляются новые файлы для преобразования. Вот где метод Init может пригодиться.
Владимир Стокич
1

Зависит от того, как вы их используете.

Я использую этот шаблон в языках сборки мусора, таких как Java / C #, когда не хочу перераспределять объект в куче (например, когда я создаю видеоигру и мне нужно поддерживать высокую производительность, сборщики мусора снижают производительность). Я использую конструктор для других необходимых ему распределений кучи и initдля создания базового полезного состояния прямо перед каждым повторным использованием. Это связано с концепцией объектных пулов.

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

В общем, это запах кода.

Cody
источник
Не будет ли reset()метод более описательным для вашего первого утверждения? Что касается второго (много конструкторов), наличие множества конструкторов является запахом кода. Предполагается, что объект имеет несколько целей / обязанностей, что предполагает нарушение SRP. У объекта должна быть одна ответственность, и конструктор должен определить необходимые зависимости для этой одной ответственности. Если у вас есть несколько конструкторов из-за необязательных значений, они должны выдвигаться (что также является запахом кода, вместо этого следует использовать конструктор).
Винс Эми
@ VinceEmigh вы можете использовать init или reset, это всего лишь имя. Init имеет больше смысла в контексте, в котором я склонен его использовать, так как не имеет смысла сбрасывать объект, который никогда не был установлен в первый раз, когда я его использую. Что касается проблемы конструктора, я стараюсь избегать большого количества конструкторов, но иногда это полезно. Посмотрите на stringсписок конструкторов любого языка , множество вариантов. Для меня обычно это может быть максимум 3 конструктора, но общее подмножество инструкций при инициализации имеет смысл, когда они разделяют какой-либо код, но отличаются в любом случае.
Коди
Имена чрезвычайно важны. Они описывают поведение, которое выполняется, не заставляя клиента читать договор. Плохое наименование может привести к ложным предположениям. Что касается нескольких конструкторов в String, это можно решить, развязав создание строк. В конце концов, a Stringесть a String, и его конструктор должен принимать только то, что ему нужно, чтобы он работал как нужно. Большинство из этих конструкторов представлены для целей преобразования, что является неправильным использованием конструкторов. Конструкторы не должны выполнять логику, иначе они рискуют потерпеть неудачу при инициализации, оставляя вас с бесполезным объектом.
Винс Эми
JDK полон ужасных замыслов, я мог бы перечислить около 10 в верхней части моей головы. Дизайн программного обеспечения развивался с тех пор, как основные аспекты многих языков были обнародованы, и они остаются из-за возможности взлома кода, если он будет изменен в наше время.
Винс Эми
1

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

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

Кроме таких случаев, я думаю, что все должно идти в конструктор.

tofro
источник
Если тип требует подключения, вы должны использовать DI. Если при создании соединения возникает проблема, вам не следует создавать объект, который требует этого. Если вы продвинете создание соединения внутри класса, вы создадите объект, этот объект будет создавать его зависимости (соединение). Если создание экземпляров зависимостей не удается, вы получаете объект, который не можете использовать, тратя таким образом ресурсы.
Винс Эми
Не обязательно. Вы получаете объект, который временно не можете использовать для любых целей. В таких случаях объект может просто действовать как очередь или прокси, пока ресурс не станет доступным. Целиком осуждающие initметоды слишком ограничивающие, ИМХО.
до
0

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

С другой стороны, я использую простую инфраструктуру конфигурации, которая требует открытого конструктора без параметров и интерфейсов для ввода зависимостей и значений конфигурации. После того, как это сделано, инфраструктура конфигурации вызывает initметод объекта: теперь вы получили все, что у меня есть для вас, выполните последние шаги, чтобы подготовиться к работе. Но обратите внимание: это инфраструктура конфигурации, которая автоматически вызывает метод init, так что вы не забудете его вызвать.

Бернхард Хиллер
источник
0

Нет никакого запаха кода, если метод init () семантически встроен в жизненный цикл состояния объекта.

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

Существует несколько технических причин, по которым такая структура существует:

  1. каркасный крюк
  2. сброс объекта в исходное состояние (избегать избыточности)
  3. возможность переопределения во время тестов
oopexpert
источник
1
Но зачем программисту встраивать его в жизненный цикл? Есть ли цель, которая оправдана (учитывая, что она поощряет частично построенные объекты) и не может быть сбита более эффективной альтернативой? Я чувствую, что встраивание его в жизненный цикл было бы запахом кода, и что его следует заменить на лучшее время создания объекта (зачем выделять память для объекта, если вы не планируете использовать его в то время? Зачем создавать объект, который частично построен, когда вы можете просто подождать, пока вам действительно не понадобится объект перед его созданием?)
Vince Emigh
Дело в том, что объект должен быть пригодным для использования ДО ТОГО, как вы вызовете метод init. Может быть, по-другому, чем после того, как это называется. Смотри государственный шаблон. В смысле частичного построения объекта это кодовый запах.
oopexpert
-4

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

Таким образом, вы строите двигатель, дверь, колесо и т. Д. На экране отображается двигатель = выключен.

Не нужно начинать наблюдение за двигателем и т. Д., Поскольку они все дорогие. Затем, когда вы поворачиваете ключ, чтобы зажечь, вы вызываете Engine-> Start. Запускаются все дорогостоящие процессы.

Теперь вы видите двигатель = вкл. И процесс зажигания начинается.

Автомобиль не включится без наличия двигателя.

Вы можете заменить двигатель с вашим сложным расчетом. Как ячейка Excel. Не все ячейки должны быть активными со всеми обработчиками событий все время. Когда вы сосредоточены на клетке, вы можете запустить ее. Увеличение производительности таким образом.

Люк Франкен
источник