Депрессия - "частично" побеждена.
Много ошибок найдено.
В частности - СПАСИБО NameRec'у - http://programmingmindstream.blogspot.ru/2014/09/blog-post_23.html?showComment=1411514362019#c1699121675114974966
Нагрузочные тесты на основе GUI-тестов - написаны.
Эмулируют работу реальных пользователей в распределённой системе.
С нескольких машин.
Примерно так:
1. Случайным образом открывают документ (из заранее определённого списка).
2. Вводят "два три слова".
3. Сохраняют.
4. Закрывают документ.
Вот кстати код теста:
Молотят уже третий день.
Намолотили порядка 9 Гб.
Оставил на выходные. Посмотрим, что будет.
По результатам - напишу глубокий анализ.
Одно пока могу сказать - даже если функция ReadFile "считала что-то" и вернула ReadSize = SizeToRead - "это не повод успокаиваться". Даже если "оно считало" то "что нужно". И даже если считанные результаты совпадают с данными из файла.
Надо проверять Result функции ReadFile, который BOOL. Ну и GetLastError.
Например оно может вернуть LockViolation или NetworkBusy.
Банально. Да.
Но я про это "ещё потом напишу".
А теперь я хотел написать про конструкторы.
По мотивам:
Ссылка. Получение ресурса есть инициализация (RAII). И "немного от себя"
Коротко. И ещё о фабриках
Коротко. Ещё немного "рассуждений о RAII"
Коротко. Ещё о фабриках
Коротко. О фабриках
Собственная реализация IUnknown и подсчёт ссылок. И примеси
Почему всегда нужно использовать FreeAndNil вместо Free - это надо перечитать особенно и внимательно потому, что моя мысль проистекает из "такой же парадигмы". Виртуальность. И классы-потомки.
Коротко. "Нелюбителям" FreeAndNil
Сегодня получил неожиданное пятикратное подтверждение тому, почему надо писать FreeAndNil, а не Free
Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе
Виртуальные функции в конструкторе и деструкторе
У gunsmoker'а написано про деструкторы, а я хочу написать про конструкторы.
Теперь собственно то, что я хотел написать о конструкторах:
Обычно конструкторы пишутся так:
Этот "стиль" нам "завещал ещё Борланд"... Почивший в бозе.
И все мы к нему "привыкли".
Что в нём не так?
Вообще говоря "правильнее" написать так:
Что мы тут сделали?
Мы ПОМЕНЯЛИ местами инициализацию "агрегированных объектов" и "вызов унаследованных конструкторов".
Это - ВАЖНО.
Почему?
Ключевое слово - виртуальность.
Что я имею в виду?
Давайте напишем "пока" так:
Как быть?
Теперь напишем так:
Проблема исчезла.
Мысль понятна?
Сразу оговорюсь - "не говорите мне про C++ и другие языки". Там "по-другому" устроено.
Процитирую:
"В этом нет никакого секрета, а просто есть правило: виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.
Правило надо заучивать, что неудобно. Проще понять принцип. А принцип тут в краеугольном камне реализации наследования в C++: при создании объекта конструкторы в иерархии вызываются от базового класса к самому последнему унаследованному. Для деструкторов все наоборот.
Что получается: конструктор класса всегда работает в предположении, что его дочерние классы еще не созданы, поэтому он не имеет права вызывать функции, определенные в них. И для виртуальной функций ему ничего не остается, как только вызвать то, что определено в нем самом. Получается, что механизм виртуальных функций тут как-бы не работает. А он тут действительно не работает, так как таблица виртуальных функций дочернего класса еще не перекрыла текущую таблицу.
В деструкторе все наоборот. Деструктор знает, что во время его вызова все дочерние классы уже разрушены и вызывать у них ничего уже нельзя, поэтому он замещает адрес таблицы виртуальных функций на адрес своей собственной таблицы и благополучно вызывает версию виртуальной функции, определенной в нем самом.
Итак, виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора."
Источник цитаты - Виртуальные функции в конструкторе и деструкторе
Это если про C++ и "другие языки".
НО!
Не про Delphi.
Теперь "пару слов" - на тему "откуда пошёл такой стиль Борладна".
А "пошло всё" из Turbo Pascal и Turbo Vision.
Там были конструкторы Init. И базовый объект TObject.
Но там была совсем ДРУГАЯ ПАРАДИГМА. Повторю - ПАРАДИГМА. И я не зря Caps Lock использовал.
Ещё раз повторю - ДРУГАЯ ПАРАДИГМА.
Что там было?
А вот что:
InstanceSize - конечно "выглядел иначе".
Но думаю, что "смысл понятен".
А теперь что будет с объектом наследником?
Напишем так:
А в "другой парадигме"?
Мысль понятна?
И "многие люди" до сих пор держат "в уме" эту ПАРАДИГМУ. Но при этом кстати почему-то "забывают" вызывать конструктор от TObject.
Я такое видел НЕ В ОДНОЙ сторонней библиотеке.
Например в том же ImageEn.
"Тема" конструкторов/деструкторов, фабрик и "RAII" - ещё совсем не закрыта. Она только открыта.
Если будет интерес - я напишу далее.
Но "пока" - вот "и всё что я хотел сказать о конструкторах".
Ну и "откуда всё взялось" - Коротко. О возбуждении исключений
P.S. Это всё конечно - "из-за кривой архитектуры", но это - "повод для отдельного разговора".
P.P.S. Собственно - "мысль поста" - была - БАНАЛЬНА - либо "использование фабрик", либо "слом парадигмы".
Хотя "для некоторых" и "использование фабрик" это - "слом парадигмы". К сожалению.
Много ошибок найдено.
В частности - СПАСИБО NameRec'у - http://programmingmindstream.blogspot.ru/2014/09/blog-post_23.html?showComment=1411514362019#c1699121675114974966
Нагрузочные тесты на основе GUI-тестов - написаны.
Эмулируют работу реальных пользователей в распределённой системе.
С нескольких машин.
Примерно так:
1. Случайным образом открывают документ (из заранее определённого списка).
2. Вводят "два три слова".
3. Сохраняют.
4. Закрывают документ.
Вот кстати код теста:
USES QuickTestsUtils.script ; Тест TK565491886 ARRAY VAR "Список документов" [ Конституция ГК НК ТК ТНВЭД ОКОФ ОКОНХ ] >>> "Список документов" BOOLEAN VAR Вечность ДА >>> Вечность ПОКА Вечность ( INTEGER VAR "Случайный документ" ( "Список документов" array:Count Random "Список документов" [i] ) >>> "Случайный документ" Параметры: ( "Документ из базы {("Случайный документ")}" ) Выполнить ( "Набить текст {('Вносим изменения в текст и сохраняем документ!!!')}" "Нажать Enter" // - чтобы отделить параграфы документа, иначе параграфы могут быть "слишком длинными" для восприятия "Сохранить документ" "Обработать сообщения" // - чтобы приложение перерисовывалось ) ) ; TK565491886
Молотят уже третий день.
Намолотили порядка 9 Гб.
Оставил на выходные. Посмотрим, что будет.
По результатам - напишу глубокий анализ.
Одно пока могу сказать - даже если функция ReadFile "считала что-то" и вернула ReadSize = SizeToRead - "это не повод успокаиваться". Даже если "оно считало" то "что нужно". И даже если считанные результаты совпадают с данными из файла.
Надо проверять Result функции ReadFile, который BOOL. Ну и GetLastError.
Например оно может вернуть LockViolation или NetworkBusy.
Банально. Да.
Но я про это "ещё потом напишу".
А теперь я хотел написать про конструкторы.
По мотивам:
Ссылка. Получение ресурса есть инициализация (RAII). И "немного от себя"
Коротко. И ещё о фабриках
Коротко. Ещё немного "рассуждений о RAII"
Коротко. Ещё о фабриках
Коротко. О фабриках
Собственная реализация IUnknown и подсчёт ссылок. И примеси
Почему всегда нужно использовать FreeAndNil вместо Free - это надо перечитать особенно и внимательно потому, что моя мысль проистекает из "такой же парадигмы". Виртуальность. И классы-потомки.
Коротко. "Нелюбителям" FreeAndNil
Сегодня получил неожиданное пятикратное подтверждение тому, почему надо писать FreeAndNil, а не Free
Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе
Виртуальные функции в конструкторе и деструкторе
У gunsmoker'а написано про деструкторы, а я хочу написать про конструкторы.
Теперь собственно то, что я хотел написать о конструкторах:
Обычно конструкторы пишутся так:
type TObject1 = class end;//TObject1 TObject2 = class end;//TObject2 TA = class private f_SomeObject1 : TObject1; public constructor Create; end;//TA TB = class(TA) private f_SomeObject2 : TObject2; public constructor Create; end;//TB ... constructor TA.Create; begin inherited Create; f_SomeObject1 := TObject1.Create; end; ... constructor TB.Create; begin inherited Create; f_SomeObject2 := TObject2.Create; end;
Этот "стиль" нам "завещал ещё Борланд"... Почивший в бозе.
И все мы к нему "привыкли".
Что в нём не так?
Вообще говоря "правильнее" написать так:
type TObject1 = class end;//TObject1 TObject2 = class end;//TObject2 TA = class private f_SomeObject1 : TObject1; public constructor Create; end;//TA TB = class(TA) private f_SomeObject2 : TObject2; public constructor Create; end;//TB ... constructor TA.Create; begin f_SomeObject1 := TObject1.Create; inherited Create; end; ... constructor TB.Create; begin f_SomeObject2 := TObject2.Create; inherited Create; end;
Что мы тут сделали?
Мы ПОМЕНЯЛИ местами инициализацию "агрегированных объектов" и "вызов унаследованных конструкторов".
Это - ВАЖНО.
Почему?
Ключевое слово - виртуальность.
Что я имею в виду?
Давайте напишем "пока" так:
type TObject1 = class public SomeField : SomeDataType; end;//TObject1 TObject2 = class public SomeField : SomeDataType; end;//TObject2 TA = class private f_SomeObject1 : TObject1; f_SomeData : Integer; protected function CalcSomeData: Integer; virtual; public constructor Create; end;//TA TB = class(TA) private f_SomeObject2 : TObject2; protected function CalcSomeData: Integer; override; public constructor Create; end;//TB ... constructor TA.Create; begin inherited Create; f_SomeObject1 := TObject1.Create; f_SomeData := CalcSomeData; end; function TA.CalcSomeData: Integer; begin Result := SomeAlgorythm1(f_SomeObject1.SomeField); // - тут всё хорошо end; ... constructor TB.Create; begin inherited Create; f_SomeObject2 := TObject2.Create; end; function TB.CalcSomeData: Integer; begin Result := SomeAlgorythm2(f_SomeObject2.SomeField); // - тут получаем AV, потому, что при вызове CalcSomeData из конструктора TA.Create - поле f_SomeObject2 - не инициализировано end;
Как быть?
Теперь напишем так:
type TObject1 = class public SomeField : SomeDataType; end;//TObject1 TObject2 = class public SomeField : SomeDataType; end;//TObject2 TA = class private f_SomeObject1 : TObject1; f_SomeData : Integer; protected function CalcSomeData: Integer; virtual; public constructor Create; end;//TA TB = class(TA) private f_SomeObject2 : TObject2; protected function CalcSomeData: Integer; override; public constructor Create; end;//TB ... constructor TA.Create; begin f_SomeObject1 := TObject1.Create; inherited Create; f_SomeData := CalcSomeData; end; function TA.CalcSomeData: Integer; begin Result := SomeAlgorythm1(f_SomeObject1.SomeField); // - тут всё хорошо end; ... constructor TB.Create; begin f_SomeObject2 := TObject2.Create; inherited Create; end; function TB.CalcSomeData: Integer; begin Result := SomeAlgorythm2(f_SomeObject2.SomeField); // - тут всё хорошо, потому, что поле f_SomeObject2 инициализровано, потому, что конструктор TA зовётся ПОЗЖЕ end;
Проблема исчезла.
Мысль понятна?
Сразу оговорюсь - "не говорите мне про C++ и другие языки". Там "по-другому" устроено.
Процитирую:
"В этом нет никакого секрета, а просто есть правило: виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.
Правило надо заучивать, что неудобно. Проще понять принцип. А принцип тут в краеугольном камне реализации наследования в C++: при создании объекта конструкторы в иерархии вызываются от базового класса к самому последнему унаследованному. Для деструкторов все наоборот.
Что получается: конструктор класса всегда работает в предположении, что его дочерние классы еще не созданы, поэтому он не имеет права вызывать функции, определенные в них. И для виртуальной функций ему ничего не остается, как только вызвать то, что определено в нем самом. Получается, что механизм виртуальных функций тут как-бы не работает. А он тут действительно не работает, так как таблица виртуальных функций дочернего класса еще не перекрыла текущую таблицу.
В деструкторе все наоборот. Деструктор знает, что во время его вызова все дочерние классы уже разрушены и вызывать у них ничего уже нельзя, поэтому он замещает адрес таблицы виртуальных функций на адрес своей собственной таблицы и благополучно вызывает версию виртуальной функции, определенной в нем самом.
Итак, виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора."
Источник цитаты - Виртуальные функции в конструкторе и деструкторе
Это если про C++ и "другие языки".
НО!
Не про Delphi.
Теперь "пару слов" - на тему "откуда пошёл такой стиль Борладна".
А "пошло всё" из Turbo Pascal и Turbo Vision.
Там были конструкторы Init. И базовый объект TObject.
Но там была совсем ДРУГАЯ ПАРАДИГМА. Повторю - ПАРАДИГМА. И я не зря Caps Lock использовал.
Ещё раз повторю - ДРУГАЯ ПАРАДИГМА.
Что там было?
А вот что:
constructor TObject.Init; begin FillChar(@Self, InstanceSize, 0); end;
InstanceSize - конечно "выглядел иначе".
Но думаю, что "смысл понятен".
А теперь что будет с объектом наследником?
Напишем так:
type TA = object(TObject) public SomeField : Integer; public constructor Init; end;//TA ... constructor TA.Init; begin TObject.Init; SomeField := 123; end; ... var A : TA; begin A := TA.Init; WriteLn(A.SomeField); // - Тут получаем 123 end;
А в "другой парадигме"?
type TA = object(TObject) public SomeField : Integer; public constructor Init; end;//TA ... constructor TA.Init; begin SomeField := 123; TObject.Init; end; ... var A : TA; begin A := TA.Init; WriteLn(A.SomeField); // - Тут получаем 0 end;
Мысль понятна?
И "многие люди" до сих пор держат "в уме" эту ПАРАДИГМУ. Но при этом кстати почему-то "забывают" вызывать конструктор от TObject.
Я такое видел НЕ В ОДНОЙ сторонней библиотеке.
Например в том же ImageEn.
"Тема" конструкторов/деструкторов, фабрик и "RAII" - ещё совсем не закрыта. Она только открыта.
Если будет интерес - я напишу далее.
Но "пока" - вот "и всё что я хотел сказать о конструкторах".
Ну и "откуда всё взялось" - Коротко. О возбуждении исключений
P.S. Это всё конечно - "из-за кривой архитектуры", но это - "повод для отдельного разговора".
P.P.S. Собственно - "мысль поста" - была - БАНАЛЬНА - либо "использование фабрик", либо "слом парадигмы".
Хотя "для некоторых" и "использование фабрик" это - "слом парадигмы". К сожалению.
Начну немного издалека.
ОтветитьУдалитьПротокол работы с объектом в Delphi состоит из трёх этапов:
1. Инициализация (конструктор)
2. Использование (обращение к остальным свойствам и методам)
3. Финализация (обычно Free, поскольку деструктор напрямую лучше не вызывать).
Пункты 1 и 3 протокола являются специальными в том смысле, что процессе их выполнения объект находится, вообще говоря, в некорректном состоянии, поскольку инварианты объекта могут выполняться, и это воспринимается чаще всего без удивления: действительно, чего ещё можно ожидать от не до конца созданного или от частично разрушенного объекта?
Теперь рассмотрим виртуальный метод, который вызывается в процессе инициализации или финализации объекта — я думаю это уместно, поскольку в отношении инвариантов ситуации совершенно идентичны.
Очевидно, что такой виртуальный метод должен быть рассчитан на то, что инварианты не выполняются, иными словами, такой виртуальный метод должен учитывать, что в момент его выполнения объект будет находиться в некорректном состоянии. Соответственно, при перекрытии такого виртуального метода в потомке, означенное обстоятельство также следует учитывать.
Поскольку у нас появляются дополнительные соглашения, они могут быть (следовательно — будут) совершенно не очевидны тому разработчику, который будет такое перекрытие выполнять. Он «всего лишь» перекрывает виртуальный метод, а тут оказывается, что это «нельзя просто так взять и...» (c) – нужно учитывать, что объект может быть в некорректном состоянии. Об этом я говорю здесь, правда, в контексте деструкторов.
Это даёт основание говорить об усложнении решения.
Теперь по-существу.
«Давайте напишем "пока" так:
…
Как быть?»
– Как минимум, я вижу два варианта:
1. Не делать так. Т.е. не вызывать виртуальные методы в конструкторе, т. е. в условиях, когда выполнение инвариантов не гарантируется.
Вызов виртуальных методов перенести в п. 2 протокола работы с объектом, например, выделив в нём этап настройки.
Если нас смущает необходимость выполнения этой настройки — можно использовать методы класса по конструированию его экземпляров или прочие разновидности фабричного метода.
IMHO – это предпочтительный вариант.
2. Вызывать виртуальные методы в перекрытии виртуального AfterConstruction.
Действительно, в этом случае этап инициализации пройден и инварианты обязаны уже выполняться. Объект находится в корректном состоянии и вызов другого виртуального метода в таких условиях не потребует учёта артефактов.
В сущности, AfterConstruction это уже этап использования, но совмещённый с окончанием инициализации.
Что мне в этом нравится меньше? - По двум причинам:
а) То, что это в меньшей степени соответствует принципу наименьшего удивления. Об AfterConstruction мало кто знает, поэтому для «не продвинутого» разработчика передача управления может выглядеть как «магия».
б) Код в AfterConstruction может быть использован для инициализации, вследствие чего возникнет очередной виток вопросов относительно того, где именно расположить вызов требуемого виртуального метода.
«Пункты 1 и 3 протокола являются специальными в том смысле, что процессе их выполнения объект находится, вообще говоря, в некорректном состоянии, поскольку инварианты объекта могут выполняться»
Удалить-- Думаю понятно, что в выделенном фрагменте имелось ввиду "не выполняться".
Я - понял. Но если "вы не поняли", то я как раз - "за фабрики".
УдалитьА про конструкторы написал - уже "от отчаянья".
«Я - понял. Но если "вы не поняли", то я как раз - "за фабрики".»
Удалить-- Александр, ну какая разница за что Вы и против чего? :-)
Я оппонирую тому, что Вы написали.
Вроде бы понятно изложил, почему нахожу использование виртуальных методов в конструкторах и деструкторах опасным. Почему считаю не лучшей идеей отказ от сложившейся практики в конструкторах сначала вызывать инициализацию предка, а потом уже переходить к своей. Только для того, чтобы обеспечить работоспособность виртуальных методов в условиях, когда объект ещё не готов к работе.
Ну и причины отчаиваться мне не очевидны. По-моему, вполне себе рабочий вопрос.
ИНОГДА мне кажется, что вы меня не слышите, а читаете, то что "хотите прочитать".. :-)
УдалитьОднако, вот "ровно то, что вы написали":
""В этом нет никакого секрета, а просто есть правило: виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.
Правило надо заучивать, что неудобно. Проще понять принцип. А принцип тут в краеугольном камне реализации наследования в C++: при создании объекта конструкторы в иерархии вызываются от базового класса к самому последнему унаследованному. Для деструкторов все наоборот.
Что получается: конструктор класса всегда работает в предположении, что его дочерние классы еще не созданы, поэтому он не имеет права вызывать функции, определенные в них. И для виртуальной функций ему ничего не остается, как только вызвать то, что определено в нем самом. Получается, что механизм виртуальных функций тут как-бы не работает. А он тут действительно не работает, так как таблица виртуальных функций дочернего класса еще не перекрыла текущую таблицу.
В деструкторе все наоборот. Деструктор знает, что во время его вызова все дочерние классы уже разрушены и вызывать у них ничего уже нельзя, поэтому он замещает адрес таблицы виртуальных функций на адрес своей собственной таблицы и благополучно вызывает версию виртуальной функции, определенной в нем самом.
Итак, виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.""
Собственно - "мысль поста" - была - БАНАЛЬНА - либо "использование фабрик", либо "слом парадигмы".
Удалить«ИНОГДА мне кажется, что вы меня не слышите, а читаете, то что "хотите прочитать".. :-)
УдалитьОднако, вот "ровно то, что вы написали":
...»
-- По этой части я ответил в личку.
«Собственно - "мысль поста" - была - БАНАЛЬНА - либо "использование фабрик", либо "слом парадигмы".»
-- Если бы такая мысль следовала из текста Вашего поста, я наверное высказался в ползу фабричного метода и против "слома парадигмы" в которой ничего плохого не вижу.
IMHO Александр, основную мысль поста Вы недостаточно артикулировали.
Теперь относительно парадигм (сокращённый вариант).
ОтветитьУдалитьЗачем нужны парадигмы?
Действительно, зачем?
Парадигма - это устоявшийся способ что-то делать.
Если вы следуете парадигме, и она известна окружающим, то что вы делаете им понятно.
В противном случае у них возникают вопросы "зачем так?" и "почему так?" И это в лучшем случае. В худшем решение или подход отбрасываются ввиду убеждённости в неграмотности автора и/или сложности решения.
Решение, игнорирующее сложившуюся парадигму действительно оказывается сложным, ввиду своей нестандартности, и оригинальности в плохом смысле.
Парадигма - это свёрнутый навык, основанный на опыте, включающем учёт ошибок и проблем, которые пришлось решать в процессе формирования опыта.
Плохо, когда парадигма становится догмой, и начинает формально применяться в ситуациях, когда контекст уже совсем другой - не тот, в котором формировалась парадигма.
Такое формальное применение ведёт к появлению эпически провальных подходов и уродливых решений.
Но если контекст "тот", стоит тысячу раз подумать, прежде чем отбрасывать её.