Подготовительные серии были тут - http://18delphi.blogspot.com/2013/07/1.html
Теперь хочется рассказать о практике создания "шаблонных" контейнеров в "стиле STL".
ПОКА - БЕЗ "настоящих" Generic'ов (http://18delphi.blogspot.ru/2013/03/generic-generic.html). Но и их - ТОЖЕ можно использовать. Но ПОКА - есть МНОЖЕСТВО НЕЗАКРЫТЫХ ошибок, которые Embarcadero почему-то не спешит закрывать. Минус ребятам. Что сказать. Даже мою ошибку (http://18delphi.blogspot.com/2013/05/resolved.html http://18delphi.blogspot.com/2013/05/xe4.html) - "типа исправили", но не закрыли. Хотя там - "копейки". Если я своим умом - всё правильно понимаю...
Одна ремарка. Несмотря на то, что в STL подобные контейнеры называются vector - я решил сохранить преемственность с Delphi и назвал их List.
Итак. Как обычно.
Модель:
Код:
List.imp.pas:
Теперь хочется рассказать о практике создания "шаблонных" контейнеров в "стиле STL".
ПОКА - БЕЗ "настоящих" Generic'ов (http://18delphi.blogspot.ru/2013/03/generic-generic.html). Но и их - ТОЖЕ можно использовать. Но ПОКА - есть МНОЖЕСТВО НЕЗАКРЫТЫХ ошибок, которые Embarcadero почему-то не спешит закрывать. Минус ребятам. Что сказать. Даже мою ошибку (http://18delphi.blogspot.com/2013/05/resolved.html http://18delphi.blogspot.com/2013/05/xe4.html) - "типа исправили", но не закрыли. Хотя там - "копейки". Если я своим умом - всё правильно понимаю...
Одна ремарка. Несмотря на то, что в STL подобные контейнеры называются vector - я решил сохранить преемственность с Delphi и назвал их List.
Итак. Как обычно.
Модель:
List.imp.pas:
{$IfNDef List_imp} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Библиотека "SandBox" // Модуль: "List.imp.pas" // Родные Delphi интерфейсы (.pas) // Generated from UML model, root element: Impurity::Class Shared Delphi Sand Box::SandBox::STLLike::List // // Абстрактный список значений // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// {$Define List_imp} PItemType = ^_ItemType_; const { Sizes } cItemSize = SizeOf(_ItemType_); type IndexType = System.Integer; _List_ = {mixin} class(_List_Parent_) {* Абстрактный список значений } private // private fields f_Data : Tl3PtrLoc; f_Count : IndexType; {* Поле для свойства Count} private // private methods procedure ReAllocList(aNewCapacity: IndexType); procedure CheckIndex(anIndex: IndexType); // can raise EListError {* проверяет валидность индекса и поднимает исключение, если он неправильный } function ItemSlot(anIndex: IndexType): PItemType; function ExpandSize(aTargetSize: IndexType): Cardinal; procedure CheckSetItem(anIndex: IndexType); // can raise EListError {* Проверяет валидность индекса при вставке } protected // property methods procedure pm_SetCount(aValue: IndexType); function pm_GetCapacity: IndexType; procedure pm_SetCapacity(aValue: IndexType); function pm_GetItems(anIndex: IndexType): _ItemType_; procedure pm_SetItems(anIndex: IndexType; const aValue: _ItemType_); protected // overridden protected methods procedure Cleanup; override; {* Функция очистки полей объекта. } public // public properties property Count: IndexType read f_Count write pm_SetCount; property Capacity: IndexType read pm_GetCapacity write pm_SetCapacity; property Items[anIndex: IndexType]: _ItemType_ read pm_GetItems write pm_SetItems; default; {* Элементы списка. } end;//_List_ {$Else List_imp} // start class _List_ procedure _List_.ReAllocList(aNewCapacity: IndexType); //#UC START# *51DEB8770017_51DEB07E03E4_var* var l_Cap : Integer; l_Cnt : Integer; //#UC END# *51DEB8770017_51DEB07E03E4_var* begin //#UC START# *51DEB8770017_51DEB07E03E4_impl* f_Data.SetSize(aNewCapacity * cItemSize); l_Cap := Self.Capacity; Assert(l_Cap >= aNewCapacity); l_Cnt := f_Count; if (l_Cap > l_Cnt) then System.FillChar(ItemSlot(l_Cnt)^, (l_Cap - l_Cnt) * cItemSize, 0); //#UC END# *51DEB8770017_51DEB07E03E4_impl* end;//_List_.ReAllocList procedure _List_.CheckIndex(anIndex: IndexType); // can raise EListError //#UC START# *51DEB95E00BD_51DEB07E03E4_var* procedure _Error; begin raise EListError.CreateFmt(SListIndexError + ' from (%d)', [anIndex, f_Count]) end; //#UC END# *51DEB95E00BD_51DEB07E03E4_var* begin //#UC START# *51DEB95E00BD_51DEB07E03E4_impl* if (anIndex < 0) or (anIndex >= f_Count) then _Error; //#UC END# *51DEB95E00BD_51DEB07E03E4_impl* end;//_List_.CheckIndex function _List_.ItemSlot(anIndex: IndexType): PItemType; //#UC START# *51DEBE2D008A_51DEB07E03E4_var* //#UC END# *51DEBE2D008A_51DEB07E03E4_var* begin //#UC START# *51DEBE2D008A_51DEB07E03E4_impl* Result := PItemType(f_Data.AsPointer + anIndex * cItemSize); assert(Result <> nil); //#UC END# *51DEBE2D008A_51DEB07E03E4_impl* end;//_List_.ItemSlot function _List_.ExpandSize(aTargetSize: IndexType): Cardinal; //#UC START# *51DEC11F0058_51DEB07E03E4_var* const cIncrArray : array [0..3] of Integer = (64 * 1024, 1024, 128, 4); cMaxForTwice : Integer = 1 * 1024 * 1024; var I : Integer; //#UC END# *51DEC11F0058_51DEB07E03E4_var* begin //#UC START# *51DEC11F0058_51DEB07E03E4_impl* Assert(aTargetSize > 0); Result := aTargetSize; if (Result > cMaxForTwice) then // большие массивы не удваиваем а подравниваем под 1мб Result := (aTargetSize div cMaxForTwice + 1) * cMaxForTwice else begin for I := 0 to High(cIncrArray) do if (aTargetSize > cIncrArray[I]) then begin Result := (aTargetSize div cIncrArray[I]) * cIncrArray[I] * 2; Break; end;//aTargetSize > cIncrArray[I] end;//Result > cMaxForTwic //#UC END# *51DEC11F0058_51DEB07E03E4_impl* end;//_List_.ExpandSize procedure _List_.CheckSetItem(anIndex: IndexType); // can raise EListError //#UC START# *51DECAA8035E_51DEB07E03E4_var* //#UC END# *51DECAA8035E_51DEB07E03E4_var* begin //#UC START# *51DECAA8035E_51DEB07E03E4_impl* CheckIndex(anIndex); //#UC END# *51DECAA8035E_51DEB07E03E4_impl* end;//_List_.CheckSetItem procedure _List_.pm_SetCount(aValue: IndexType); //#UC START# *51DEB1ED0017_51DEB07E03E4set_var* procedure SayBadCount(aNewCount: LongInt); begin raise EListError.CreateFmt(sListIndexError, [aNewCount]); end; var l_Ptr : PItemType; {$IfNDef l3Items_IsUnrefcounted} l_Index : Integer; {$EndIf l3Items_IsUnrefcounted} //#UC END# *51DEB1ED0017_51DEB07E03E4set_var* begin //#UC START# *51DEB1ED0017_51DEB07E03E4set_impl* if (aValue < 0) then SayBadCount(aValue); if (aValue < f_Count) then begin l_Ptr := ItemSlot(aValue); {$IfDef l3Items_IsUnrefcounted} System.FillChar(l_Ptr^, (f_Count - 1 - aValue) * cItemSize, 0); {$Else l3Items_IsUnrefcounted} for l_Index := aValue to f_Count - 1 do begin FreeItem(l_Ptr^); Inc(PMem(l_Ptr), cItemSize); end;//for i {$EndIf l3Items_IsUnrefcounted} end//aValue < f_Count else if (aValue > Self.Capacity) then ReAllocList(ExpandSize(aValue)); if (f_Count < aValue) then System.FillChar(ItemSlot(f_Count)^, (aValue - f_Count) * cItemSize, 0); f_Count := aValue; //#UC END# *51DEB1ED0017_51DEB07E03E4set_impl* end;//_List_.pm_SetCount function _List_.pm_GetCapacity: IndexType; //#UC START# *51DEB20E0130_51DEB07E03E4get_var* //#UC END# *51DEB20E0130_51DEB07E03E4get_var* begin //#UC START# *51DEB20E0130_51DEB07E03E4get_impl* Result := f_Data.GetSize div cItemSize; //#UC END# *51DEB20E0130_51DEB07E03E4get_impl* end;//_List_.pm_GetCapacity procedure _List_.pm_SetCapacity(aValue: IndexType); //#UC START# *51DEB20E0130_51DEB07E03E4set_var* procedure SayBadCap(aNewCapacity: IndexType); begin raise EListError.CreateFmt(sListIndexError, [aNewCapacity]); end; //#UC END# *51DEB20E0130_51DEB07E03E4set_var* begin //#UC START# *51DEB20E0130_51DEB07E03E4set_impl* if (aValue < 0) then SayBadCap(aValue); if (pm_GetCapacity <> aValue) then begin { If the list is shrinking, then update _Count for the smaller size. } if (aValue < f_Count) then Count := aValue; ReAllocList(aValue); end;//GetCapacity(Self) <> aValue //#UC END# *51DEB20E0130_51DEB07E03E4set_impl* end;//_List_.pm_SetCapacity function _List_.pm_GetItems(anIndex: IndexType): _ItemType_; //#UC START# *51DECA1202C5_51DEB07E03E4get_var* //#UC END# *51DECA1202C5_51DEB07E03E4get_var* begin //#UC START# *51DECA1202C5_51DEB07E03E4get_impl* CheckIndex(anIndex); Result := ItemSlot(anIndex)^; //#UC END# *51DECA1202C5_51DEB07E03E4get_impl* end;//_List_.pm_GetItems procedure _List_.pm_SetItems(anIndex: IndexType; const aValue: _ItemType_); //#UC START# *51DECA1202C5_51DEB07E03E4set_var* {$IfNDef l3Items_IsAtomic} var l_P : PItemType; {$EndIf l3Items_IsAtomic} //#UC END# *51DECA1202C5_51DEB07E03E4set_var* begin //#UC START# *51DECA1202C5_51DEB07E03E4set_impl* CheckSetItem(anIndex); {$IfDef l3Items_IsAtomic} PItemType(ItemSlot(anIndex))^ := aValue; {$Else l3Items_IsAtomic} l_P := PItemType(ItemSlot(anIndex)); if not IsSame(l_P^, aValue) then begin FreeItem(l_P^); FillItem(l_P^, aValue); end;//not IsSame(l_P^, anItem) {$EndIf l3Items_IsAtomic} //#UC END# *51DECA1202C5_51DEB07E03E4set_impl* end;//_List_.pm_SetItems procedure _List_.Cleanup; //#UC START# *479731C50290_51DEB07E03E4_var* //#UC END# *479731C50290_51DEB07E03E4_var* begin //#UC START# *479731C50290_51DEB07E03E4_impl* Count := 0; f_Data.SetSize(0); inherited; //#UC END# *479731C50290_51DEB07E03E4_impl* end;//_List_.Cleanup {$EndIf List_imp}UnrefcountedListPrim.imp.pas:
{$IfNDef UnrefcountedListPrim_imp} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Библиотека "SandBox" // Модуль: "UnrefcountedListPrim.imp.pas" // Родные Delphi интерфейсы (.pas) // Generated from UML model, root element: Impurity::Class Shared Delphi Sand Box::SandBox::STLLike::UnrefcountedListPrim // // Список значений без какого то бы ни было подсчёта ссылок // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// {$Define UnrefcountedListPrim_imp} {$Define l3Items_IsUnrefcounted} _List_Parent_ = _UnrefcountedListPrim_Parent_; {$Include List.imp.pas} _UnrefcountedListPrim_ = {mixin} class(_List_) {* Список значений без какого то бы ни было подсчёта ссылок } end;//_UnrefcountedListPrim_ {$Else UnrefcountedListPrim_imp} // start class _UnrefcountedListPrim_ function IsSame(const A: _ItemType_; const B: _ItemType_): Boolean; //#UC START# *51DECB820261_51DED02E0163_var* //#UC END# *51DECB820261_51DED02E0163_var* begin //#UC START# *51DECB820261_51DED02E0163_impl* Result := (A = B); //#UC END# *51DECB820261_51DED02E0163_impl* end;//IsSame type _List_R_ = _UnrefcountedListPrim_; {$Include List.imp.pas} {$EndIf UnrefcountedListPrim_imp}AtomicList.imp.pas:
{$IfNDef AtomicList_imp} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Библиотека "SandBox" // Модуль: "AtomicList.imp.pas" // Родные Delphi интерфейсы (.pas) // Generated from UML model, root element: Impurity::Class Shared Delphi Sand Box::SandBox::STLLike::AtomicList // // Список атомарных значений // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// {$Define AtomicList_imp} {$Define l3Items_IsAtomic} _UnrefcountedListPrim_Parent_ = _AtomicList_Parent_; {$Include UnrefcountedListPrim.imp.pas} _AtomicList_ = {mixin} class(_UnrefcountedListPrim_) {* Список атомарных значений } end;//_AtomicList_ {$Else AtomicList_imp} // start class _AtomicList_ procedure FillItem(var thePlace: _ItemType_; const aFrom: _ItemType_); forward; procedure FreeItem(var thePlace: _ItemType_); //#UC START# *51DEC20B01D7_51DED48301D9_var* //#UC END# *51DEC20B01D7_51DED48301D9_var* begin //#UC START# *51DEC20B01D7_51DED48301D9_impl* thePlace := _ItemType_(0); //#UC END# *51DEC20B01D7_51DED48301D9_impl* end;//FreeItem procedure FillItem(var thePlace: _ItemType_; const aFrom: _ItemType_); //#UC START# *51DECB440087_51DED48301D9_var* //#UC END# *51DECB440087_51DED48301D9_var* begin //#UC START# *51DECB440087_51DED48301D9_impl* thePlace := aFrom; //#UC END# *51DECB440087_51DED48301D9_impl* end;//FillItem type _UnrefcountedListPrim_R_ = _AtomicList_; {$Include UnrefcountedListPrim.imp.pas} {$EndIf AtomicList_imp}Код тут - http://sourceforge.net/p/rumtmarc/code-0/19/tree/trunk/Blogger/SandBox и http://sourceforge.net/p/rumtmarc/code-0/19/tree/trunk/Blogger/SandBoxTest
NameRec:
ОтветитьУдалитьВызывает уважение.
Особенно, если учесть, что код сгенерирован из UML-диаграммы.
1. Правильно ли я понимаю, что ваш (Вашей организации) инструмент UML-моделирования поддерживает возможность при определении методов в классе на UML-модели сразу же указывать код реализации?
На эту мысль наводят пары #UC START# - #UC END#, ну и группировка локальных процедур в #UC START# *..._var* - #UC END# *..._var*.
Или, реализация методов лежит отдельно от UML-диаграммы?
2. Относительно реализации контейнеров. Вы не смотрели SDL/DeCAL - библиотеке, о которой рассказывал (http://gurin.tomsknet.ru/delphidecal.html) Уважаемый мною Сергей Гурин?
"Вызывает уважение."
УдалитьСпасибо :-)
"Или, реализация методов лежит отдельно от UML-диаграммы?"
И да и нет :-) В данном случае "пары #UC START# - #UC END#" - это как раз "маркеры" - "вот сюда впишите код".
"Относительно реализации контейнеров. Вы не смотрели SDL/DeCAL"
УдалитьСтыдно признаться. Но. Я о ней - не знал.
Посмотрю внимательно.
Но вот это мне знакомо:
TObject0 = class
public
I: Integer;
constructor Create(aI: Integer);
{$IFDEF TTUSEPOOL}
class function NewInstance : TObject; override;
procedure FreeInstance; override;
{$ENDIF}
end;
TTUSEPOOL !!!!!
-- я САМ такое тоже придумал :-)
«В данном случае "пары #UC START# - #UC END#" - это как раз "маркеры" - "вот сюда впишите код".»
Удалить-- Таким образом, из UML генерируется структура класса, GUID в #UC START# *...*, как я понимаю, идентификатор метода в UML-редакторе? Вероятно, назначается автоматически...
Тэги #UC START# - #UC END# используются при генерации кода из UML-модели при её (модели) изменении для того, чтобы не потерять существующую реализацию. Я правильно понимаю?
«TTUSEPOOL !!!!!
-- я САМ такое тоже придумал :-)»
-- Да, там есть условно-поключаемая реализация хранения служебных структур в своём собственном пуле RAM. Мы отключаем - FastMM работает достаточно эффективно...
Кстати, есть порт DeCAL к XE-версиям Delphi.
"как я понимаю, идентификатор метода в UML-редакторе? Вероятно, назначается автоматически..."
УдалитьИменно. Автоматически.
"Тэги #UC START# - #UC END# используются при генерации кода из UML-модели при её (модели) изменении для того, чтобы не потерять существующую реализацию. Я правильно понимаю?"
Всё правильно.
Одна ремарка. Бывает (если это описано в шаблоне генерации), что код - тоже "сам" генерируется. Тогда как раз он тегами - не окружается.
"Да, там есть условно-поключаемая реализация хранения служебных структур в своём собственном пуле RAM"
Знакомо :-)
"Мы отключаем - FastMM работает достаточно эффективно..."
Я знаю :-) FastMM - хорош.
"Кстати, есть порт DeCAL к XE-версиям Delphi."
Спасибо. Обязательно посмотрю. А что там с 64-битами? Не знаете случайно?
NameRec:
Удалить«Одна ремарка. Бывает (если это описано в шаблоне генерации), что код - тоже "сам" генерируется. Тогда как раз он тегами - не окружается.»
-- На шаблоны я обратил внимание.
Как я понимаю, это ваш (Вашей организации) собственный DSL.
Пока могу только догадываться, в каких случаях требуются шаблоны и чем мотивировано их появление. Вероятно, это вызвано желанием определить специальные способы трансляции отношений (зависимость, агрегирование) для разных UML-диаграм. Надеюсь, Вы найдёте время раскрыть эту тему.
«"Да, там есть условно-поключаемая реализация хранения служебных структур в своём собственном пуле RAM"
Знакомо :-)»
-- Да, такое решение относительно часто встречается. Другое дело, я никогда не находил в нём особенной пользы, поскольку работать оно начинает на большом количестве данных, т.е. в случаях, когда уже уместно применить простую СУБД (например, SQLite).
«"Мы отключаем - FastMM работает достаточно эффективно..."
Я знаю :-) FastMM - хорош.»
-- Он кстати, доступен со времён Delphi 7, если не ошибаюсь...
«"Кстати, есть порт DeCAL к XE-версиям Delphi."
Спасибо. Обязательно посмотрю. А что там с 64-битами? Не знаете случайно?»
-- Здесь - не уверен. Хотя модуль небольшой, "хоккея" там практически нет :-) Это не какой-нибудь code injection :-)
Я не думаю, что XE-реализацию будет сложно портировать на 64-разряда, хотя и утверждать не возьмусь, поскольку соответствующего опыта не имею...
"Как я понимаю, это ваш (Вашей организации) собственный DSL.
УдалитьПока могу только догадываться, в каких случаях требуются шаблоны и чем мотивировано их появление. Вероятно, это вызвано желанием определить специальные способы трансляции отношений (зависимость, агрегирование) для разных UML-диаграм. Надеюсь, Вы найдёте время раскрыть эту тему."
Именно так. Надеюсь - найду.
"Да, такое решение относительно часто встречается. Другое дело, я никогда не находил в нём особенной пользы, поскольку работать оно начинает на большом количестве данных, т.е. в случаях, когда уже уместно применить простую СУБД (например, SQLite)."
У меня была реальная необходимость.
"Он кстати, доступен со времён Delphi 7, если не ошибаюсь..."
Тогда я его к сожалению не использовал "промышленно".
"Здесь - не уверен. Хотя модуль небольшой, "хоккея" там практически нет :-) Это не какой-нибудь code injection :-)
Я не думаю, что XE-реализацию будет сложно портировать на 64-разряда, хотя и утверждать не возьмусь, поскольку соответствующего опыта не имею..."
Я вот как раз и надеюсь на "отсутствие хоккея" и что всё будет хорошо. Другое дело, что у меня СОБСТВЕННАЯ инфраструктура (контейнеров в частности) - посему не факт, что удастся быстро и безболезненно "скрестить ежа с ужом".
"собственный DSL"
УдалитьПока одно могу сказать, что его можно править в процессе редактирования самой прикладной модели. Через UML.
Заводить новые стереотипы и отношения между ними. Видоизменять шаблоны генерации.
Т.е. DSL - "растёт" вместе с прикладными проектами.
Т.е. "по ходу пьесы" - можно выделять "шаблонные решения". Что я активно и делаю.
"Это не какой-нибудь code injection"
УдалитьВы про "вызов локальных функций"? :-)
NameRec:
Удалить«"...Надеюсь, Вы найдёте время раскрыть эту тему."
Именно так. Надеюсь - найду.»
-- Было бы очень интересно.
Хотелось бы также услышать от Вас причины, приведшие к необходимости разработки собственного DSL для кодогенерации. Этот DSL показался мне излишне криптованым, понятно, что XML"многословен", но IMHO он и знаком большему количеству людей (что снижает "порог входа") и, на мой взгляд, несколько более структурирован.
Кроме того, в случае, можно было бы попробовать некий "симбиоз" XML-описания с LUA/Python императивом.
Возможно, это всё выглядело бы не столь кратко, но возможно, чуть более понятно.
Хотя, говорить это меня подталкивает, скорее, личный опыт, нежели точное осознание задач, решаемых кодогенерацией.
На эту тему хотелось бы получить Ваши комментарии.
«"Да, такое решение относительно часто встречается. Другое дело, я никогда не находил в нём особенной пользы, поскольку работать оно начинает на большом количестве данных, т.е. в случаях, когда уже уместно применить простую СУБД (например, SQLite)."
У меня была реальная необходимость.»
-- Если не секрет, поделитесь хотя бы типажом задачи.
Интересно уловить ситуации, когда потребуются, скажем, массивы (или списки) долиной порядка 100 000 элементов.
«Другое дело, что у меня СОБСТВЕННАЯ инфраструктура (контейнеров в частности) - посему не факт, что удастся быстро и безболезненно "скрестить ежа с ужом".»
-- Это-то как раз, понятно :-)
«"собственный DSL"
Пока одно могу сказать, что его можно править в процессе редактирования самой прикладной модели. Через UML.»
-- Вы говорите о шаблонах, которые используются для кодогенерации и обеспечивают раскрытие специфики конкретной диаграммы? Иля я Вас неправильно понял?
«Заводить новые стереотипы и отношения между ними. Видоизменять шаблоны генерации.»
-- Тема стереотипов и их отображения в код, IMHO самая существенная часть Вашей методологии. Было бы очень интересно, если бы Вы осветили эту тему.
«Т.е. DSL - "растёт" вместе с прикладными проектами.»
-- Вероятно, под DSL Вы понимаете сейчас чуть ли не саму совокупность UML-диаграм, описывающих конкретный проект. Иля я понял Вас неверно?
«Т.е. "по ходу пьесы" - можно выделять "шаблонные решения". Что я активно и делаю.»
-- Не ошибусь ли я, если предположу, что:
1. "Шаблонные решения" привязаны к конкретной диаграмме или, возможно, к семейству диаграмм?
2. Необходимость в выделении "шаблонных решений" вызывается потребностью конкретизировать отношения между элементами диаграмм? Если так, то какие отношения Вы находите необходимым специализировать шаблонами?
«"Это не какой-нибудь code injection"
Вы про "вызов локальных функций"? :-)»
-- Нет :-)
Про code injection я упомянул как о некоей разновидности "шаманства", грозящей сложностями с адаптацией при смене версии Delphi или, уж тем более, разрядности платформы.
К распространённому (в своё время) примеру такого "шаманства" я отношу реализацию итераторов в Turbo Vision, от которой нам проще было отказаться, чем адаптировать её для 32-разрядного Delphi.
""Шаблонные решения" привязаны к конкретной диаграмме или, возможно, к семейству диаграмм?"
УдалитьШаблонные решения привязываются к стереотипу.
"примеру такого "шаманства" я отношу реализацию итераторов в Turbo Vision"
ОтветитьУдалитьhttp://18delphi.blogspot.ru/2013/03/blog-post_5929.html
;-)
NameRec: Разумеется, моё упоминание итераторов Turbo Vision было неслучайным ;-)
Удалить