В некотором смысле в продолжение темы поднятой тут - Коротко. О возбуждении исключений
Отчасти в продолжение вот этой темы - Фабричный метод
Как можно создавать объект?
Ну конечно же так:
А можно так:
-- в чём разница?
А в том, что в "фабричном методе" можно вставить некоторую бизнес-логику.
В нашем случае это - IsValidData(aSomeData).
Но можно пойти дальше:
А можно пойти ещё дальше:
Ну и можно пойти и ещё дальше:
Ну вот собственно и всё.
Надеюсь, что это кому-нибудь понравится.
Опять же оговорюсь, что это всего лишь "макет".
Да!
И ещё одна краткая ремарка.
Как сделать так, чтобы "не прошли мимо фабрики"?
Т.е. чтобы не вызвали "паразитный умолчательный конструктор". Который от TObject.
А вот так:
А можно сделать ещё "веселее", вот так:
-- тогда ошибочный код вообще компилироваться не будет.
А можно наверное вообще так:
Отчасти в продолжение вот этой темы - Фабричный метод
Как можно создавать объект?
Ну конечно же так:
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
В дополнение. Недавно делал так:
ОтветитьУдалитьTmyNULLObject = class(TMyIntefacedObjectNoRefCount, ImyInterface)
class function Make(aSomeData: TSomeData): ImyInterface;
class function MakeAsRef(aSomeData: TSomeData): TmyNULLObject;
end;//TmyNULLObject
На свой страх и риск разумеется.
"На свой страх и риск разумеется"
Удалить-- страх и риск в чём? В смешении объектов интерфейсов? Или в отсутствии подсчёта ссылок?
Или я вообще что-то не так понял?
В сочетании. Ну т.е. всё нормально пока этим кодом пользуется автор или тот кто изучил детали реализации. Но по ощущениям, не самый красивый подход.
УдалитьСейчас задумался. Наверное больше смущает всё же отсутствие подсчёта ссылок. И корректнее было бы так:
TmyNULLObject = class(TMyIntefacedObjectNoRefCount, ImyInterface)
class function MakeNoRefCount(aSomeData: TSomeData): ImyInterface;
class function MakeAsObject(aSomeData: TSomeData): TmyNULLObject;
end;//TmyNULLObject
Лично мне это всё знакомо, но когда смотришь на это со стороны..... в этом есть некий "фан" :)
ОтветитьУдалитьВ дополнение - мы ещё такую фишку используем:
TSomeAbstractClass = class
class procedure DoSomething; virtual; abstract;
class procedure DoSomethingElse; virtual; abstract;
end;
и от него есть наследники. Много наследников.
а дальше, в другом классе есть переменная, что-то вида:
FHelper: TSomeAbstractClass
которая инициализируется один раз, но при необходимости может подменяться (переинициаилизироваться)
ну и соответственно в коде идут вызовы вида
FHelper.DoSomething;
FHelper.DoSomethingElse;
т.е. используется как некая имитация множественного наследования, что-ли..
«а дальше, в другом классе есть переменная, что-то вида:
УдалитьFHelper: TSomeAbstractClass
которая инициализируется один раз, но при необходимости может подменяться (переинициаилизироваться)
ну и соответственно в коде идут вызовы вида
FHelper.DoSomething;
FHelper.DoSomethingElse;»
-- Было бы очень интересно ознакомиться с причинами, которые вызвали такое архитектурное решение.
Если бы был приведён реальный (не "синтетический") пример, так вообще было бы замечательно...
ну вкратце - это хорошая замена case'у. Реальный пример - хорошо, я постараюсь это описать..
УдалитьКстати, за примером далеко ходить не надо, в FMX такое используется.
УдалитьС реальным примером из нашего проекта -- тяжело, слишком много кода.
Но я ещё и не правильно написал. Кроме TSomeAbstractClass есть ссылка на класс:
type
TSomeAbstractClassClass = class of TSomeAbstractClass;
И FHelper имеет тип TSomeAbstractClassClass.
В нашем проекте TSomeAbstractClassClass содержит только классовые методы, поэтому от TSomeAbstractClassClass объекты не создаются. Но можно и создать, т.е. сделать что-то типа такого:
FHelperClass: TSomeAbstractClassClass
FHelper: TSomeAbstractClass
..
init FHelperClass
..
FHelper := FHelperClass.Create;
>> Было бы очень интересно ознакомиться с причинами, которые вызвали такое архитектурное решение
УдалитьА причины простые: быстродействие и расход памяти. (Да, это работа с табличными данными, порой даже с большими объёмами -- я уже как-то писал в блоге, что мы не используем стандартные датасеты.)
Звучит похоже на "паттерн стратегия".
УдалитьНо смущает в этом лишь то, что эта переменная "при необходимости может подменяться (переинициаилизироваться)".
«С реальным примером из нашего проекта -- тяжело, слишком много кода.»
Удалить-- Жаль... :-(
Уже было обрадовался...
Код ненужен. Хотелось бы понять задачу, которую решает этот код.
Просто под обозначенную Вами схему решения подпадает много разных вариантов моделируемых ситуаций.
Они все основаны на объекте-посреднике (FHelper у Вас), но служат совершенно разным целям.
Вот "на вскидку", смотрите: паттерны Заместитель, Адаптер, Декоратор, Команда используют объект посредник, но решаемые задачи заметно отличаются. Да и реализации Шаблонного метода и Наблюдателя тоже часто опираются на объекты-посредники.
А я бы мог со своей стороны поделиться тем, как решаются такие задачи у нас.
Это запланировано, но... Чем больше реальных примеров - тем лучше :-)
" в этом есть некий "фан" :)"
Удалить-- да "фан" есть, когда другие люди "от сохи" делают примерно то же "что и ты".
"т.е. используется как некая имитация множественного наследования, что-ли.."
Удалить-- мне что-то подсказывает, что пройдёт год-другой и мы с Вами Николай сможем и примеси обсудить :-)
Которые пока всем показались не "комильфо".
А там может и АОП не на иньекциях, а на примесях :-)
Ни в коем случае не сочтите это менторством.
Может и я в свою очередь через год-два - в примесях разочаруюсь.
"Всё течёт, всё меняется".
А "если серьёзно", то "достаточно часто" - множественное наследование и примеси (как частный случай) - легко и элегантно заменяются делегированием, стратегией, фасадом и прочими "шаблонами проектирования от GoF".
Удалить"Достаточно часто", но не везде. Например RefCounting - это всё же примесь. Просто из соображений эффективности и "экономии на спичках".
Позволю процитировать себя - http://18delphi.blogspot.ru/2013/04/iunknown.html - "Собственная реализация IUnknown и подсчёт ссылок. И примеси"
Удалить"«С реальным примером из нашего проекта -- тяжело, слишком много кода.»
Удалить-- Жаль... :-("
-- жаль что мы "разбросаны по городам и весям", иначе могли бы устроить "фидопойку" и "обменяться опытом"...
ведь "на пальцах" - зачастую "быстрее объяснить"...
Но! Письменно - хотя и дольше, но качественнее, это надо признать.
"Было бы очень интересно ознакомиться с причинами, которые вызвали такое архитектурное решение."
Удалить-- NameRec, я могу конечно ошибаться, но по-моему это некая "замена" Вашему, "расширенному делегированию".
«NameRec, я могу конечно ошибаться, но по-моему это некая "замена" Вашему, "расширенному делегированию".»
Удалить-- Нет... Скорее наоборот. Я могу предоставить модуль, где решается аналогичная задача, немного в более общем виде, с другими соглашениями и с применением ED.
Обобщение состоит в том, что поля записи не обязаны находиться в едином буфере, с типизацией ситуация похожая - набор типов открыт для расширения, а доступ к значениям полей производится средствами ED.
Модуль применялся как прослойка для доступа к наборам данных HyTech из Python.
Я там по молодости реально переборщил с общностью, в частности, набор типов расширять не потребовалось (или потребовалось, но один раз, т.е. считайте - не потребовалось), а целый ряд характеристик полей из стремления к общности я сделал динамическими атрибутами, что тоже не ускорило решение :-) впрочем, если и замедлило, то незаметно.
ED было применено для отделения способа физического представления данных (как данные расположены и где - в RAM, в на диске при буферизации с LRU-своппингом или в наборе данных, в полях BDS/SAB (термины HyTech) или в полях потомка DB.DataSet) - за это отвечал поставщик данных...
Про "использование классовых методов" отчасти написано тут - http://habrahabr.ru/post/232955/.
ОтветитьУдалитьТот самый RmsShape и список RegisteredShapes, которые вызвали вопросы :-)
Всё же, я решил написать о решаемой задачи. Прошу прощения - сумбурно, наверное с ошибками, ибо уже 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 - представляет интерфейс для реализации вычисляемых полей (которые реализуются в наследниках).
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 часа ночи.»
Удалить-- Напротив, я убеждён, что у Вас получилось с чувством с толком и с расстановкой.
Мне, по крайней мере, многое стало понятно. В частности, Ваш "FPhisical" при описанном подходе выглядит вполне уместно.
Большое спасибо за контекст.
«На самом деле, я уже давно сомневаюсь, на сколько такой подход оправдан.»
-- За это - отдельное спасибо.
Не многие способны критически посмотреть на результаты своей работы и на то, к чему привыкли за много лет работы.
«Однако, если бы оно было по другому реализовано, то и генератор бы у меня был бы другим... (позволю себе оставить ссылку: http://www.delphinotes.ru/2011/07/blog-post_14.html)»
-- И ссылка Ваша совершенно к месту, поскольку без неё трудно было бы сообразить, для чего нужен генератор кода.
В общем (да и в частности) Вы создали два совершенно безупречных поста Николай.
Ещё раз благодарю.
Что-то я смотрю, что "разбередил" такую тему в которой и сам уже начинаю "терять мысль" :-)
УдалитьНеожиданно...
По существу. Как и обещал, расскажу "как у нас"...
УдалитьМы используем потомки DB.DataSet. Any/FreeDAC для многозвенной архитектуры, и kbmMW - для многозвенной.
Проблему, описанную в Вашей замечательной статье решили несколько иначе: у нас есть модули с константами имён полей, т.е. вручную их набирают при первом использовании, возможные ошибки в написании "отбиваются" при проверке и тестировании.
Причины, по которым мы не используем ORM-подход (это когда по метаданным генерируются классы для таблиц и полей, например):
* Приложение должно "уметь" работать с БД, полная структура которой неизвестна.
Действительно, относительно незначительная часть полей из общей совокупности обрабатывается бизнес-логикой, что даёт возможность как автоматического формирования форм по метаданным, так и обеспечить алгоритмы изменения данных в большинстве случаев совершенно автоматически.
* Структура БД может измениться, но приложение должно быть устойчиво к некритичным изменениям.
* Мы стремимся очень редко открывать таблицу с полным набором атрибутов - такое бывает только в формах ввода, а в недалёком будущем - даже в них BLOB-поля буду оказываться только по запросу. В такой ситуации класс, содержащий определения всех полей — избыточен, хотя понятно, можно добавить соответствующий признак наличия поля в курсоре.
Таблиц в БД предметной области ~ 618.
В целом то, что компилятор не проверяет наличие полей в таблице проблем не создаёт, хотя и накладывает определённые технологические требования.
В любом случае, пока не слышал о том, чтобы кто-то позиционировал это как проблему.
С другой стороны, вполне допускаю, что у Вас — другая ситуация.
Очень интересно было бы узнать, почему Вы решили пойти путём отказа от доступа к данным посредством потомков DB.TDataSet.
Не сомневаюсь, что разработка своего варианта модуля DB и DB-aware компонентов была достаточно затратной. Вероятно, были серьёзные причины пойти на этот шаг...
>> В любом случае, пока не слышал о том, чтобы кто-то позиционировал это как проблему.
УдалитьКонечно, это не проблема. Но у нас:
а) таблиц тоже больше 500. Имена их всех в голове и не удержишь
б) удобство при наборе кода - я начинаю набирать имя таблицы, жамкаю Ctrl+Space - и вижу варианты. Так легче вспомнить имя таблицы, чем отрываться из IDE в другое приложение. То же самое и с именами полей - ставлю точку после имени таблицы, Ctrl+Space - и все поля (в том числе и локальные - лукапы и вычисляемые) перед глазами. В общем суть в том, что Ctrl+Space значительно дешевле, чем Alt+Tab.
Есть ещё такое удобство - допустим у меня есть подозрение, что некое поле в некой таблице не используется (в рамках приложения). И имя этого поля какое-нибудь такое, которое используется в других таблицах. И, допустим, я хочу:
а) проверить, действительно ли оно нигде не используется
б) удалить и забыть.
Вот с кодогенератором всё просто - я удаляю объявление поля из XML, а дальше компилятор покажет, были ли ссылки на это поле из кода. Это быстрее, чем поиск по исходникам (хотя привычка сначала искать, а потом удалять, у меня до сих пор осталась).
>> Очень интересно было бы узнать, почему Вы решили пойти путём отказа от доступа к данным посредством потомков DB.TDataSet
На это я отвечу ссылкой.
Хотя на самом деле, не моя это была идея. Когда я пришёл в нашу компанию (2006 год), там уже свои наработки были. Примерно с 1998 года эти исходники потихоньку создавались. Я лишь уже развивал начатое, оптимизировал, "кодогенерил"..
А DB-aware компонентов мы не используем, используем стандартные компоненты и над ними есть обёртки. И это очень удобно - например вот и вот - достаточно было в одном месте прописать вызов этих "полезняшек"
Да уж ребят. Почитал я вас и понял, что у меня в проекте всё совсем несерьёзно.
УдалитьДаже задумался, а не сделать ли мне свой генератор классов по объектам в БД, чтобы автокомплит работал. Интересно, насколько удобно будет пользоваться IDE если там по классу на каждую таблицу, view, sp будет общим количеством в пару тысяч штук. Наверно не очень.
Не знаю... Мне конечно интересно, когда кто-то делает что-то такое, на что я не решился, но предложение генерировать классы для того, чтобы было удобно использовать IDE мне представляется несколько... м-м-м-м... избыточным, наверное...
УдалитьIMHO разного уровня проблема и решение.
Думаю, не так трудно разработать мастер для IDE, который позволит автоматически определять константы имён для интересующий полей и вставлять их в код после выбора из таблицы соединения, ассоциированного с проектом.
Но вообще-то, тема представляется мне несколько надуманной, поскольку имена полей из таблиц БД предметной области не единственное (а в нашем случае, даже не основное место), где эти имена используются. Например, имена полей массово упоминаются в формах ввода, отчётах и, в нашем случае, формах просмотра, которые пользователь может сам создавать.
Что касается удобства ссылки на поля таблиц в коде, то IMHO не такая уж это проблема, чтобы "огород городить"...
Разумеется, всё сказанное - мои личные ощущения, цена которым - ноль.