Старый конь…. или обращение к head-hunter-ам

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

Импортозамещение в сети

Хочу рассказать о проекте, в котором я участвовал с 2010 по 2014 г.

Это некий аналог Skype , но более приспособленный для видео-конференций и дистанционного обучения.

Моей задачей было участие в разработке общей архитектуры и написание back-end сервера.

На самом деле серверов было 3.

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

Медиа-сервер отвечал за трансляцию аудио/видео потоков.

Платежный сервер позволял производить пополнение счета пользователей большинством известных способов, как с использованием карт, так и онлайн платежных сервисов.

Теперь чуть подробнее. Ну, поскольку проект является коммерческим, и я давал «подписку о неразглашении», все секреты раскрывать не имею права… Расскажу о некоторых решениях, может и нестандартных, которые позволили реализовать проект. Ну и конечно, о моих любимых паттернах проектирования, которые были там применены (в продолжении давно начатого цикла).

Основной упор при разработке сервера был на его защищенность, для чего был разработан оригинальный протокол с шифрованием трафика. Все взаимодействие клиента с сервером делалось через удаленный вызов процедур, ну и конечно, речи не шло о передаче по сети SQL-запросов :).

Короткие сжатые запросы с пересылкой номера команды и упакованного массива аргументов гарантировали высокую степень защищенности. Были и другие решения для защиты от Ddos-атак, показавшие эффективность при тестировании…

База в облаке. Часть 3. Что предлагается.

1. Отказаться от RationalRose. Либо свой UML-редактор, либо существующий, но с доработками — возможностью редактирования поведения объектов. Например — WhiteStarUML (если есть исходные коды). Выигрыш во времени с доработкой существующего очевиден.


2. Хранение модели и БД перенести в облако. Для этого необходим собственный веб-сервер (написан уже на 80%).

Это позволит позиционировать платформу как «более продвинутую» CMS, но для создания не сайтов,  а информационных систем. Вот такой продукт, IMHO, будет продаваться :).

База в облаке. Часть 2. Что получилось.

   В период с 2004 по 2009 г. я очень плотно занимался этим проектом (рабочее название FastBase).

   Был написан деск-топный вариант. Использовались компоненты компании BoldSoft (компания была куплена корпорацией Borland), к сожалению, даже не нашел ссылки на ее сайт.

   Для разработки UML-модели применялся редактор RationalRose, для которого BoldSoft и разработала плагин. Основной особенностью компонента было то, что он позволял, во-первых, генерировать структуру БД, во-вторых, внести объекты UML-модели в объектное пространство приложения, и оперировать с ними как с классами Дельфи.

   Сначала немного о терминах:
 

База в облаке. Часть 1. Что хочется.

                  Новая парадигма проектирования приложений

   Большинство предлагаемых сред разработки приложений ориентировано на парадигму «форма приложения -> элемент управления -> свойства элемента управления –> объект модели». При этом элемент управления (окно ввода, сетка и т.п.) может быть привязан к объектам модели. Разработчик настраивает свойства элемента управления для достижения необходимой функциональности (представление объекта, события при его изменении, проверки введенной информации …). Как правило, бизнес-логика (свойства и поведение объектов модели) реализуется на сервере БД в виде триггеров и хранимых процедур, или в кодеприложения. При этом приложение часто состоит из множества форм с однотипными элементами управления, на которых приходится повторять рутинную процедуру их настройки. При изменении свойств объекта модели необходимо менять свойства всех элементов управления, связанных с объектом.
В результате «классический» способ разработки приложений и информационных систем имеет множество ограничений. При изменении бизнес-логикиили набора атрибутов объекта необходимо менять структуру БД (не потеряв при этом введенные данные) и кодпрограммы, свойства элементов управления (полей ввода/редактирования и т.д.), что приводит к необходимости многократно переписывать и отлаживать приложение. Такой подход затрудняет модификацию и развитие информационной системы.

Предлагается новая парадигма – «объект модели -> свойства объекта-> свойства элемента управления». Она является дальнейшим развитием технологии Model Driven Architecture (MDA) — (архитектура, управляемая моделью). От «классического» способа разработки данная парадигма отличается более высоким уровнем «абстракции» — во главу угла ставятся свойства объекта модели, а не элементов управления, связанных с объектом. Основное положение — модель приложения (описание структуры БД на языке UML) и поведениеобъектов (то есть бизнес-логика) отделеныот кода приложения.
На базе этой парадигмы планируется создать платформу разработки информационных систем
Что хочется получить:
  • Для разработки информационной системы достаточно спроектировать структуру БД на языке UML. 
  • Настройка свойств объектов, не входящих в спецификацию языка UML, производится в визуальном редакторе модели. 
  • Большинство стандартных операций должно быть уже встроено в платформу, для более детального описания — «мастера» (визарды), облегчающие работу с платформой. 
  • После описания свойств объектов модели приложение должно быть готово к работе.
Должен получится продукт не для программиста, а для конечного пользователя (конечно, достаточно продвинутого).

Все формы ввода/отображения объектов строятся динамическисогласно информации о свойствах и поведении объектов в модели — исполняющий модуль приложения не зависит от структуры БД и бизнес-логики (подобно тому, как Internet Explorer не зависит от html-страниц — и отображает ее независимо от содержащихся в ней данных).
Преимущества такой технологии:

ООП и паттерны проектирования: практическое применение.

ООП и паттерны проектирования: практическое применение.

В Интернете выложено достаточно большое количество статей на тему применения паттернов (шаблонов) проектирования. К сожалению, примеров кода на Delphi немного, да и сами примеры достаточно упрощенные, и не дают возможность оценить всю мощь использования этой технологии. Предлагаемый цикл статей – попытка показать начинающим программистам пользу применения паттернов проектирования на реальном коммерческом проекте, пошагово решая часто возникающие практические задачи.
Это не обзор паттернов, и не учебник по их применению, это – практические занятия. Надеюсь, их получится несколько.

Занятие 1. Чтение настроек программы.
Постановка задачи: программа при начальном запуске должна прочитать настройки подключения к базе данных и соединиться с ней.
Такую задачу решал, пожалуй, каждый программист, и в любом учебнике программирования Вы легко найдете решение:

procedure ConnectDataBase;
var
IniFile : TIniFile;
Section, FileName, Password, UserName : String;
Begin
IniFile := TIniFile.Create(IniFilePath);
Section := 'Main';
with IniFile do begin
FileName:= ReadString(Section,'FileName','Application.gdb');
Password:= ReadString(Section,'Password','masterkey');
UserName:= ReadString(Section,'UserName','SYSDBA');
end;
MainDatabase.Params.Clear;
MainDatabase.Params.Add('password='+ Password);
MainDatabase.Params.Add('user_name='+'UserName')
MainDatabase.FileName := FileName;
MainDatabase.Connected := true;
end;

Вроде все легко и просто?
Тогда зададим себе несколько вопросов…

А если у нас несколько подключаемых БД, да еще и с разными вариантами подключения?

А если мы хотим добавить новые параметры настройки без перекомпиляции кода?

А если мы решим использовать для сохранения настроек не Ini-файл, а реестр, или cookie?

И мы видим, что предлагаемое решение нарушает несколько принципов объектно-ориентированного программирования!

Первое нарушение – решение не приспособлено к изменениям требований (а требования к программе меняются всегда!).

Второе нарушение — нарушение принципа «разделения ответственностей» – процедура выполняет одновременно несколько обязанностей: хранит параметры настройки (FileName, Password, UserName), читает их из файла и производит подключение к БД.
Рассмотрим эти вопросы по очереди. Если у нас несколько БД – необходимо хранить отдельно настройки для каждой.Можно в одном ini-файле в разных секциях, но лучше – для каждой БД создать свой файл.

В моей программе – две базы данных, БД проекта, и БД приложения. Соответственно, будет четыре файла – ApplicationLocalConnection.ini и ApplicationRemoteConnection.ini для приложения, ProjectLocalConnection.ini и ProjectRemoteConnection.ini для проекта.
А чтобы при запуске узнать, какие соединения использовать – еще и главный ini-файл для приложения, тут удобнее использовать название ini-файла, совпадающее с названием приложения. А поскольку одни и те же настройки могут использовать разные программы, для каждой надо создать свой каталог для хранения настроек:
Bin (тут лежат сами exe-файлы)
Application1.LocalSettings (тут лежат настроечные файлы приложения 1)
Application2.LocalSettings (тут лежат настроечные файлы приложения 2)

Второй вопрос – параметры настройки… Чтобы менять настройки без перекомпиляции кода, параметры надо хранить отдельно в виде списка строк, и регистрировать при создании экземпляра класса. Тут есть два пути – можно создать один общий класс хранения настроек, и параметризовать его списком переменных, либо использовать полиморфизм, и создать несколько классов по типам настроек.

Третий вопрос — сохранения настроек различными способами (в Ini-файле, в реестр, cookie или где-то еще).

Вот тут мы используем один из главных принципов объектно-ориентированного программирования – «инкапсулируй то, что может измениться». У нас может измениться способ чтения-сохранения настроек, так «спрячем» возможные изменения за «стеной» абстракции – создадим абстрактный класс TSettingsReader – «читатель» настроек. Он описывает интерфейс для чтения и сохранения настроек без конкретизации — как это делается.

Второй принцип, который мы применим – принцип «разделения ответственностей». Каждый класс должен выполнять только то, что нужно, и ничего лишнего. И для выполнения разных обязанностей нам необходимо создать 3 класса:

— TSettingsHolder. Класс для хранения параметров настройки. Он знает — что хранить.

— TSettingsReader. Класс для чтения и сохранения их. Он знает — как хранить.

— Класс для подключения к БД. Он знает — как использовать настройки.

Как видим, каждый класс занимается своим делом и не зависит от других. Более того, все эти классы – абстрактные, а использовать мы будем конкретных наследников!

Внимательный читатель вправе задать вопрос: а где же паттерны проектирования? А они – в реализации использования наших абстрактных классов! Обратимся к классикам – к «банде четырех» (Э.Гамма, Р.Хелм, Р.Джонсон, Дж.Влиссидес. Приемы объектно-ориентированного проектирования)!
Рассмотрим описание паттерна проектирования «Мост» (Bridge).
Применимость: используйте паттерн «Мост», когда:
— хотите избежать постоянной привязки абстракции к реализации. Так, например, бывает, когда реализацию необходимо выбирать во время выполнения программы.
— и абстракции, и реализации должны расширяться новыми подклассами. В таком случае паттерн «Мост» позволяет комбинировать разные абстракции и реализации и изменять их независимо.
— изменения в реализации абстракции не должны сказываться на клиентах, то есть клиентский код не должен перекомпилироваться. — вы хотите скрыть от клиентов реализацию абстракции.

А это как раз наш случай! И вот как выглядит диаграмма для нашей конкретной реализации паттерна:

Реализация паттерна Bridge

Каковы основные результаты применения паттерна?
— отделение реализации от интерфейса. Реализации больше не имеет постоянной привязки к интерфейсу. Реализацию абстракции можно конфигурировать во время выполнения. Разделение классов TSettingsHolder и TSettingsReader устраняет зависимость от их реализации.
— повышение степени расширяемости. Можно расширять независимо иерархии классов TSettingsHolder и TSettingsReader.

Вот теперь – первый пример кода:

Класс TSettingsReader – «читатель» настроек. Описывает абстрактный интерфейс для чтения и сохранения настроек.

Для возможности добавлять параметры настройки без перекомпиляции кода они создаются динамически при инициализации,и названия параметров хранятся в отдельном IniKeys : TStringList и регистрируются в нем при создании конкретного класса.

type TSettingsReader = class
private
fIniSection: String;
fOwnerName: String;
fIsReadOnly: Boolean;
fSourcePath: String;
fDriveType : Word;
fDriveTypeDescription : String;
fIniKeys: TStringList;
procedure SetSourcePath(const Value: String);
function CheckKeyName(const KeyName: string): Boolean;
 
protected
property IniKeys : TStringList read fIniKeys;
public
class function GetStorageType: String;virtual;abstract; // В наследниках выдает способ сохранения параметров
function CheckExists: Boolean; virtual;abstract; // Проверка наличия файла или ветки реестра
procedure RegisterIniKey(const KeyName, DefaulValue : String);// Процедура регистрации параметра настройки
function SectionExists(const Section: string): Boolean; virtual;abstract;// Проверка наличия секции
property IniSection: String read fIniSection write fIniSection; // Секция хранения
property SourcePath: String read fSourcePath write SetSourcePath; // Путь к ini-файлу или ветке реестра
property IsReadOnly: Boolean read fIsReadOnly; // Доступ к записи файла (программа может быть на CD-Rom)
property DriveType : Word read fDriveType; // Тип устройства хранения
property DriveTypeDescription: String read fDriveTypeDescription; // Описание устройства хранения
property OwnerName: String read fOwnerName write fOwnerName; // Имя владельца - хранителя
constructor Create;
destructor Destroy;override;
 
function Load:Boolean;virtual; // Функция загрузки параметров из файла в IniKeys
function Update:Boolean; virtual; abstract; // Функция сохранения параметров в файле
 
function GetIniValue(const KeyName : string): string;
procedure SetIniValue(const KeyName, Value : string);
end;
 
implementation

constructor TSettingsReader.Create;
begin
inherited;
fIniKeys := TStringList.Create;
fIniKeys.CaseSensitive := False;
end;
 
destructor TSettingsReader.Destroy;
begin
IniKeys.Free;
inherited;
end;
 
function TSettingsReader.CheckKeyName(const KeyName: string): Boolean;
begin
Result := (IniKeys.IndexOfName(KeyName)>=0);
if not Result then
MsgError('['+Self.IniSection+'] '+KeyName+' not registered!', Self.OwnerName+'-GetIniValue');
end;
 
function TSettingsReader.GetIniValue(const KeyName: string): string;
begin
if CheckKeyName(KeyName) then
Result := Trim(IniKeys.Values[KeyName])
end;
 
function TSettingsReader.Load: Boolean;
begin
Result := CheckExists; // В базовом классе – только проверка. В наследниках будет добавлена сама процедура чтения.
end;
 
procedure TSettingsReader.RegisterIniKey(const KeyName, DefaulValue: String);
begin
IniKeys.Add(KeyName+'='+DefaulValue);
end;
 
procedure TSettingsReader.SetIniValue(const KeyName, Value: string);
begin
if CheckKeyName(KeyName) then
IniKeys.Values[KeyName] := Value;
end;
 
procedure TSettingsReader.SetSourcePath(const Value: String);
begin
fSourcePath := Value;
fDriveType := CheckDriveType(Value);
case fDriveType of
DRIVE_FIXED : fDriveTypeDescription := 'Fixed drive';
DRIVE_REMOTE : fDriveTypeDescription := 'Remote (network) drive';
DRIVE_CDROM : fDriveTypeDescription := 'CD-ROM drive';
DRIVE_RAMDISK : fDriveTypeDescription := 'RAM disk';
DRIVE_REMOVABLE : fDriveTypeDescription := 'Removable drive (USB)';
end;
fIsReadOnly := (fDriveType = DRIVE_CDROM); //Надо определить, не является-ли диск CD-диском?
end;
 
function CheckDriveType(const FilePath: String): Word;
var
FileDrive : string;
begin
FileDrive := ExtractFileDrive(FilePath);
Result := GetDriveType(PChar(FileDrive));
end;
 

Как видим, класс  TSettingsReader содержит две виртуальные функции Load и Update, которые, будучи перекрыты в наследниках, определяют способ чтения и сохранения настроек. В то же время класс имеет всю базовую функциональность для хранения настроек.

Класс TSettingsReader  – абстрактный, и конкретная реализация методов перекрыта в наследниках:
— TFBIniFileReader — «читатель» ini-файла

— TFBRegistryReader — «читатель» реестра

— TFBCookieReader — «читатель» файла cookie

Соответствующий экземпляр класса будет создаваться в процессе загрузки программы в зависимости от настроек.

Вот пример реализации класса для чтения из ini-файла (я намеренно убрал все проверки и реакцию на ошибки чтения/записи, чтобы не загромождать листинг, полный код можно получить по ссылке):


type TFBIniFileReader = class (TSettingsReader)
private
fIniFile: TIniFile;
function GetIniFile: TIniFile; // "Отложенное" создание Ini-File - когда понадобится!
public
class function GetStorageType: String; override;
constructor Create;
destructor Destroy; override;
function CheckExists: Boolean; override;
function SectionExists(const Section: string): Boolean; override;
function Load:Boolean; override;
function Update:Boolean; override;
property IniFile: TIniFile read GetIniFile;
end;
 
implementation;
class function TFBIniFileReader.GetStorageType: String;
begin
Result := 'IniFile';
end;
 
function TFBIniFileReader.CheckExists: Boolean;
begin
Result := FileExists(SourcePath);
end;
 
constructor TFBIniFileReader.Create;
begin
inherited;
end;
 
destructor TFBIniFileReader.Destroy;
begin
inherited;
fIniFile.Free;
end;
 
function TFBIniFileReader.Load: Boolean;
//При чтении из ini-файла при отсутствии значений им присваиваются значения по-умолчанию!
var
i: Integer;
KeyName , KeyValue, DefaultValue : string;
begin
Result := inherited Load;
if not Result then
Exit;
try
with IniFile do begin // Мы обращаемся к свойству IniFile, сам файл будет открыт в момент обращения!
for i := 0 to IniKeys.Count - 1 do begin
KeyName := IniKeys.Names[i];
DefaultValue := IniKeys.ValueFromIndex[i];
KeyValue := ReadString(IniSection, KeyName, DefaultValue);
if Trim(KeyValue) = '' then
KeyValue := DefaultValue;
IniKeys.ValueFromIndex[i] := KeyValue;
end;
Result := true;
end;
except // … реакция на ошибки чтения/записи
end;
end;
 
 
function TFBIniFileReader.Update: Boolean;
var
i: Integer;
KeyName : string;
ActualValue : string;
begin
Result := True;
if IsReadOnly then begin
MsgError('Путь: '+Self.SourcePath+#10+'Ошибка - '+ExtractFileDrive(Self.SourcePath)+
' ('+Self.DriveTypeDescription+') - только для чтения!', 'Невозможно записать ini-файл');
Exit;
end;
BackUpFile(Self.SourcePath);
try
with IniFile do begin
for i := 0 to IniKeys.Count - 1 do begin
KeyName := IniKeys.Names[i];
ActualValue := IniKeys.ValueFromIndex[i];
WriteString(IniSection, KeyName, ActualValue);
end;
UpdateFile;
end;
except //… реакция на ошибки чтения/записи
end;
end;
 
function TFBIniFileReader.GetIniFile: TIniFile; // "Отложенное" создание Ini-File - когда понадобится!
begin
if not Assigned(fIniFile) then
try
fIniFile := TIniFile.Create(Self.SourcePath);
except //… реакция на ошибки чтения/записи
end;
Result:=fIniFile;
end;
 
 
 
Опишем класс TCustomSettingsHolder, который содержит абстрактный интерфейс для хранителя настроек.
 
type TCustomSettingsHolder = class
private
fSettingsReader: TSettingsReader;
procedure SetSettingsReader(const Value: TSettingsReader);
public
class function GetConnectionType: String;virtual;abstract;//Метод класса, выдает описание типа соединения c БД
class function GetDescription : String;virtual;abstract;//Метод класса, выдает описание подключенной БД
property SettingsReader: TSettingsReader read fSettingsReader write SetSettingsReader; //Читатель настроек
end;
 
implementation
procedure TCustomSettingsHolder.SetSettingsReader (const Value: TSettingsReader);
//Как видим, хранитель настроек ссылается на абстрактный класс читателя настроек, что позволяет при инициализации
//приложения назначить конкретного наследника класса!
begin
fSettingsReader := Value;
fSettingsReader.OwnerName := Self.GetDescription;//Чтобы хранитель мог при ошибке сообщить, из какого соединения он вызван
end;


Класс TFBSettingsHolder (наследник TCustomSettingsHolder)реализует функциональность по чтению и сохранению настроек.
 
type TFBSettingsHolder = class(TCustomSettingsHolder)
public
// Начальная инициализация - создание параметров настроек.Функция должна вызываться после установки SettingsReader!
procedure Initialize; virtual; abstract;
// Чтение настроек из файла.
function ReadParameters:Boolean;virtual;
end;

От него унаследованы классы TMainConnection, TApplicationLocalConnection, TApplicationRemoteConnection,
TProjectLocalConnection, TProjectRemoteConnection, которые отличаются реализацией процедуры инициализации.
Для примера приводятся только процедуры для настроек самого приложения и локального подключения к БД:
 
procedure TMainConnection.Initialize;
begin
with Self.SettingsReader do begin
IniSection:='Main';
RegisterIniKey('SettingsStorage', 'ini'); // Способ хранения настроек
RegisterIniKey('Application', 'ApplicationLocalConnection'); // Способ подключения к БД приложения
RegisterIniKey('Project', 'ProjectLocalConnection'); // Способ подключения к БД проекта
RegisterIniKey('RootDir', 'C:Program FilesFastBase'); // Корневой каталог программы
RegisterIniKey('TempDir', 'C:Program FilesFastBaseTMP');
RegisterIniKey('LogDir', 'C:Program FilesFastBaseLog');
RegisterIniKey('WebServer', 'WebServerLocalConnection');
RegisterIniKey('RememberUser', 'no');
RegisterIniKey('Login', 'no');
RegisterIniKey('Category', 'no');
end;
end;
 
procedure TApplicationLocalConnection.Initialize;
begin
with Self.SettingsReader do begin
IniSection:='ApplicationLocalConnection';
RegisterIniKey('WorkDirectory', 'c:Program FilesFastBaseDB');
RegisterIniKey('FileName', 'Application.gdb');
RegisterIniKey('Password', 'masterkey');
RegisterIniKey('UserName', 'SYSDBA');
RegisterIniKey('ServerName', 'localhost');
end;
end;

Как видим, параметры настроек для разных классов соединения различаются, причем легко добавить либо изменить их без внесения правок в отлаженный код. Чтение настроек производится вызовом функции GetIniValue(IniKey).
На следующем занятии мы рассмотрим применение паттерна «Абстрактная фабрика».

Очень мне понравилось работать с таким замечательным компонентом, как Fast Report!
Задача — дать возможность пользователям визуально настроить внешний вид чека, печатаемого на кассовом аппарате. И чтобы можно было использовать выражения
и запросы к БД. Решил проблему за 2 часа!
Производится настройка путем создания отчетной формы Fast Report, в поля которого занесены команды управления фискальным регистратором. Применение такого способа позволяет очень гибко настроить внешний вид чека, а так же производить вычисления арифметических и логических выражений прямо в форме.
При печати в отчетную форму передаются данные текущего чека. После формирования отчета вывод его на экран не производится, а делается экспорт отчета в текстовый файл во временный каталог. После этого файл обрабатываются программой печати чека с разбором введенных команд, вычисленных выражений и формата строк.

Применение tagged values в БД

   Применение стандартных методов проектирования баз данных не всегда удовлетворяет запросы программиста. Одно из ограничений – невозможность изменить набор атрибутов объекта после создания БД, что, при постоянно изменяемых требованиях заказчика, приводит к необходимости менять структуру БД (не потеряв при этом введенные данные), менять и заново отлаживать код программы, поля ввода/редактирования и т.д.
   Вашему вниманию предлагается один из способов решения этой проблемы — применение tagged values (TV , тэг-значений, или именованных значений).  
Сами значения хранятся в БД в строковом формате, но могут интерпретироваться в приложении по-разному, в зависимости от заданного типа значения. Аналог – всем известные ini-files! Основное преимущество применения TV – это возможность расширения списка значений в процессе исполнения приложения, без изменения структуры БД!
(image placeholder)

Список тэг-значений хранятся в таблице TaggedValue.

  • DefaultValue – это значение будет использоваться, если не задано действующее значение.
  • DefaultValueAlias – соответственно, значение для локализации.
  • ValueType – тип значения. Кроме стандартных типов, можно использовать свои, все зависит от фантазии разработчика. В зависимости от типа значения выбирается встроенный редактор значений. Например, для типа «OCL» используется редактор OCL-выражений. Для перечислимого типа производится выбор значений, которые хранятся в таблице TV_ValueSet. Возможен выбор из списка объектов БД – тогда надо задать ValuesListName.
  • Hint – подсказка при выборе/редактировании значения.
  • HintAlias – локализованная подсказка.
  • IsReadOnly – ну, это понятно.
  • ValuesListName – название класса объектов, из списка значений которых будет производиться выбор. Например «City», и тогда будет предложен выбор из списка ClassByExpressionName(‘City’).

   Список значений для набора объектов – в таблице TV_ValueSet. Например, для логического типа можно задать значения «True/False», а возможно и «Можно/Нельзя».
  Сами значения TV хранятся в таблице TV_Value. StringValue – действительное значение, значение, StringValueAlias – то, которое используется для локализации программы (или для показа юзеру ().

(image placeholder)

Для того, чтобы подключить к объекту тэг-значения, нужно создать ассоциацию типа n-n – OverridenTagedValues. Вся прелесть в том, что таким образом хранятся только переопределенные значения! Если нет значения – будет использоваться значение по-умолчанию! Классом такой ассоциации будет TV_Value, то есть, экземпляр класса – тэг-значение будет создан только при действительном вводе информации! Если грамотно подобрать умолчания, можно существенно сэкономить на размере БД!

Программная реализация.

Эта функция возвращает действующее тэг-значение по его имени. Если значение не было переопределено – возвращается значение по умолчанию.

function TMetaActorProperty.ActualValueByName(Tag: String): String;
var
  _TaggedValue:TTaggedValue;
  _TV_Value:TTV_Value;
begin
  _TaggedValue:=Self.TaggedValues.EvaluateExpressionAsDirectElement(‘self->select(name=’+QuotedStr(Tag)+’)->first’) as TTaggedValue;
  if Self.OverridenTaggedValues.Includes(_TaggedValue) then begin
    _TV_Value:=Self.TV_Value.BoldObjects[Self.OverridenTaggedValues.IndexOf(_TaggedValue)];
    Result:=_TV_Value.StringValue;
  end
  else
    Result:=Self.TaggedValues.EvaluateExpressionAsString(‘self->select(name=’+QuotedStr(Tag)+’)->first.defaultValue’, 1);
end;

Эта процедура устанавливает тэг-значение.

procedure TMetaActorProperty.SetTVByName(Tag: String; Value: String);
var
  _TaggedValue:TTaggedValue;
  _TV_Value:TTV_Value;
  Overriden: Boolean;
begin
  _TaggedValue:=LocateInList(Self.TaggedValues, ‘name’, Tag) as TTaggedValue;
  if not Assigned(_TaggedValue) then begin
     MsgError(Self.Name+’: Not found TV=’+Tag);
     Exit;
  end;  
  Overriden:=Self.OverridenTaggedValues.Includes(_TaggedValue);
  if Overriden then begin //Ранее было переопределено
    _TV_Value:=           //Найдем переопределенное значение
      Self.TV_Value.BoldObjects[Self.OverridenTaggedValues.IndexOf(_TaggedValue)];
    if (Value=_TaggedValue.DefaultValue) then //Если ввели значение по умолчанию — удалим переопределенное
       Self.OverridenTaggedValues.Remove(_TaggedValue)
    else  //Ранее было переопределено и изменили значение
      _TV_Value.StringValue:=Value;
  end
  else   //Ранее было по умолчанию
  begin
    Assert(Assigned(_TaggedValue), ‘SetTVByName’);
    Self.OverridenTaggedValues.Add(_TaggedValue);
    _TV_Value:=           //Найдем переопределенное значение
      Self.TV_Value.BoldObjects[Self.OverridenTaggedValues.IndexOf(_TaggedValue)];
    _TV_Value.StringValue:=Value;
  end;
end;

Функция дает список подключенных тэг-значений.

function TMetaActorProperty.TaggedValues: TTaggedValueList;  //Find TaggedValueList
var
  expr: string;
begin
  Result:=nil;
  if not Assigned(Self) then Exit;
  expr:=’TV_Section.allInstances->select(name=’+QuotedStr(Self.MetaClassName)+’)->first.taggedValues’;
  Result:=TBoldSystem.DefaultSystem.EvaluateExpressionAsDirectElement(expr)as TTaggedValueList;
end;

Такие тэг-значения удобны для сохранения в БД настроек программы, быстрого создания прототипов проекта для согласования с заказчиком и добавления/изменения атрибутов на «лету» (в дальнейшем, после обкатки приложения, легко все оформить в виде «настоящих» атрибутов).

Для  создания/редактирования тэг-значений применяется редактор на основе компонента TCommonInspector из пакета Greatis — http://www.greatis.com.