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

12

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

пример (в моем ржавом C ++)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

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

Редактировать: я говорю в целом, но мы кодируем на Python.

Стефано Борини
источник
Это общее правило или специфическое для конкретных языков?
ChrisF
Какой язык? В C ++ это больше, чем просто шаблон: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers
@ Lenny222, ОП говорит о «методах класса», что, по крайней мере для меня, означает методы, не относящиеся к экземпляру . Который поэтому не может быть виртуальным.
Петер Тёрёк
3
@Alb В Java это совершенно нормально. Чего не следует делать, так это явно передавать thisлюбой из методов, которые вы вызываете из конструктора.
Бизиклоп
3
@Stefano Borini: Если вы пишете код на Python, почему бы не показать пример на Python вместо ржавого C ++? Также, пожалуйста, объясните, почему это плохо. Мы делаем это все время.
S.Lott

Ответы:

26

Вы не указали язык.

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

Конструктор может вызывать не виртуальные функции.

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

C #, кажется, справляется с ситуацией так, как вы ожидаете: вы можете вызывать виртуальные методы в конструкторах, и это вызывает самую финальную версию. Так что в C # не анти-паттерн.

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

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

У Java и C # нет деструкторов, у них есть финализаторы. Я не знаю поведение с Java.

C #, кажется, справляется с очисткой правильно в этом отношении.

(Обратите внимание, что, хотя Java и C # имеют сборку мусора, она управляет только распределением памяти. Есть другая очистка, которую должен сделать ваш деструктор, которая не освобождает память).

Дойная корова
источник
13
Здесь есть несколько небольших ошибок. Методы в C # не являются виртуальными по умолчанию. C # имеет другую семантику, чем C ++, при вызове виртуального метода в конструкторе; будет вызываться виртуальный метод для самого производного типа, а не виртуальный метод для той части типа, которая в настоящее время создается. C # называет свои методы финализации «деструкторами», но вы правы, что они имеют семантику финализаторов. Виртуальные методы, вызываемые в деструкторах C #, работают так же, как и в конструкторах; самый производный метод называется.
Эрик Липперт
@ Петер: Я намеревался использовать методы экземпляра. извините за путаницу.
Стефано Борини
1
@ Эрик Липперт. Спасибо за ваш опыт в C #, я отредактировал свой ответ соответственно. Я не владею этим языком, я очень хорошо знаю C ++ и менее хорошо знаю Java.
CashCow
5
Пожалуйста. Обратите внимание, что вызов виртуального метода в конструкторе базового класса в C # все еще довольно плохая идея.
Эрик Липперт
Если вы вызываете (виртуальный) метод в Java из конструктора, он всегда вызывает наиболее производное переопределение. Однако то, что вы называете «так, как вы ожидаете», это то, что я бы назвал сбивающим с толку. Поскольку Java вызывает наиболее производное переопределение, этот метод будет видеть только обработанные полевые инициализаторы, но не конструктор своего собственного запуска класса. Вызов метода для класса, у которого еще не установлен инвариант, может быть опасным. Поэтому я думаю, что C ++ сделал лучший выбор здесь.
5gon12eder
18

Хорошо, теперь , что путаница в отношении методов класса против методов экземпляра прояснилась, я могу дать ответ :-)

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

C ++ и C # уже обсуждались другими. В Java будет вызываться виртуальный метод самого производного типа, однако этот тип еще не инициализирован. Таким образом, если этот метод использует какие-либо поля из производного типа, эти поля могут еще не инициализироваться должным образом в данный момент времени. Эта проблема подробно обсуждается в Effecive Java 2nd Edition , Item 17: Разработка и документирование для наследования, или же запретить ее .

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

Петер Тёрёк
источник
3
(+1) «находясь внутри конструктора, объект еще не полностью построен». То же, что "методы класса против экземпляра". Некоторые языки программирования считают, что он создается при входе в конструктор, как если бы программист присваивал значения конструктору.
umlcat
7

Я бы не стал считать вызовы методов самим антипаттерном, скорее запахом кода. Если класс предоставляет resetметод, который возвращает объект в его первоначальное состояние, то вызов reset()в конструкторе - DRY. (Я не делаю никаких заявлений о методах сброса).

Вот статья, которая может помочь удовлетворить вашу апелляцию: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

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

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

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

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

Пол Бучер
источник
3

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

Технически, в этом нет ничего плохого, в C ++, особенно в Python, вам нужно быть осторожным.

Практически, вы должны ограничивать вызовы только такими методами, которые инициализируют членов класса.


источник
2

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

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

Мейсон Уилер
источник
2

Есть две проблемы с вызовом метода:

  • вызов виртуального метода, который может сделать что-то неожиданное (C ++) или использовать части объектов, которые еще не были инициализированы
  • вызов открытого метода (который должен обеспечивать выполнение инвариантов класса), поскольку объект еще не обязательно завершен (и, следовательно, его инвариант может не сохраняться)

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

Матье М.
источник
1

Я не покупаю это. В объектно-ориентированной системе вызов метода - это почти единственное, что вы можете сделать. Фактически, это более или менее определение «объектно-ориентированного». Итак, если конструктор не может вызывать какие-либо методы, то что он может делать?

Йорг Миттаг
источник
Инициализируйте объект.
Стефано Борини
@ Стефано Борини: Как? В объектно-ориентированной системе единственное, что вы можете сделать, это вызвать методы. Или посмотреть на это с противоположной стороны: все делается путем вызова методов. И «все», очевидно, включает в себя инициализацию объекта. Итак, если для инициализации объекта вам нужно вызывать методы, но конструкторы не могут вызывать методы, то как конструктор может инициализировать объект?
Йорг Миттаг
это абсолютно не правда, что единственное, что вы можете сделать, это вызвать методы. Вы можете просто инициализировать состояние без какого-либо вызова, напрямую к внутренностям вашего объекта ... Смысл конструктора в том, чтобы привести объект в согласованное состояние. Если вы вызываете другие методы, у них могут возникнуть проблемы с обработкой объекта в частичном состоянии, если только они не являются методами, специально созданными для вызова из конструктора (как правило, в качестве вспомогательных методов)
Стефано Борини
@ Стефано Борини: «Вы можете просто инициализировать состояние без какого-либо вызова, напрямую к внутренностям вашего объекта». К сожалению, когда это связано с методом, что вы делаете? Скопировать и передать код?
S.Lott
1
@ S.Lott: нет, я называю это, но я пытаюсь сохранить его модульной функцией вместо метода объекта и заставить его предоставлять возвращаемые данные, которые я могу поместить в состояние объекта в конструкторе. Если мне действительно нужен объектный метод, я сделаю его закрытым и поясню, что он предназначен для инициализации, например, присвоения ему правильного имени. Однако я бы никогда не вызвал публичный метод для установки статуса объекта из конструктора.
Стефано Борини
0

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

В C ++ и Delphi. Если бы мне пришлось задавать начальные значения для некоторых свойств («членов поля»), а код очень расширен, я добавляю несколько вторичных методов в качестве расширения конструкторов.

И не вызывайте другие методы, которые делают более сложные вещи.

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

В конструкторе я присваиваю значения «по умолчанию» полям состояния свойств, БЕЗ вызова «аксессоров».

umlcat
источник