Вот век живи, век учись.
Оказывается в большинстве ООП языков (Java, C#, TypeScript) нет возможности присвоения пропертей до вызова конструтора суперкласса, а вызов виртуального метода из конструтора — плохая практика.
Рассмотрим следующий код:
class AButton {
constructor() {
this.fill();
}
protected fill() {
// предполагается, что он строит кнопку на основе getColor()
// вообще, код этого метода мог бы вполне содержаться и в конструкторе
}
protected abstract getColor(): string;
}
class ButtonWithCustomColor extends AButton {
protected color: string;
constructor(color: string) {
this.color = color; // Получаем ошибку здесь - доступа к свойствам ещё нет
super();
}
protected getColor() { return this.color; }
}
Не выходит аленький цветочек.
Придётся каждый раз вызывать не только конструктор, но и метод fill() извне (а из конструктора его выбросим), чем полностью убиваем смысл сокрытия кишок внутри объекта.
Было:
btn = new Button();
Стало:
btn = new Button();
btn.fill(); // не дай вам бог забыть это вызвать
btn.fillSomethingElse(); // а у нас наверняка и другие кишочки выпрут наружу рано или поздно
Что самое интересное, так это то, что ничего по сути не меняется, кроме нотации.
Ничто не мешает компилятору давать доступ хотя бы к пропертям объекта до вызова конструктора суперкласса.
Кто-нибудь может мне пояснить, какая же best practice для подобных случаев? И почему простое логичное решение заменено плохим нелогичным?
Re: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, sharez, Вы писали:
S>Вот век живи, век учись. S>Оказывается в большинстве ООП языков (Java, C#, TypeScript) нет возможности присвоения пропертей до вызова конструтора суперкласса, а вызов виртуального метода из конструтора — плохая практика.
Ну если она разрешена, то на самом деле не очень и плохая Но надо учитывать особенности.
В Java, C#, а одним из первых в Object Pascal (эппловский) было так, что конструктор предка при вызове виртуального метода фактически вызывает метод потомка (того, кого сказали конструировать). В C++, наоборот, ему доступны только методы своего класса и предков, но потомков — ещё нет. Это сокращает функциональность, но повышает безопасность.
S>Стало:
S>btn = new Button(); S>btn.fill(); // не дай вам бог забыть это вызвать S>btn.fillSomethingElse(); // а у нас наверняка и другие кишочки выпрут наружу рано или поздно
Там, где можно вызвать конструктор в виде явного конструктора, можно вызвать и что-то вроде public static Button make(), которое само вызывает конструктор (возможно, объявленный private, чтобы не показывать незавершённое состояние), вызывает нужные методы и возвращает готовый результат. Такой себе конструктор, не выглядящий синтаксически как конструктор. (В родной библиотеке Java есть множество методов с неинтуитивным названием valueOf(), которые делают что-то похожее.) После этого специфика потрохов будет скрыта только внутри данного класса и его потомков, которые должны будут отразить специфику такого формирования у себя, но не у пользователей.
По-моему, это вполне решение, которое покрывает подавляющее большинство таких ситуаций.
S>Что самое интересное, так это то, что ничего по сути не меняется, кроме нотации. S>Ничто не мешает компилятору давать доступ хотя бы к пропертям объекта до вызова конструктора суперкласса.
S>Кто-нибудь может мне пояснить, какая же best practice для подобных случаев? И почему простое логичное решение заменено плохим нелогичным?
В том-то и дело, что "простого логичного" тут нет вообще. Есть 2-3 крайних варианта, каждый со своими тараканами, и попытка выбора компромисса между ними. Можно как в C++ — начинаем всегда с предка, предок ничего не знает про потомков. Можно как в Java, C# — начинаем всегда с предка, но через виртуальные методы можем получать дозированное знание о потомках, плюс защитные хаки вроде предварительной инициализации полей (см. ссылку от Sharov). Это не позволяет заранее получить согласованное состояние потомка в целом, но позволяет делать чуть улучшений. Можно как в Python — где и как вызывается конструктор предка — чисто на совести программиста, защита есть только на уровне целостности среды исполнения. Есть ещё варианты?
Здравствуйте, netch80, Вы писали:
N>Там, где можно вызвать конструктор в виде явного конструктора, можно вызвать и что-то вроде public static Button make(), которое само вызывает конструктор (возможно, объявленный private, чтобы не показывать незавершённое состояние), вызывает нужные методы и возвращает готовый результат. Такой себе конструктор, не выглядящий синтаксически как конструктор. (В родной библиотеке Java есть множество методов с неинтуитивным названием valueOf(), которые делают что-то похожее.) После этого специфика потрохов будет скрыта только внутри данного класса и его потомков, которые должны будут отразить специфику такого формирования у себя, но не у пользователей.
А по моему это называется Фабрика? Не?
Ну в том смысле что конструктор у нас private, и никак создать объект, кроме как этим фабричным методом не выйдет. А он де факто берет на себя помимо создания еще и всю нужную дополнительную инициализацию...
Здравствуйте, Carc, Вы писали:
C>Здравствуйте, netch80, Вы писали:
N>>Там, где можно вызвать конструктор в виде явного конструктора, можно вызвать и что-то вроде public static Button make(), которое само вызывает конструктор (возможно, объявленный private, чтобы не показывать незавершённое состояние), вызывает нужные методы и возвращает готовый результат. Такой себе конструктор, не выглядящий синтаксически как конструктор. (В родной библиотеке Java есть множество методов с неинтуитивным названием valueOf(), которые делают что-то похожее.) После этого специфика потрохов будет скрыта только внутри данного класса и его потомков, которые должны будут отразить специфику такого формирования у себя, но не у пользователей. C>А по моему это называется Фабрика? Не?
C>Ну в том смысле что конструктор у нас private, и никак создать объект, кроме как этим фабричным методом не выйдет. А он де факто берет на себя помимо создания еще и всю нужную дополнительную инициализацию...
Похоже, что фабрика здесь хороший способ сокрыть реализацию, потому что в случае такой иерархии:
CustomColorButtonWithSomeOtherFeature получил бы два статических метода create(), один из которых ему как козе боян.
Но есть нюанс. Если мы хотим дать программисту (или 3-rd parties) наследоваться от Button, то нам обязательно придется ему вталдычить, чтобы он не забывал вызывать fill() после создания своей кнопки. А это печаль в некотором роде, потому что программистам обязательно захочется отнаследоваться для случая
Button < ToolbarButton
чтобы банально разом для всех кнопок указать высоту и ширину.
Re[2]: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, sharez, Вы писали:
S>Здесь можно почитать со всеми ссылками и т.д.
> When you combine these two facts you are left with the problem that if you make a virtual method call in a constructor, and it is not the most derived type in its inheritance hierarchy, that it will be called on a class whose constructor has not been run, and therefore may not be in a suitable state to have that method called.
Как раз это и есть проблема, что наследник имеет возможность переопределить поведение объекта, но не имеет возможности это поведение настроить (указать переменные/поля/аргументы) до его вызова.
Иными словами в конструкторе есть табу на вызов виртуальных методов, т. к. у нас нет возможности передать им какие-то дополнительные данные, необходимые для наследников (хотя бы через поля).
Я никак не могу взять в толк, зачем вообще компиляторы реализованы так, что не имеют возможности обработать следующую конструкцию:
class Child extends Parent {
protected data: SomeObject;
constructor(data: SomeObject) {
this.data = data; // укажем новую настройку объекта
super(); // и вызовем конструирование
}
// вызовется во время выполения конструктора Parent
public init() {
super.init(data.x); // и старую реализацию скорректировали
useAnotherFeature(data.y); // и новый функционал добавили
}
}
ведь с такой конструкцией вообще не было бы никаких проблем, если бы она поддерживалась, разве нет?
Re[3]: Присвоение свойств и вызов виртуального метода из конструктора
S>Но есть нюанс. Если мы хотим дать программисту (или 3-rd parties) наследоваться от Button, то нам обязательно придется ему вталдычить, чтобы он не забывал вызывать fill() после создания своей кнопки. А это печаль в некотором роде, потому что программистам обязательно захочется отнаследоваться для случая Button < ToolbarButton чтобы банально разом для всех кнопок указать высоту и ширину.
А если попробовать заюзать какой-нить NVI-паттерн?
Т.е. типа сделать в верхнем базовом классе какой-нить ShowButton, который показывает кнопку. Причем сделать его невиртуальным, ибо все равно там простенький функционал в стиле показать\скрыть, и перекрывать его все равно никто не будет, ибо не зачем.
А вот это non virtual ShowButton, будет уже вызывать тот самый виртуальный fill, который запросит атрибуты (цвета там или что там было).
Т.е. нечто вида
//где-то в CButton есть метод виртуальныйvirtual void fill();//или GetColor - чего там было то?
CButton::ShowButton(BOOL bShow)
{
if (bShow)//если показываем кнопку
fill();//то вызываем внутри виртуальный метод, который все сначала запросит
CWidget::ShowButton(bShow);//ну а это какая-то простенькая вещь: показать скрыть кнопку
}
Т.е. таким образом мы явно провоцируем последующих кодеров (разработчиков наследников от CButton), вызывать невиртуальный метод показа кнопки. А вот он уже вызывает нужный виртуальный fill, т.к. фактически объект к моменту вызова ShowButton уже полностью создан, отработали все конструкторы всей иерархии.
Костыль конечно, но по крайней мере явный. Без ShowButton ничего нет в UI (не видно), звать обязательно. А вот оно уже всю неочевидную инициализацию завершит.
Здравствуйте, Carc, Вы писали:
C>А если попробовать заюзать какой-нить NVI-паттерн?
C>Т.е. типа сделать в верхнем базовом классе какой-нить ShowButton, который показывает кнопку. Причем сделать его невиртуальным, ибо все равно там простенький функционал в стиле показать\скрыть, и перекрывать его все равно никто не будет, ибо не зачем.
C>А вот это non virtual ShowButton, будет уже вызывать тот самый виртуальный fill, который запросит атрибуты (цвета там или что там было).
А я примерно такую Lazy Initialization и сделал.
По сути чтобы кнопку добавить в DOM-дерево HTML необходимо выполнить
element.appendChild(button.getElement());
Собственно в getElement() я и вызываю fill(), если этого не было еще сделано.
Однако, как всегда и здесь есть нюанс.
Есть методы setEnabled(bool), setTitle(string) и т. д., которые теперь вместо просто модификации элемента, должны ещё проверять, что он вообще был создан, т. е. как минимум:
public setTitle(title: string) {
if (this.el == null)
this.fill();
this.el.textContent = title;
}
и так во всех, обращающихся к элементу методах.
Можно, конечно, заставить все методы обращаться к this.el по методу this.getElement(), кроме this.fill:
public setTitle(title: string) {
this.getElement().textContent = title;
}
Для этого придется сделать this.el приватным и передавать его в fill() как параметр. Плюс надо не забыть потом при дописывании базового класса, что этот el должен быть использован только через getElement(), хотя возможность такая есть. Вариант с удлинением цепочки наследования (Button < ButtonWithPrivateEl) мы не рассматриваем из-за выходящего из-под контроля over-engineering'а.
В общем, всё это делается, да, но не кажется ли, что мы излишне усложнили архитектуру просто из-за надуманного ограничения на задание полей класса до вызова суперконструтора?
Re: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, sharez, Вы писали:
S>Вот век живи, век учись.
Это баян. S>Кто-нибудь может мне пояснить, какая же best practice для подобных случаев?
Надо делать редизайн иерархии, использовать RAII и применить пару шаблонов проектирования. S>И почему простое логичное решение заменено плохим нелогичным?
Проблема там только в том, что поля не инициализировались. Если тебе очень хочется, то можно передавать всё через параметры виртуальной функции и чётко контролировать инциализацию полей класса. Вопрос в другом, зачем тогда ООП и вся его экосистема классов если приходится эту же систему обходить?
Sic luceat lux!
Re[4]: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, Sharov, Вы писали:
S>Осн. идея в том, что обращение к this до создания родительского объекта не валидно. На мой взгляд, вполне разумно и безопасно.
Лично я вижу здесь разногласия о том, что должен делать конструктор, а должен он:
1) Выделить память (это такой атавизм, из которого и растут корни)
2) Задать свойства
и так как он технически является ещё и функцией, то
3) Инициализировать себя
но этот шаг почему-то встревает между 1 и 2 по необъяснимой причине.
Часто натыкаюсь в интернете, что не стоит пользоваться свойством конструктора-функции, т. е. использовать (1,2) и не использовать (3).
Я же думаю, что выделение памяти это как бы предконструирование объекта, которое вообще должно быть вынесено за пределы контролируемого процесса в ЯВУ, и логически эти пунткы должны группироваться вот так (1)-(2, 3) или (1)-(3, 2), где шаг (1) вообще сокрыт от нас, и вызывается по цепочке отдельно от конструктора.
Мы же в современных ЯВУ имеем тенденцию склеивать совершенно разнородные процессы (1,2) только ради правильного выделения памяти.
Вот как должно быть: когда мы вызываем конктруктор, то он вызывает по цепочке все предконструкторы (без каких-либо параметров) просто для выделения памяти под все поля всех предков, и уже потом оперируем конструктором как собственно виртуальной функцией, вызываем её предков как сами того хотим (или не вызывать вообше, если не хотим).
Практически такое сейчас невозможно, но разве это не тянет на логичне продолжение концепции ООП?
Меня не покидает мысль, что разработчики компиляторов всё-таки чем-то могли руководствоваться, не внедряя такую очевидную вещь. Но сколько не набрасываю в уме различные задачи, так и не натыкаюсь на ответ, чем.
Re[2]: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, Kernan, Вы писали:
K>Здравствуйте, sharez, Вы писали:
S>>Кто-нибудь может мне пояснить, какая же best practice для подобных случаев? K>Надо делать редизайн иерархии, использовать RAII и применить пару шаблонов проектирования.
Это альтернатива простому решению.
K>Проблема там только в том, что поля не инициализировались.
Я выше предлагал решение с разделением конструктора на предконструктор без параметров (безальтернативно неявно вызывающийся по цепочке в самом начале конструктора) и виртаульную функцию (которая может вызывать или не вызывать виртуальные-функции-конструкторы предков в любом порядке).
Но это на уровне языка.
K>Если тебе очень хочется, то можно передавать всё через параметры виртуальной функции и чётко контролировать инциализацию полей класса. Вопрос в другом, зачем тогда ООП и вся его экосистема классов если приходится эту же систему обходить?
А как виратуальной функции что-то передать, если её вызывает конструктор Parent'а, которому даже нельзя передать параметров, о которых знает только Child?
Предусмотреть в нём параметр params: HashMap, чтобы грязным образом через Reflections задавать проброшенные через params поля? Этого действительно необходимо избегать.
Re[5]: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, sharez, Вы писали:
S>Здравствуйте, Sharov, Вы писали:
S>>Осн. идея в том, что обращение к this до создания родительского объекта не валидно. На мой взгляд, вполне разумно и безопасно.
S>Лично я вижу здесь разногласия о том, что должен делать конструктор, а должен он: S>1) Выделить память (это такой атавизм, из которого и растут корни) S>2) Задать свойства S>и так как он технически является ещё и функцией, то S>3) Инициализировать себя S>но этот шаг почему-то встревает между 1 и 2 по необъяснимой причине.
S>Часто натыкаюсь в интернете, что не стоит пользоваться свойством конструктора-функции, т. е. использовать (1,2) и не использовать (3). S>Я же думаю, что выделение памяти это как бы предконструирование объекта, которое вообще должно быть вынесено за пределы контролируемого процесса в ЯВУ, и логически эти пунткы должны группироваться вот так (1)-(2, 3) или (1)-(3, 2), где шаг (1) вообще сокрыт от нас, и вызывается по цепочке отдельно от конструктора. S>Мы же в современных ЯВУ имеем тенденцию склеивать совершенно разнородные процессы (1,2) только ради правильного выделения памяти.
S>Вот как должно быть: когда мы вызываем конктруктор, то он вызывает по цепочке все предконструкторы (без каких-либо параметров) просто для выделения памяти под все поля всех предков, и уже потом оперируем конструктором как собственно виртуальной функцией, вызываем её предков как сами того хотим (или не вызывать вообше, если не хотим).
Вы рассуждаете с той точки зрения, что вызывая n предконструкторов у нас будет n объектов. В реальности child и parent явл. собой один объект (область в памяти). Т.е. конструктор child не заменяет, а расширяет parent конструктор. Без parent конструктора говорить о child смысла нет. Поэтому пляски вокруг базовых конструкторов.
Кодом людям нужно помогать!
Re[6]: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, Sharov, Вы писали:
S>Вы рассуждаете с той точки зрения, что вызывая n предконструкторов у нас будет n объектов. В реальности child и parent явл. собой один объект (область в памяти). Т.е. конструктор child не заменяет, а расширяет parent конструктор. Без parent конструктора говорить о child смысла нет. Поэтому пляски вокруг базовых конструкторов.
Нет, отчего же, конструктор Parent выделяет память под instance.parent_field_x, instance.parent_field_y, потом Child выделяет память под instance.child_field_z.
Вызываем по цепочке предконструкторы и предварительно выделяем память для всего объекта целиком, чтобы Child мог его использовать.
Точно так же, кстати, можно автоматически подчищать память в деструкторе, которые теперь не обязан быть виртуальным (как в С++), а вот постдеструкторы будут виртуальными всегда.
Re: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, sharez, Вы писали:
S>Не выходит аленький цветочек.
А почему проще нельзя?
class AButton {
private color: string;//класс AButton должен знать о цвете (у тебя же был abstract getColor()), так и пусть знает явно.
constructor() {
this.color = DEFAULT_COLOR;
this.fill();
}
constructor(color: string) {
this.color = color;
this.fill();
}
private fill(color: string) {
// предполагается, что он строит кнопку на основе getColor()
// вообще, код этого метода мог бы вполне содержаться и в конструкторе
}
}
class ButtonWithCustomColor extends AButton {
constructor(color: string) {
super(color);
}
}
Здравствуйте, sharez, Вы писали:
S>Здравствуйте, Sharov, Вы писали:
S>>Вы рассуждаете с той точки зрения, что вызывая n предконструкторов у нас будет n объектов. В реальности child и parent явл. собой один объект (область в памяти). Т.е. конструктор child не заменяет, а расширяет parent конструктор. Без parent конструктора говорить о child смысла нет. Поэтому пляски вокруг базовых конструкторов.
S>Нет, отчего же, конструктор Parent выделяет память под instance.parent_field_x, instance.parent_field_y, потом Child выделяет память под instance.child_field_z. S>Вызываем по цепочке предконструкторы и предварительно выделяем память для всего объекта целиком, чтобы Child мог его использовать.
Ну правильно, сначала констр. объекты класса, потом используем поля данного класса. В приведенной выше схеме попытка исп. поля до конструкции объекта. Приведенный код весьма прост, а вот если в общем случае будет вызываться метод, который будет использовать поля Parent, а он(Parent) еще не создан.
Кодом людям нужно помогать!
Re[2]: Присвоение свойств и вызов виртуального метода из конструктора
·>Т.е. вместо того, чтобы впихивать невпихуемое в один класс — раздели на два. ·>Короче, давай конкретную проблему, посмотрим как её можно решить.
И назвать это Pattern Decorator?
Sic luceat lux!
Re[6]: Присвоение свойств и вызов виртуального метода из кон
Здравствуйте, sharez, Вы писали:
S>В общем, всё это делается, да, но не кажется ли, что мы излишне усложнили архитектуру просто из-за надуманного ограничения на задание полей класса до вызова суперконструтора?
Не кажется.
Вне зависимости от возможности делать виртуальный вызов в конструкторе — контрол (кнопка) будет:
1. Иметь состояние attached/detached (to DOM). Особенно в тех фреймворках кто может аттачится на уже существующий элемент и/или достраивать дом с плэйсхолдеров.
2. Свойства на подобии title и color — они или бесполезные поля, или же имеют аксессоры и ими можно пользоваться как до так и после аттача с отражением изменений в DOM. Опционально — вычитка значений из DOM.
3. События — аналогичны (2) — подписываемся/отписываемся в любое время, но в DOM они должны подписываться/отписываться железобетонно. Иначе ликать будет. (Хотя современные браузеры если ещё нет, то скоро и с этим справятся).
Я вообще не вижу зачем тут метод fill особенно в конструкторе.
Конструктор должен создавать объект в консистентном состоянии и только. Таким образом все параметры необходимые для конструкции объекта — необходимо передавать через параметры конструктора (парадокс?).
Для того что бы не вызывать тонну пропертей после конструкции объекта — можно соорудить ControlParams, ButtonParams или вообще безликий/динамический объект-холдер с настройками при инициализации. Заодно это могло бы упростить создание контрола фабрикой по уже готовому DOM с параметрами (или частью) которые лежат в атрибутах элемента.
Собственно нечто подобное я "изобретал" для себя, достаточно неплохо имхо организовано в google closure tools (library).
Re[3]: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, Kernan, Вы писали:
K>·>Т.е. вместо того, чтобы впихивать невпихуемое в один класс — раздели на два. K>·>Короче, давай конкретную проблему, посмотрим как её можно решить. K>И назвать это Pattern Decorator?
Да хоть горшком назови... Но зачем пытаться использовать языковые средства не по назначению?!
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[8]: Присвоение свойств и вызов виртуального метода из конструктора
Здравствуйте, Sharov, Вы писали:
S>Ну правильно, сначала констр. объекты класса, потом используем поля данного класса. В приведенной выше схеме попытка исп. поля до конструкции объекта. Приведенный код весьма прост, а вот если в общем случае будет вызываться метод, который будет использовать поля Parent, а он(Parent) еще не создан.
В схеме с предконструкторами такого быть не может. Любой метод (равно как и само тело метода-конструктора) выполнятся только после выполнения всех предконструкторов. Можно дать возможность переопределять их (но из них вызовы this.method() делать категорически нельзя, только this.property). Заполнение полей дефолтными значениями (включая инициализацию ссылок на поля-объекты таких как protected prop = new ArrayList()) была бы как раз их задача.