По результатам предыдущего поста (про программку с MySQL). Предыстория:
размещают на моем форуме спам. Я реагирую, и смотрю, кто такие. Думал, программа написана на Firebird. Ан нет, включает в себя MySQL. Тогда я пошел на форум, и спросил — либо исходники открывайте, либо покажите что купили коммерческую лицензию на MySQL.
Весь диалог тут:
https://clientbase.ru/forum/index.php?showtopic=1620
Общее резюме — «Мы не считаем себя нарушителями лицензии, пока обратное нам не докажет правообладатель либо законодательство.»
Просто замечательно, как мне кажется.
ООП и паттерны проектирования: практическое применение.
В Интернете выложено достаточно большое количество статей на тему применения паттернов (шаблонов) проектирования. К сожалению, примеров кода на 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).
Применимость: используйте паттерн «Мост», когда:
— хотите избежать постоянной привязки абстракции к реализации. Так, например, бывает, когда реализацию необходимо выбирать во время выполнения программы.
— и абстракции, и реализации должны расширяться новыми подклассами. В таком случае паттерн «Мост» позволяет комбинировать разные абстракции и реализации и изменять их независимо.
— изменения в реализации абстракции не должны сказываться на клиентах, то есть клиентский код не должен перекомпилироваться. — вы хотите скрыть от клиентов реализацию абстракции.
А это как раз наш случай! И вот как выглядит диаграмма для нашей конкретной реализации паттерна:
Каковы основные результаты применения паттерна?
— отделение реализации от интерфейса. Реализации больше не имеет постоянной привязки к интерфейсу. Реализацию абстракции можно конфигурировать во время выполнения. Разделение классов 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).
На следующем занятии мы рассмотрим применение паттерна «Абстрактная фабрика».
Некие негодяи залезли на мой форум и разместили там спам-сообщение.
Я, как водится, полез по ссылке, чтобы посмотреть, что рекламируют. И, о ужас! Не удержусь от публикации ссылки — https://clientbase.ru/download/ — здесь можно скачать программу CRM с MySQL! А программа-то без исходников! Вот я им завтра-то позвоню, и спрошу, где исходники, или оплачивали-ли они лицензию. Контора, похоже, большая, т.к. даже номер с 8-800 зарегистрирован.
upd: по признакам «большой» контора вроде проходит — спам и телефон 8-800…, но наименование организации: ИП Гарифуллин Марат Мидхатович, ИНН 165700291820, Адрес: 420024, Казань, Ямашева, 36
Специально для ITBlogs — НГ
Вопрос к ITBlogs — а мы будем встречаться до НГ? Обсудить, так сказать, что было, что будет, чем сердце успокоится? Или в разных заведениях уже все капитально забронировано?
Думаю, надо или на следующей неделе, или отказать.
p.s. организацию на себя не возьму, увы, но примкну обязательно.
Здравствуй …, Новый Год?
В октябре я запорол статистику своего сайта в Google Analytics. Случилось это когда Yandex вдруг включил проверку на вирусы в своем «вебмастере», и из-за одной древней затесавшейся страницы с вирусом весь сайт был помечен как нежелательный к посещению.
Ситуация ужасная, потому что вот все было ок, а тут вдруг яндекс посылает, а гугл молчит, и что делать — непонятно. Я начал метаться, и поскольку уже давно не заглядывал в код страниц, мне показалось что гугловская аналитика и есть вирус (из-за слова unescape). Я ее удалил, потом восстановил, причем делал это на основных страницах, обновляемых одним шаблоном.
Через некоторое время я обнаружил, что Гугль Аналитика сообщает мне о 2-3 кратном снижении посещений сайта. Удар ниже пояса. На то чтобы догадаться о причине, ушла примерно неделя. Т.е. я связывал всю эту историю с падением посещаемости, но думал что это естественная, а не искусственная причина.
Оказалось все более чем прозаично — в том шаблоне при восстановлении кода GA я перепутал и воткнул идентификатор другого сайта. Отличие было всего в одной цифре -1 вместо -9, но гугл стал игнорировать неверную статистику.
Собственно, после исправления «циферок» все вернулось на свои места. Фух. Надо признать, что я забыл одно из основных правил «определения проблем» — если что-то пошло не так, то нужно искать, что происходило и делалось в самом начале этого «не так».
Back to GPL
Буквально вчера мне прислали интересную ссылку:
«Именное лицензионное соглашение на Ubuntu Desktop»
Сильно удивившись, я начал копать, и обнаружил, что компания Инфра Ресурс продает не только лицензии, но и комплекты типа
OpenOffice.org pro Бизнес Пак (DVD, Краткое руководство, Лицензия, Наклейка).
Волшебно, не находите?
То есть, мы сделаем по образцу и подобию MS, а как там на самом деле с лицензией, проверять никто не будет. Ну и зарабатывают свою копеечку…
В реальности, GPL запрещено переводить, совершенно официально. Причем мотивация отсутствия легальных переводов — натуральный «отмаз», если не сказать хуже:
The reason the FSF does not approve these translations as officially valid is that checking them would be difficult and expensive (needing the help of bilingual lawyers in other countries)
Дорого, видите ли. Примерно 2-3 года назад я занимался этим вопросом, сделать заверенный перевод стоит примерно 200-300 баксов. То есть, мы за бесплатное ПО, поэтому денег на переводы у нас нет.
Однако, любые переводы, даже заверенные нотариально, но выполненные третьими сторонами, не имеют никакого значения (ничтожны), поскольку нет подтверждения оригинальности текста лицензии от ее инициатора. Впрочем, выше я уже сказал, что любой перевод «does not legally state the distribution terms for software that uses the GNU GPL», и должен содержать в самом начале текст, который однозначно указывает на юридическое бессилие этого перевода.
Вот еще, ГНУ/Линуксцентр тоже предлагает «именную лицензию Ubuntu 8.10 с печатью и подписью (такую лицензию вы сможете предъявить проверяющим органам в случае осуществления ими проверки лицензионности ПО)«.
Какую лицензию, с чьей печатью и подписью? Кто видел, сообщите, пожалуйста.
Если имеется в виду вот это, то при всем моем уважении к ЛинуксЦентру это филькина грамота (в части «лицензии на ubuntu).
Ну и, на закуску, напомню что с лицензиями на MySQL все так же плохо: или GPL с требованием открыть исходники, или покупать платную закрытую лицензию. Я это к тому, что сегодня мне прислали ссылку на поставщика системы на MySQL, который ставит клиентам бесплатный MySQL вместе со своей системой, напропалую. А ведь совершенно однозначно написано:
OEMs, ISVs and VARs that want the benefits of embedding commercial binaries of MySQL software in their commercial applications but do not want to be subject to the GPL and do not want to release the source code for their proprietary applications should purchase a commercial license from Sun.
https://mysql.com/about/legal/licensing/oem/#3
Вебинар — зачем он нужен?
Интернет в регионах становится все быстрее и дешевле. И правда, если 2 года назад в Калуге безлимитных тарифов не было, то сейчас мегабитный безлимит стоит 750 руб в месяц, а год назад 900 рублей стоил безлимит 128к.
Кризис тоже прогрессирует, по крайней мере в области ИТ и ПО, и на смену очным семинарам прочно пришли вебинары. И правда, зачем снимать помещение, платить за кофе-брейк, если можно собрать людей не отрывая их от компьютеров.
В пятницу мы провели тестовый вебинар на 20 минут, через Microsoft Live Meeting. Вышло неплохо. После вебинара на форуме пошла дискуссия, в какое время лучше организовывать такие мероприятия, и вообще.
И тут меня посетила мысль. А зачем вообще делать вебинар (вместо семинара)? Ведь можно записать (!) материал в любом формате, отредактировать его (!), и выложить хоть на ютуб.
Вернее, так: вебинары вместо семинаров мне кажутся бессмысленными. То есть, вебинары вполне имеют смысл, когда группе людей нужно обменяться мнениями, например, это может быть общение головной фирмы с филиалами, или общение фирмы с партнерами, и т.п. Ну а если аудитория вебинара превышает 20-30 человек? Ведь вебинар имеет определенные неудобства, даже в отношении очного семинара:
- докладчик все равно должен в конкретное время все показать и рассказать, точно так же как на очном семинаре
- слушатель не «привязан» посещением очного семинара, поэтому может легко сорваться с места, и не участвовать в вебинаре (по разным причинам, включая разницу во времени)
- «рулить» вопросами «из зала» очень тяжело, т.к. управление микрофонами на вебинаре сложнее, чем на очном семинаре
- из-за специфики конкретного канала звук докладчика у слушателя может быть плохим
И при всем этом большинство вебинаров можно просмотреть в записи после их проведения.
Так может проще записать мероприятие, и сразу выложить его для всеобщего доступа, не «проводя»?
Вот смотрите, обычный семинар преследует ряд целей:
- привлечь внимание аудитории и медиа
- собрать N слушателей
- дать возможность слушателям общаться с докладчиком и между собой
Кстати, что интересно, после семинаров обычно обещают «выложить презентации», но без озвучивания их докладчиком они представляют гораздо меньший интерес. То есть, не пришел на семинар — много потерял.
В проведении вебинара есть пункты 1 и 2, а пункт 3 или отсутствует или сильно ограничен. Кроме того, если после вебинара доступна запись, то на него совершенно «необязательно ходить».
В общем, пока я вижу вебинар как некий «суррогат», или даже если хотите, трансляцию стереотипа обычных семинаров в интернет. Но если вебинар не проводить, то тогда получается, что пропадает событие как таковое — ведь не будут же СМИ сообщать, что «компания X выложила новый ролик про свой продукт N на сайте»?
Очень мне понравилось работать с таким замечательным компонентом, как 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 — https://greatis.com.