Mirage писал(а):Билдер это в общем-то просто объект. Можно его заполнять (необязательные поля) обычными присваиваниями.
Просто fluent-style выглядит красивее. Но это дело вкуса. Плюс иногда бывает группа параметров, по отдельности не имеющих смысла.
Про fluent-style - писать обёртку ради "красивее"? Fluent-style вообще в паскале применяется не так уж и часто - функция может и nil вернуть.
А что ещё хуже:
Код: Выделить всё
procedure TSomeEpicBuilder.SetVariable(a: Integer);
begin
fa:=a;
// а вот Result:=Self програмист и забыл.
end;
может слегка порушить создание объекта.
Mirage писал(а):Перво-наперво надо сделать допущение, что в FPC есть immutable объекты. Иначе смысла использовать билдер нет никакого. Думаю, это понятно.
Пожалуйста, весьма Immutable!
Код: Выделить всё
type
TImmutableSomething = class()
private
fYouCantChangeMe: TType;
public
property YouCantChangeMe: TType read fYouCantChangeMe;
end;
Mirage писал(а):Разница в том, что затем экземпляр билдера передается в конструктор immutable объекта.
...
Переиспользовать билдер это экономить на спичках. Если бы создание объектов являлось узким местом, то оно также было бы вынесено в разные потоки.
И переиспользование билдера было бы бессмысленно по описанным мной причинам.
А так оно бессмысленно потому что экономит там где не нужно.
Чтобы не было такого соблазна, то во-первых не должно быть возможности создать билдер отдельно от его основного класса,
только через специальный метод:
Ну и в методе build() он сам себя освобождает.
Теперь не только бессмысленно, но и нельзя.
Я же говорю - очень много инфраструктуры.
Что ещё хуже, "builder" по-своей логике служебный класс (т.к. он используется только для порождения новых классов), превращается в коренной класс,
т.к. без него "immutable object" не создастся, ибо должен передаваться в конструктор.
Mirage писал(а):Переиспользование повышает вероятность ошибок, т.к. вносит дополнительную зависимость от того, что было в объекте ранее.
Буквально вчера по работе исправлял очередную такую ошибку, где переиспользовался объект, но одно поле обнулить забыли.
Добавили поле, а метод Clear() не обновили. Ну что поделать - бывает.
Я бы предложил переписать всё на пересоздание объекта (вместо очищения).
Возможно, в итоге придётся вместо исправляния ошибок "не очищения", заниматся оптимизацей (вeрнувшись к переиспользованию объектов

).
А теперь посмотрим на туже проблему с точки зрения билдера.
Добавили новое поле в базовый класс, а вот код build() не поменяли, или даже метода не добавили для инициализации этого поля.
И оно приняло значение 0, хотя по-умолчанию, должна иметь другое значение.
Наличие билдера проблему не решает никак.
Mirage писал(а):Вынужден спросить: Вы занимались разработкой многопоточных программ?
Это не атомарно и не потокобезопасно.
Например вызываем stamp := BuildStampWithSomeData(blabla...);
Процессор (или компилятор) переставляют инструкции таким образом, что сперва идет Result := TStamp.Create(), затем stamp := Result, а затем уже все остальное. Имеет право, между прочим.
Вот после stamp := Result первый поток засыпает, просыпается второй и спокойно использует stamp, где не инициализировано ни одно поле.
А потом просыпается первый поток и дописывает поля stamp'a. Поверх того, что уже успел записать туда второй поток.
Такую ошибку можно долго искать.
Это если одно ядро. На нескольких все еще печальнее.
O.o
Отвечу на первый вопрос: да - занимаюсь разработкой многопоточных программ.
Вообще не понял к чему здесь:
Mirage писал(а):Процессор (или компилятор) переставляют инструкции таким образом, что сперва идет Result := TStamp.Create(), затем stamp := Result, а затем уже все остальное. Имеет право, между прочим.
Вот после stamp := Result первый поток засыпает, просыпается второй и спокойно использует stamp
[поскипан некорректный код]
Если не волнует, что printingThread может получить неинициализированный объект и, соответственно, напечатать ерунду, то не нужен.
Для начала:
Локальные данные располагаются в стеке самого потока, т.е. если поток №1 и поток №2 выполняют один и тот же код.
Код: Выделить всё
procedure SomeProc;
var
stamp: TStamp;
begin
..
stamp := BuildStampWithSomeData(blabla...)
Так у каждого "stamp" и "Result" будет свой - и никаких проблем нет.
Но к примеру, который я привёл, это вообще не имеет отношение!
Вызов BuildStampWithSomeData выполняется главным потоком.
Соответственно объект будет инициализирован перед, запуском потоков-исполнителей.
Просто потому что запуск потоков-исполнителей так же выполняется главным потоком.
Если ты хочешь сказать, что основной поток внезапно перепрыгнет
со строчки
на
Код: Выделить всё
for j:=1 to 10 do
LaunchPrintingThread(stamp)
и при этом исполнение BuildStampWithSomeData не завершилось, то я требую доказательств в виде примера.
(...этакие фантомные goto операции... )
Mirage писал(а):Билдер сам по себе ничего не гарантирует, кроме передачи всех параметров прямо в конструктор.
Однако, если объект immutable и это поддерживается компилятором, то есть гарантия, что все поля по выходу из конструктора
для всех потоков имеют именно то значение, какое присвоено в конструкторе.
В Java это гарантируется для всех final полей класса.
Подумай ещё раз.
Final поля выполняют проверку
во время компиляции, но не во время исполнения программы.
К потокобезопасности вообще никакого отношения не имеют.
(там же приведён пример, того что для "сложных" классов, понадобится писать дополнительные immutable обёртки - и для них тоже свои билдеры

Так вот спички скалдываются в горы спичек

но стоит ли экономить? )
Mirage писал(а):Это вообще-то не алгоритмическое решение проблемы синхронизации, а архитектурное. И возможно оно далеко не всегда.
...
При групповой разработке больших проектов довольно трудно понять, что менять можно, а что нет.
При групповой разработке очень помогает документация (на которую нет ни времени, ни сил - ещё бы!).
Ещё больше документация помогает реализовывать архитектурные решения.
Но это к теме никакого отношения не имеет.
Mirage писал(а):Объявить все поля как final. Сам класс, на всякий случай, тоже final.
О чём я и говорю, что на каждый "mutable" класс понадобится написать "immutable" а к нему ещё и "builder"

Написанный код увеличится втрое, скомпилированный вдвое. Каких-то дополнительных улучшений не будет. Зачем?
На самом деле, похоже что восхваляемые "immutable" объекты это просто "общая-копия" данных.
Сделай копию и используй её спокойно во всех потоках сколько влезет (о синхронизации можно и не думать).
Почему бы вместо builder-а просто не использовать соответствующий рабочий класс.
С сохранением логической порядка, где ImmutableObject это служебный класс и зависит от основного.
Код: Выделить всё
type
TMutableObject = class(TObject)
public
Data : Integer;
end;
type
TImmutableObject = class(TObject)
private
fData: integer;
public
constructor Create(amutable: TMutableObject); // конструктор копирует нужные данные
property Data : Integer read fData;
end;
Все дела! нахрена спец-билдеры c fluent-style и build() методом? непонятно!
На самом деле понятно, но я
причины не буду озвучивать, ибо это сугубо моё личное мнение в сторону автора
книжки/статьи про билдеры, никак с технической стороной вопроса не связанные.