насколько сложным должен быть конструктор

18

Я веду дискуссию с моим коллегой о том, сколько работы может сделать конструктор. У меня есть класс B, который внутренне требует другого объекта A. Объект A - один из немногих членов, которым класс B должен выполнять свою работу. Все его открытые методы зависят от внутреннего объекта A. Информация об объекте A хранится в БД, поэтому я пытаюсь проверить и получить ее, просмотрев ее в БД в конструкторе. Мой коллега отметил, что конструктор не должен выполнять большую работу, кроме как записывать параметры конструктора. Так как все публичные методы в любом случае потерпят неудачу, если объект A не будет найден с использованием входных данных для конструктора, я утверждал, что вместо того, чтобы разрешить создание экземпляра и последующий сбой, на самом деле лучше бросить конструктор раньше.

Что думают другие? Я использую C #, если это имеет какое-либо значение.

Чтение Есть ли когда-нибудь причина выполнять всю работу с объектом в конструкторе? мне интересно, что выбор объекта A путем перехода в DB является частью «любой другой инициализации, необходимой для того, чтобы сделать объект готовым к использованию», потому что, если пользователь передал неверные значения конструктору, я не смог бы использовать ни один из его открытых методов.

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

Таин Ким
источник
8
Я не согласен. Взгляните на принцип инверсии зависимостей, было бы лучше, если бы объект A передавался объекту B в допустимом состоянии в его конструкторе.
Джимми Хоффа
5
Вы спрашиваете, сколько можно сделать? Сколько нужно сделать? Или сколько из вашей конкретной ситуации должно быть сделано? Это три совершенно разных вопроса.
Бобсон
1
Я согласен с предложением Джимми Хоффа взглянуть на внедрение зависимости (или «инверсию зависимости»). Это была моя первая мысль при прочтении описания, потому что, похоже, вы уже на полпути; у вас уже есть функциональность, выделенная в класс A, который использует класс B. Я не могу говорить о вашем случае, но я обычно нахожу, что рефакторинг кода для использования шаблона внедрения зависимостей делает классы проще / менее переплетенными.
Ученик доктора Вилли
1
Бобсон, я изменил название на «должен». Я прошу о лучшей практике здесь.
Таин Ким
1
@JimmyHoffa: может быть, вы должны расширить свой комментарий в ответ.
Кевин Клайн

Ответы:

22

Ваш вопрос состоит из двух совершенно разных частей:

Должен ли я выбросить исключение из конструктора, или у меня должен быть метод сбой?

Это явно применение принципа Fail-fast . Сделать сбой конструктора гораздо легче, чем найти причину сбоя метода. Например, вы можете получить экземпляр, уже созданный из какой-то другой части кода, и вы получите ошибки при вызове методов. Очевидно ли, что объект создан неправильно? Нет .

Что касается проблемы «обернуть вызов в try / catch». Исключения должны быть исключительными . Если вы знаете, что какой-то код вызовет исключение, вы не заключаете код в try / catch, но проверяете параметры этого кода перед выполнением кода, который может вызвать исключение. Исключения предназначены только для того, чтобы система не попала в недопустимое состояние. Если вы знаете, что некоторые входные параметры могут привести к неверному состоянию, убедитесь, что эти параметры никогда не встречаются. Таким образом, вы должны делать try / catch только в тех местах, где вы можете логически обработать исключение, которое обычно находится на границе системы.

Могу ли я получить доступ к «другим частям системы, например БД» из конструктора.

Я думаю, что это идет вразрез с принципом наименьшего удивления . Не многие ожидают, что конструктор получит доступ к БД. Так что нет, вы не должны этого делать.

Euphoric
источник
2
Более подходящим решением для этого обычно является добавление метода «открытого» типа, где конструирование является свободным, но до «открытия» / «подключения» / и т. Д. Ничего не может быть использовано, где происходит вся фактическая инициализация. Сбой конструкторов может вызвать всевозможные проблемы, когда в будущем вы захотите выполнить частичное построение объектов, что является полезной способностью.
Джимми Хоффа
@JimmyHoffa Я не вижу разницы между методом конструктора и конкретным методом конструирования.
Euphoric
4
Вы не можете, но многие делают. Подумайте, когда вы создаете объект подключения, конструктор подключает его? Можно ли использовать объект, если он не подключен? В обоих вопросах ответ отрицательный, потому что полезно иметь объекты соединения частично сконструированными, чтобы вы могли настроить параметры и все, прежде чем открывать соединение. Это общий подход к этому сценарию. Я все еще думаю, что внедрение в конструктор является правильным подходом для сценариев, когда объект A имеет ресурсную зависимость, но открытый метод все же лучше, чем когда конструктор выполняет опасную работу.
Джимми Хоффа
@JimmyHoffa Но это специфическое требование класса соединения, а не общее правило дизайна ООП. Я бы назвал это исключительным случаем, чем правилом. Вы в основном говорите о модели строителя.
Euphoric
2
Это не требование, это было дизайнерское решение. Они могли бы заставить конструктор взять строку подключения и сразу подключиться при создании. Они решили не слишком, потому что полезно иметь возможность создать объект, затем настроить схему соединения или другие биты и всплывающие подсказки и подключить ее позже ,
Джимми Хоффа,
19

Тьфу.

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

В этом есть две части: в конструкторе и с помощью конструктора. Это неудобно в конструкторе, потому что вы не можете сделать там много, когда возникает исключение. Вы не можете вернуть другой тип. Вы не можете вернуть ноль. Вы можете перебросить исключение, вернуть сломанный объект (плохой конструктор) или (в лучшем случае) заменить некоторую внутреннюю часть нормальным значением по умолчанию. Использовать конструктор неудобно, потому что тогда ваши экземпляры (и экземпляры всех производных типов!) Могут быть сгенерированы. Тогда вы не можете использовать их в инициализаторах членов. Вы в конечном итоге используете исключения для логики, чтобы увидеть «удалось ли моему созданию?», За исключением читабельности (и хрупкости), вызванной попыткой / отловом везде.

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

Telastyn
источник
2
Сложные сценарии строительства - это именно то, почему существуют такие шаблоны создания, как вы упомянули здесь. Это хороший подход к этим проблемным конструкциям. Мне дано подумать о том, как в .NET Connectionобъект может CreateCommandдля вас, чтобы вы знали, что команда правильно подключена.
Джимми Хоффа
2
К этому я бы добавил, что база данных - это сильная внешняя зависимость. Предположим, вы хотите переключить базы данных в какой-то момент в будущем (или смоделировать объект для тестирования), перенести этот тип кода в класс Factory (или что-то вроде IService). провайдер) кажется более чистым подходом
Джейсон Сперске
4
Можете ли вы уточнить, что "поведение исключений в конструкторах неудобно"? Если бы вы использовали Create, вам не нужно было бы оборачивать его в try catch так же, как вы бы обернули вызов конструктора? Я чувствую, что Create - это просто другое имя для конструктора. Может быть, я не правильно понимаю ваш ответ?
Таин Ким
1
Пришлось попытаться отловить исключения, всплывающие из конструкторов, +1 к аргументам против этого. Работая с людьми, которые говорят «Нет работы в конструкторе», это также может создать некоторые странные шаблоны. Я видел код, где каждый объект имеет не только конструктор, но и некоторую форму Initialize (), где, угадайте, что? Объект не действительно действителен, пока не будет вызван. И тогда у вас есть две вещи для вызова: ctor и Initialize (). Проблема выполнения работы по настройке объекта не исчезает - C # может дать другие решения, чем, скажем, C ++. Фабрики хороши!
J Trana
1
@jtrana - да, инициализация не годится. Конструктор инициализирует некоторый нормальный стандарт или фабрику, или метод create создает его и возвращает пригодный для использования экземпляр.
Теластин
1

Конструктор - баланс сложности метода - действительно вопрос обсуждения хорошего стиля. Довольно часто используется пустой конструктор Class() {}с методом Init(..) {..}для более сложных вещей. Среди аргументов, которые необходимо принять во внимание:

  • Возможность сериализации класса
  • С помощью метода отделения Init от конструктора вам часто приходится повторять одну и ту же последовательность Class x = new Class(); x.Init(..);много раз, частично в модульных тестах. Не так хорошо, однако, кристально чистый для чтения. Иногда я просто помещаю их в одну строку кода.
  • Когда целью Unit Test является просто тест инициализации класса, лучше иметь собственный Init, чтобы выполнять более сложные действия, выполняемые один за другим с промежуточными утверждениями.
Павел Храпкин
источник
Преимущество, на мой взгляд, initметода в том, что обработка ошибок намного проще. В частности, возвращаемое значение initметода может указывать, успешна ли инициализация, и метод может гарантировать, что объект всегда находится в хорошем состоянии.
pqnet