четверг, 21 августа 2014 г.

Коротко. О фабриках

В некотором смысле в продолжение темы поднятой тут - Коротко. О возбуждении исключений

Отчасти в продолжение вот этой темы - Фабричный метод

Как можно создавать объект?

Ну конечно же так:

type
 TmyObject = class
  public
   constructor Create(aSomeData: TSomeData);
 end;//TmyObject

...
var
 myObject : TmyObject;
...
 myObject := TmyObject.Create(aSomeData);

А можно так:

type
 TmyObject = class
  protected
   constructor Create(aSomeData: TSomeData);
  public
   class function Make(aSomeData: TSomeData): TmyObject;
 end;//TmyObject

...
class function TmyObject.Make(aSomeData: TSomeData): TmyObject;
begin
 if IsValidData(aSomeData) then
  Result := Self.Create(aSomeData)
 else
  Result := nil;
end;
...
var
 myObject : TmyObject;
...
 myObject := TmyObject.Make(theConcreteData);

-- в чём разница? 

А в том, что в "фабричном методе" можно вставить некоторую бизнес-логику.

В нашем случае это - IsValidData(aSomeData).

Но можно пойти дальше:

interface
...
type
 TmyObject = class
  protected
   constructor Create(aSomeData: TSomeData);
   procedure SomeMethodToOverride; virtual;
  public
   class function Make(aSomeData: TSomeData): TmyObject;
 end;//TmyObject
...
implementation
...
 TmySpecialObject = class(TmyObject)
  protected
   procedure SomeMethodToOverride; override;
 end;//TmySpecialObject

...
class function TmyObject.Make(aSomeData: TSomeData): TmyObject;
begin
 if IsMySpecialData(aSomeData) then
  Result := TmySpecialObject.Create(aSomeData) 
 else
 if IsValidData(aSomeData) then
  Result := Self.Create(aSomeData)
 else
  Result := nil;
end;
...
var
 myObject : TmyObject;
...
 myObject := TmyObject.Make(theConcreteData);

А можно пойти ещё дальше:

interface
...
type
 TmyObject = class
  protected
   constructor Create(aSomeData: TSomeData);
   procedure SomeMethodToOverride; virtual;
  public
   class function Make(aSomeData: TSomeData): TmyObject;
 end;//TmyObject
...
implementation
...
 TmyNULLObject = class(TmyObject)
  protected
   procedure SomeMethodToOverride; override;
 end;//TmyNULLObject

 TmySpecialObject = class(TmyObject)
  protected
   procedure SomeMethodToOverride; override;
 end;//TmySpecialObject

...
class function TmyObject.Make(aSomeData: TSomeData): TmyObject;
begin
 if IsMySpecialData(aSomeData) then
  Result := TmySpecialObject.Create(aSomeData) 
 else
 if IsValidData(aSomeData) then
  Result := Self.Create(aSomeData)
 else
  Result := TmyNULLObject.Create(aSomeData);
end;
...
var
 myObject : TmyObject;
...
 myObject := TmyObject.Make(theConcreteData);

Ну и можно пойти и ещё дальше:

interface
...
type
 ImyInterface = interface
  procedure SomeMethodToOverride;
 end;//ImyInterface

 TmyObject = class(TIntefacedObject, ImyInterface)
  protected
   constructor Create(aSomeData: TSomeData);
   procedure SomeMethodToOverride; virtual;
  public
   class function Make(aSomeData: TSomeData): ImyInterface;
 end;//TmyObject
...
implementation
...
 TmyNULLObject = class(TIntefacedObject, ImyInterface)
  protected
   procedure SomeMethodToOverride;
   // - Тут понятное дело override уже не нужен
   constructor Create(aSomeData: TSomeData);
  public
   class function Make(aSomeData: TSomeData): ImyInterface;
   // - а тут вообще говоря можно "забабахать синглетон"
 end;//TmyNULLObject

 TmySpecialObject = class(TmyObject)
  protected
   procedure SomeMethodToOverride; override;
 end;//TmySpecialObject

...
class function TmyObject.Make(aSomeData: TSomeData): ImyInterface;
begin
 if IsMySpecialData(aSomeData) then
  Result := TmySpecialObject.Create(aSomeData) 
 else
 if IsValidData(aSomeData) then
  Result := Self.Create(aSomeData)
 else
  Result := TmyNULLObject.Make(aSomeData);
end;
...
var
 myObject : ImyInterface;
...
 myObject := TmyObject.Make(theConcreteData);

Ну вот собственно и всё.

Надеюсь, что это кому-нибудь понравится.

Опять же оговорюсь, что это всего лишь "макет".

Да!

И ещё одна краткая ремарка.

Как сделать так, чтобы "не прошли мимо фабрики"?

Т.е. чтобы не вызвали "паразитный умолчательный конструктор". Который от TObject.

А вот так:

type
 TmyObject = class
  protected
   constructor Create(aSomeData: TSomeData); overload;
  public
   class function Make(aSomeData: TSomeData): TmyObject;
   constructor Create; overload;
 end;//TmyObject
...
constructor TmyObject.Create;
begin
 Assert(false, 'Надо вызывать фабричный метод, а не унаследованный конструктор');
end;

А можно сделать ещё "веселее", вот так:

type
 TmyObject = class
  protected
   constructor InternalCreate(aSomeData: TSomeData);
  public
   class function Make(aSomeData: TSomeData): TmyObject;
   procedure Create;
 end;//TmyObject
...
procedure TmyObject.Create;
begin
 Assert(false, 'Надо вызывать фабричный метод, а не унаследованный конструктор');
end;

-- тогда ошибочный код вообще компилироваться не будет.

А можно наверное вообще так:

type
 TmyObject = class
  protected
   constructor InternalCreate(aSomeData: TSomeData);
  public
   class function Create(aSomeData: TSomeData): TmyObject;
 end;//TmyObject

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

  1. В дополнение. Недавно делал так:

    TmyNULLObject = class(TMyIntefacedObjectNoRefCount, ImyInterface)
    class function Make(aSomeData: TSomeData): ImyInterface;
    class function MakeAsRef(aSomeData: TSomeData): TmyNULLObject;
    end;//TmyNULLObject

    На свой страх и риск разумеется.

    ОтветитьУдалить
    Ответы
    1. "На свой страх и риск разумеется"

      -- страх и риск в чём? В смешении объектов интерфейсов? Или в отсутствии подсчёта ссылок?

      Или я вообще что-то не так понял?

      Удалить
    2. В сочетании. Ну т.е. всё нормально пока этим кодом пользуется автор или тот кто изучил детали реализации. Но по ощущениям, не самый красивый подход.
      Сейчас задумался. Наверное больше смущает всё же отсутствие подсчёта ссылок. И корректнее было бы так:

      TmyNULLObject = class(TMyIntefacedObjectNoRefCount, ImyInterface)
      class function MakeNoRefCount(aSomeData: TSomeData): ImyInterface;
      class function MakeAsObject(aSomeData: TSomeData): TmyNULLObject;
      end;//TmyNULLObject

      Удалить
  2. Лично мне это всё знакомо, но когда смотришь на это со стороны..... в этом есть некий "фан" :)

    В дополнение - мы ещё такую фишку используем:
    TSomeAbstractClass = class
    class procedure DoSomething; virtual; abstract;
    class procedure DoSomethingElse; virtual; abstract;
    end;
    и от него есть наследники. Много наследников.

    а дальше, в другом классе есть переменная, что-то вида:
    FHelper: TSomeAbstractClass
    которая инициализируется один раз, но при необходимости может подменяться (переинициаилизироваться)
    ну и соответственно в коде идут вызовы вида
    FHelper.DoSomething;
    FHelper.DoSomethingElse;

    т.е. используется как некая имитация множественного наследования, что-ли..

    ОтветитьУдалить
    Ответы
    1. «а дальше, в другом классе есть переменная, что-то вида:
      FHelper: TSomeAbstractClass
      которая инициализируется один раз, но при необходимости может подменяться (переинициаилизироваться)
      ну и соответственно в коде идут вызовы вида
      FHelper.DoSomething;
      FHelper.DoSomethingElse;»

      -- Было бы очень интересно ознакомиться с причинами, которые вызвали такое архитектурное решение.
      Если бы был приведён реальный (не "синтетический") пример, так вообще было бы замечательно...

      Удалить
    2. ну вкратце - это хорошая замена case'у. Реальный пример - хорошо, я постараюсь это описать..

      Удалить
    3. Кстати, за примером далеко ходить не надо, в FMX такое используется.

      С реальным примером из нашего проекта -- тяжело, слишком много кода.

      Но я ещё и не правильно написал. Кроме TSomeAbstractClass есть ссылка на класс:

      type
      TSomeAbstractClassClass = class of TSomeAbstractClass;

      И FHelper имеет тип TSomeAbstractClassClass.

      В нашем проекте TSomeAbstractClassClass содержит только классовые методы, поэтому от TSomeAbstractClassClass объекты не создаются. Но можно и создать, т.е. сделать что-то типа такого:

      FHelperClass: TSomeAbstractClassClass
      FHelper: TSomeAbstractClass
      ..
      init FHelperClass
      ..
      FHelper := FHelperClass.Create;

      Удалить
    4. >> Было бы очень интересно ознакомиться с причинами, которые вызвали такое архитектурное решение
      А причины простые: быстродействие и расход памяти. (Да, это работа с табличными данными, порой даже с большими объёмами -- я уже как-то писал в блоге, что мы не используем стандартные датасеты.)

      Удалить
    5. Звучит похоже на "паттерн стратегия".
      Но смущает в этом лишь то, что эта переменная "при необходимости может подменяться (переинициаилизироваться)".

      Удалить
    6. «С реальным примером из нашего проекта -- тяжело, слишком много кода.»
      -- Жаль... :-(
      Уже было обрадовался...
      Код ненужен. Хотелось бы понять задачу, которую решает этот код.
      Просто под обозначенную Вами схему решения подпадает много разных вариантов моделируемых ситуаций.
      Они все основаны на объекте-посреднике (FHelper у Вас), но служат совершенно разным целям.
      Вот "на вскидку", смотрите: паттерны Заместитель, Адаптер, Декоратор, Команда используют объект посредник, но решаемые задачи заметно отличаются. Да и реализации Шаблонного метода и Наблюдателя тоже часто опираются на объекты-посредники.
      А я бы мог со своей стороны поделиться тем, как решаются такие задачи у нас.
      Это запланировано, но... Чем больше реальных примеров - тем лучше :-)

      Удалить
    7. " в этом есть некий "фан" :)"
      -- да "фан" есть, когда другие люди "от сохи" делают примерно то же "что и ты".

      Удалить
    8. "т.е. используется как некая имитация множественного наследования, что-ли.."
      -- мне что-то подсказывает, что пройдёт год-другой и мы с Вами Николай сможем и примеси обсудить :-)

      Которые пока всем показались не "комильфо".

      А там может и АОП не на иньекциях, а на примесях :-)

      Ни в коем случае не сочтите это менторством.

      Может и я в свою очередь через год-два - в примесях разочаруюсь.

      "Всё течёт, всё меняется".

      Удалить
    9. А "если серьёзно", то "достаточно часто" - множественное наследование и примеси (как частный случай) - легко и элегантно заменяются делегированием, стратегией, фасадом и прочими "шаблонами проектирования от GoF".

      "Достаточно часто", но не везде. Например RefCounting - это всё же примесь. Просто из соображений эффективности и "экономии на спичках".

      Удалить
    10. Позволю процитировать себя - http://18delphi.blogspot.ru/2013/04/iunknown.html - "Собственная реализация IUnknown и подсчёт ссылок. И примеси"

      Удалить
    11. "«С реальным примером из нашего проекта -- тяжело, слишком много кода.»
      -- Жаль... :-("

      -- жаль что мы "разбросаны по городам и весям", иначе могли бы устроить "фидопойку" и "обменяться опытом"...

      ведь "на пальцах" - зачастую "быстрее объяснить"...

      Но! Письменно - хотя и дольше, но качественнее, это надо признать.

      Удалить
    12. "Было бы очень интересно ознакомиться с причинами, которые вызвали такое архитектурное решение."
      -- NameRec, я могу конечно ошибаться, но по-моему это некая "замена" Вашему, "расширенному делегированию".

      Удалить
    13. «NameRec, я могу конечно ошибаться, но по-моему это некая "замена" Вашему, "расширенному делегированию".»
      -- Нет... Скорее наоборот. Я могу предоставить модуль, где решается аналогичная задача, немного в более общем виде, с другими соглашениями и с применением ED.
      Обобщение состоит в том, что поля записи не обязаны находиться в едином буфере, с типизацией ситуация похожая - набор типов открыт для расширения, а доступ к значениям полей производится средствами ED.
      Модуль применялся как прослойка для доступа к наборам данных HyTech из Python.
      Я там по молодости реально переборщил с общностью, в частности, набор типов расширять не потребовалось (или потребовалось, но один раз, т.е. считайте - не потребовалось), а целый ряд характеристик полей из стремления к общности я сделал динамическими атрибутами, что тоже не ускорило решение :-) впрочем, если и замедлило, то незаметно.
      ED было применено для отделения способа физического представления данных (как данные расположены и где - в RAM, в на диске при буферизации с LRU-своппингом или в наборе данных, в полях BDS/SAB (термины HyTech) или в полях потомка DB.DataSet) - за это отвечал поставщик данных...

      Удалить
  3. Про "использование классовых методов" отчасти написано тут - http://habrahabr.ru/post/232955/.

    Тот самый RmsShape и список RegisteredShapes, которые вызвали вопросы :-)

    ОтветитьУдалить
  4. Всё же, я решил написать о решаемой задачи. Прошу прощения - сумбурно, наверное с ошибками, ибо уже 2 часа ночи.

    Задача, которую мы решаем - работа с наборами данных. Эдакий аналог стандартного датасета. Попробую по-короче описать, как мы это делаем.

    Есть датасет, в нём есть набор полей (столбцов) и набор записей (строк). Данные в оперативной памяти хранятся по строкам. Т.е. набор полей, как правило, для датасета задаётся один раз. Каждое поле определяет, сколько памяти нужно под хранение одного значения (ну, грубо, в зависимости от типа данных в поле - SizeOf(Integer), SizeOf(Extended), SizeOf(Pointer) и т.п.). Кроме того поле может определять, нужно ли отдельно хранить признак is null. Ну и другие вещи. Когда набор полей в датасете сформирован - определяется итоговый размер памяти, необходимый под хранение записи целиком. Т.е. под одну запись память выделяется линейным куском типа GetMem(ARecord, FRecSize). Доступ к значению поля в записи (т.е. доступ к конкретной ячейке) - это есть обращение к записи с некоторым смещением, фиксированным для каждого поля. PByte(Integer(ARecord) + AField.Offset).
    Вобщем тип данных поля (DataType) - это один из факторов, которые могут дополняться в процессе развития системы. В стандартном модуле DB - это обычное наследование от базового TField, т.е. определяя новый тип данных - просто наследуемся от TField. У нас - не так. У нас здесь используется тот самый посредник. Чуть ниже - пример.
    Есть другой фактор - тип поля (FieldKind). Как пример - DataField (поле владеет значением непосредственно), LookupField (значение поля вычисляется по принципу подстановки), CalcField (вычисляемое поле). (А ещё, в процессе эволюции, у нас появилось DataLookup.)
    Вот в DB тип поля - есть просто некое свойство. И TField там выступает в качестве базового класса.
    У нас же иерархия классов такая:
    TBaseField -> TDataField
    TBaseField -> TLookupField
    TBaseField -> TCalcField -> TCalcProc1, TCalcProc2...
    TBaseField - базовый класс, он не знает о том, как обратиться к значению. К значению обращаются: TDataField - по смещению в записи (Offset), TLookupField - вычисляет по параметрам подстановки, TCalcField - представляет интерфейс для реализации вычисляемых полей (которые реализуются в наследниках).

    ОтветитьУдалить
    Ответы
    1. Helper приходит на помощь, когда необходимо получить значение AsXXX. AsInteger, или AsString, или AsVariant, или AsDisplayString. У нас есть абстрактный класс, целиком код не привожу, для сути только:
      TPhisicalField = class
      class function NeedInitialization: Boolean; virtual;
      class function GetFieldType: TDataType; virtual; abstract;
      class function GetValueSize: Longword; virtual; abstract;
      ...
      class function IsEqual(PValue1, PValue2: Pointer): Boolean; virtual; abstract;
      class function Compare(PValue1, PValue2: Pointer; const Options: TPhCompareOptions = []): Integer; virtual; abstract;
      ...
      class function GetDisplayString(PValue: Pointer; const Format: string): string; virtual; abstract;
      ...
      class function GetAsVariant(PValue: Pointer): Variant; virtual; abstract;
      class function GetAsString(PValue: Pointer): string; virtual; abstract;
      class function GetAsInteger(PValue: Pointer): Integer; virtual;
      ...
      class procedure SetAsVariant(PValue: Pointer; const Value: Variant); virtual;
      class procedure SetAsString(PValue: Pointer; const Value: string); virtual; abstract;
      class procedure SetAsInteger(PValue: Pointer; Value: Integer); virtual;
      ...
      end;
      TPhisicalFieldClass = class of TPhisicalField;
      И от него наследники: TphInteger, TphString, TphNumericSortString, TphBLOB... Наследники сообщают о размере под данные (GetValueSize) и делают все необходимые конвертации при обращениях Get/Set AsXXX. И много чего ещё.
      Соответственно Helper в TBaseField у нас называется FPhisical и имеет тип TPhisicalFieldClass; все обращения на уровне BaseField.GetAsXXX сводятся к вызову вида BaseField.FPhisical.GetAsXXX(GetPValue(ARecord)).
      (Тут GetPValue - это абстарктный метод в TBaseField, который реализуется в наследниках (Offset для Data-поля и т.д.))
      FPhisical инициализируется при создании поля. Но иногда он может безболезненно подмениться, например TphString на TphNumericSortString и обратно (второй является наследником от первого и просто по другому реализует метод сравнения).
      P.S.: На самом деле, я уже давно сомневаюсь, на сколько такой подход оправдан. Но у нас на нём вся "архитектура" построена. Есть даже побочный эффект, которым мы пользуемся при генерации кода: когда есть два идентичных по структуре полей датасета, то от имени поля из первого датасета я могу безопасно обратиться к значению записи из второго датасета. Это удобно при написании кода. Однако, если бы оно было по другому реализовано, то и генератор бы у меня был бы другим... (позволю себе оставить ссылку: http://www.delphinotes.ru/2011/07/blog-post_14.html)

      Удалить
    2. Николай, тут "статьёй пахнет" :-)

      Удалить
    3. «Всё же, я решил написать о решаемой задачи. Прошу прощения - сумбурно, наверное с ошибками, ибо уже 2 часа ночи.»
      -- Напротив, я убеждён, что у Вас получилось с чувством с толком и с расстановкой.
      Мне, по крайней мере, многое стало понятно. В частности, Ваш "FPhisical" при описанном подходе выглядит вполне уместно.
      Большое спасибо за контекст.

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

      «Однако, если бы оно было по другому реализовано, то и генератор бы у меня был бы другим... (позволю себе оставить ссылку: http://www.delphinotes.ru/2011/07/blog-post_14.html)»
      -- И ссылка Ваша совершенно к месту, поскольку без неё трудно было бы сообразить, для чего нужен генератор кода.
      В общем (да и в частности) Вы создали два совершенно безупречных поста Николай.
      Ещё раз благодарю.

      Удалить
    4. Что-то я смотрю, что "разбередил" такую тему в которой и сам уже начинаю "терять мысль" :-)

      Неожиданно...

      Удалить
    5. По существу. Как и обещал, расскажу "как у нас"...
      Мы используем потомки DB.DataSet. Any/FreeDAC для многозвенной архитектуры, и kbmMW - для многозвенной.
      Проблему, описанную в Вашей замечательной статье решили несколько иначе: у нас есть модули с константами имён полей, т.е. вручную их набирают при первом использовании, возможные ошибки в написании "отбиваются" при проверке и тестировании.
      Причины, по которым мы не используем ORM-подход (это когда по метаданным генерируются классы для таблиц и полей, например):

      * Приложение должно "уметь" работать с БД, полная структура которой неизвестна.
      Действительно, относительно незначительная часть полей из общей совокупности обрабатывается бизнес-логикой, что даёт возможность как автоматического формирования форм по метаданным, так и обеспечить алгоритмы изменения данных в большинстве случаев совершенно автоматически.
      * Структура БД может измениться, но приложение должно быть устойчиво к некритичным изменениям.
      * Мы стремимся очень редко открывать таблицу с полным набором атрибутов - такое бывает только в формах ввода, а в недалёком будущем - даже в них BLOB-поля буду оказываться только по запросу. В такой ситуации класс, содержащий определения всех полей — избыточен, хотя понятно, можно добавить соответствующий признак наличия поля в курсоре.

      Таблиц в БД предметной области ~ 618.
      В целом то, что компилятор не проверяет наличие полей в таблице проблем не создаёт, хотя и накладывает определённые технологические требования.
      В любом случае, пока не слышал о том, чтобы кто-то позиционировал это как проблему.
      С другой стороны, вполне допускаю, что у Вас — другая ситуация.

      Очень интересно было бы узнать, почему Вы решили пойти путём отказа от доступа к данным посредством потомков DB.TDataSet.
      Не сомневаюсь, что разработка своего варианта модуля DB и DB-aware компонентов была достаточно затратной. Вероятно, были серьёзные причины пойти на этот шаг...

      Удалить
    6. >> В любом случае, пока не слышал о том, чтобы кто-то позиционировал это как проблему.
      Конечно, это не проблема. Но у нас:
      а) таблиц тоже больше 500. Имена их всех в голове и не удержишь
      б) удобство при наборе кода - я начинаю набирать имя таблицы, жамкаю Ctrl+Space - и вижу варианты. Так легче вспомнить имя таблицы, чем отрываться из IDE в другое приложение. То же самое и с именами полей - ставлю точку после имени таблицы, Ctrl+Space - и все поля (в том числе и локальные - лукапы и вычисляемые) перед глазами. В общем суть в том, что Ctrl+Space значительно дешевле, чем Alt+Tab.

      Есть ещё такое удобство - допустим у меня есть подозрение, что некое поле в некой таблице не используется (в рамках приложения). И имя этого поля какое-нибудь такое, которое используется в других таблицах. И, допустим, я хочу:
      а) проверить, действительно ли оно нигде не используется
      б) удалить и забыть.
      Вот с кодогенератором всё просто - я удаляю объявление поля из XML, а дальше компилятор покажет, были ли ссылки на это поле из кода. Это быстрее, чем поиск по исходникам (хотя привычка сначала искать, а потом удалять, у меня до сих пор осталась).

      >> Очень интересно было бы узнать, почему Вы решили пойти путём отказа от доступа к данным посредством потомков DB.TDataSet
      На это я отвечу ссылкой.
      Хотя на самом деле, не моя это была идея. Когда я пришёл в нашу компанию (2006 год), там уже свои наработки были. Примерно с 1998 года эти исходники потихоньку создавались. Я лишь уже развивал начатое, оптимизировал, "кодогенерил"..
      А DB-aware компонентов мы не используем, используем стандартные компоненты и над ними есть обёртки. И это очень удобно - например вот и вот - достаточно было в одном месте прописать вызов этих "полезняшек"

      Удалить
    7. Да уж ребят. Почитал я вас и понял, что у меня в проекте всё совсем несерьёзно.

      Даже задумался, а не сделать ли мне свой генератор классов по объектам в БД, чтобы автокомплит работал. Интересно, насколько удобно будет пользоваться IDE если там по классу на каждую таблицу, view, sp будет общим количеством в пару тысяч штук. Наверно не очень.

      Удалить
    8. Не знаю... Мне конечно интересно, когда кто-то делает что-то такое, на что я не решился, но предложение генерировать классы для того, чтобы было удобно использовать IDE мне представляется несколько... м-м-м-м... избыточным, наверное...
      IMHO разного уровня проблема и решение.
      Думаю, не так трудно разработать мастер для IDE, который позволит автоматически определять константы имён для интересующий полей и вставлять их в код после выбора из таблицы соединения, ассоциированного с проектом.
      Но вообще-то, тема представляется мне несколько надуманной, поскольку имена полей из таблиц БД предметной области не единственное (а в нашем случае, даже не основное место), где эти имена используются. Например, имена полей массово упоминаются в формах ввода, отчётах и, в нашем случае, формах просмотра, которые пользователь может сам создавать.
      Что касается удобства ссылки на поля таблиц в коде, то IMHO не такая уж это проблема, чтобы "огород городить"...
      Разумеется, всё сказанное - мои личные ощущения, цена которым - ноль.

      Удалить