пятница, 26 сентября 2014 г.

Коротко. О конструкторах

Депрессия - "частично" побеждена.

Много ошибок найдено.

В частности - СПАСИБО 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. Собственно - "мысль поста" - была - БАНАЛЬНА - либо "использование фабрик", либо "слом парадигмы".

Хотя "для некоторых" и "использование фабрик" это - "слом парадигмы". К сожалению.

8 комментариев:

  1. Начну немного издалека.
    Протокол работы с объектом в Delphi состоит из трёх этапов:
    1. Инициализация (конструктор)
    2. Использование (обращение к остальным свойствам и методам)
    3. Финализация (обычно Free, поскольку деструктор напрямую лучше не вызывать).
    Пункты 1 и 3 протокола являются специальными в том смысле, что процессе их выполнения объект находится, вообще говоря, в некорректном состоянии, поскольку инварианты объекта могут выполняться, и это воспринимается чаще всего без удивления: действительно, чего ещё можно ожидать от не до конца созданного или от частично разрушенного объекта?
    Теперь рассмотрим виртуальный метод, который вызывается в процессе инициализации или финализации объекта — я думаю это уместно, поскольку в отношении инвариантов ситуации совершенно идентичны.
    Очевидно, что такой виртуальный метод должен быть рассчитан на то, что инварианты не выполняются, иными словами, такой виртуальный метод должен учитывать, что в момент его выполнения объект будет находиться в некорректном состоянии. Соответственно, при перекрытии такого виртуального метода в потомке, означенное обстоятельство также следует учитывать.
    Поскольку у нас появляются дополнительные соглашения, они могут быть (следовательно — будут) совершенно не очевидны тому разработчику, который будет такое перекрытие выполнять. Он «всего лишь» перекрывает виртуальный метод, а тут оказывается, что это «нельзя просто так взять и...» (c) – нужно учитывать, что объект может быть в некорректном состоянии. Об этом я говорю здесь, правда, в контексте деструкторов.
    Это даёт основание говорить об усложнении решения.

    Теперь по-существу.
    «Давайте напишем "пока" так:

    Как быть?»

    – Как минимум, я вижу два варианта:
    1. Не делать так. Т.е. не вызывать виртуальные методы в конструкторе, т. е. в условиях, когда выполнение инвариантов не гарантируется.
    Вызов виртуальных методов перенести в п. 2 протокола работы с объектом, например, выделив в нём этап настройки.
    Если нас смущает необходимость выполнения этой настройки — можно использовать методы класса по конструированию его экземпляров или прочие разновидности фабричного метода.
    IMHO – это предпочтительный вариант.
    2. Вызывать виртуальные методы в перекрытии виртуального AfterConstruction.
    Действительно, в этом случае этап инициализации пройден и инварианты обязаны уже выполняться. Объект находится в корректном состоянии и вызов другого виртуального метода в таких условиях не потребует учёта артефактов.
    В сущности, AfterConstruction это уже этап использования, но совмещённый с окончанием инициализации.
    Что мне в этом нравится меньше? - По двум причинам:
    а) То, что это в меньшей степени соответствует принципу наименьшего удивления. Об AfterConstruction мало кто знает, поэтому для «не продвинутого» разработчика передача управления может выглядеть как «магия».
    б) Код в AfterConstruction может быть использован для инициализации, вследствие чего возникнет очередной виток вопросов относительно того, где именно расположить вызов требуемого виртуального метода.

    ОтветитьУдалить
    Ответы
    1. «Пункты 1 и 3 протокола являются специальными в том смысле, что процессе их выполнения объект находится, вообще говоря, в некорректном состоянии, поскольку инварианты объекта могут выполняться»
      -- Думаю понятно, что в выделенном фрагменте имелось ввиду "не выполняться".

      Удалить
    2. Я - понял. Но если "вы не поняли", то я как раз - "за фабрики".

      А про конструкторы написал - уже "от отчаянья".

      Удалить
    3. «Я - понял. Но если "вы не поняли", то я как раз - "за фабрики".»
      -- Александр, ну какая разница за что Вы и против чего? :-)
      Я оппонирую тому, что Вы написали.
      Вроде бы понятно изложил, почему нахожу использование виртуальных методов в конструкторах и деструкторах опасным. Почему считаю не лучшей идеей отказ от сложившейся практики в конструкторах сначала вызывать инициализацию предка, а потом уже переходить к своей. Только для того, чтобы обеспечить работоспособность виртуальных методов в условиях, когда объект ещё не готов к работе.
      Ну и причины отчаиваться мне не очевидны. По-моему, вполне себе рабочий вопрос.

      Удалить
    4. ИНОГДА мне кажется, что вы меня не слышите, а читаете, то что "хотите прочитать".. :-)

      Однако, вот "ровно то, что вы написали":

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

      Правило надо заучивать, что неудобно. Проще понять принцип. А принцип тут в краеугольном камне реализации наследования в C++: при создании объекта конструкторы в иерархии вызываются от базового класса к самому последнему унаследованному. Для деструкторов все наоборот.

      Что получается: конструктор класса всегда работает в предположении, что его дочерние классы еще не созданы, поэтому он не имеет права вызывать функции, определенные в них. И для виртуальной функций ему ничего не остается, как только вызвать то, что определено в нем самом. Получается, что механизм виртуальных функций тут как-бы не работает. А он тут действительно не работает, так как таблица виртуальных функций дочернего класса еще не перекрыла текущую таблицу.

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

      Итак, виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.""

      Удалить
    5. Собственно - "мысль поста" - была - БАНАЛЬНА - либо "использование фабрик", либо "слом парадигмы".

      Удалить
    6. «ИНОГДА мне кажется, что вы меня не слышите, а читаете, то что "хотите прочитать".. :-)
      Однако, вот "ровно то, что вы написали":
      ...»

      -- По этой части я ответил в личку.

      «Собственно - "мысль поста" - была - БАНАЛЬНА - либо "использование фабрик", либо "слом парадигмы".»
      -- Если бы такая мысль следовала из текста Вашего поста, я наверное высказался в ползу фабричного метода и против "слома парадигмы" в которой ничего плохого не вижу.
      IMHO Александр, основную мысль поста Вы недостаточно артикулировали.

      Удалить
  2. Теперь относительно парадигм (сокращённый вариант).
    Зачем нужны парадигмы?
    Действительно, зачем?
    Парадигма - это устоявшийся способ что-то делать.
    Если вы следуете парадигме, и она известна окружающим, то что вы делаете им понятно.
    В противном случае у них возникают вопросы "зачем так?" и "почему так?" И это в лучшем случае. В худшем решение или подход отбрасываются ввиду убеждённости в неграмотности автора и/или сложности решения.
    Решение, игнорирующее сложившуюся парадигму действительно оказывается сложным, ввиду своей нестандартности, и оригинальности в плохом смысле.
    Парадигма - это свёрнутый навык, основанный на опыте, включающем учёт ошибок и проблем, которые пришлось решать в процессе формирования опыта.
    Плохо, когда парадигма становится догмой, и начинает формально применяться в ситуациях, когда контекст уже совсем другой - не тот, в котором формировалась парадигма.
    Такое формальное применение ведёт к появлению эпически провальных подходов и уродливых решений.
    Но если контекст "тот", стоит тысячу раз подумать, прежде чем отбрасывать её.

    ОтветитьУдалить