Быть учителем, перестав быть учеником, невозможно
Общение с читателями блога и телеграм-канала добавляет в мою жизнь новые краски: ощущаю себя учеником пятого класса, которому приходится переписывать чуть ли не половину тетради, выполняя работу над ошибками. В данном случае речь идет об ошибках, допущенных при разделении процедур и функций по модулям. Эти ошибки не позволяют интегрировать отдельные модули в другие приложения из-за наличия цикличных связей в секции uses, что приводит в отчаяние некоторых моих читателей.
Справедливости ради хочу заметить, что если бы я получал обратную связь по публикациям сразу, то исправления были бы выполнены мной незамедлительно. Но, как говорится, лучше позже, чем никогда.
Итак, задача заключается в необходимости перераспределить содержимое отдельных библиотечных модулей, чтобы исключить циклическую зависимость и минимизировать связанность модулей. Самой последней и потому самой полной версией модулей я считаю ту, которая используется в проекте DataKeeper, поэтому над ней и будут проделаны все необходимые манипуляции.
И принялся я за “перекладывание” процедур и функций из одного файла модуля в другой. Заняло это на порядок больше времени, чем я планировал изначально. Ситуация напомнила одну замечательную историю.
![](https://k245.ru/wp-content/uploads/2024/02/monah.jpg)
В древнем храме бога Брахмы находились три алмазных стержня, на один из них было нанизано 64 золотых диска. Однажды, в самом начале времён, монахи храма провинились перед Брахмой, и он обрёк их на перекладывание дисков. Теперь, как только все диски окажутся на другом стержне, стены храма обратятся в пыль и настанет конец мира.
Дифференциация
Правильно разделить код по отдельным файлам. Задача на первый взгляд простая: группируй себе по категориям, к которым относятся те или иные функции или процедуры. Но при этом нужно учитывать зависимости, не допуская циклических ссылок. И в этом кроется дьявол ручной дифференциации, который может свести с ума любого, кто не будет придерживаться четких правил. Например таких:
- Скрипты сгруппированы по категориям и видам.
- Вид скрипта определяет место его хранения – файл с расширением *.pas, который также называется модулем.
- Категория скрипта реализована как папка, в которой находятся отдельные файлы с расширением *.pas.
- Категории имеют иерархию, которая определяет их зависимости друг от друга. Категории делятся на базовые и зависимые.
- Модули в базовых категориях не могут ссылаться на модули в других категориях, но могут иметь иерархические ссылки внутри категории.
- Модули в зависимых категориях также могут иметь иерархические ссылки внутри категории, а также ссылки на модули других категорий.
В каждой папке (категории) расположены файлы (модули), внутри которых хранятся процедуры и функции.
Категория | Описание | Зависит от |
---|---|---|
System | Модули общего назначения, реализация которых не использует какие-либо другие категории модулей. | |
Tools | Модули, реализующие отдельные вспомогательные инструменты, не относящиеся к основной функциональности программы: отладка, обозреватель классов и т.д. | |
VClass | Модули виртуальных классов. | System |
AppExt | Модули, расширяющие возможности приложения. | System, VClass |
UserApp | Модули для хранения процедур и функция конкретного проекта | System, AppExt, VClass |
Forms | Модули, в которых хранятся обработчики событий форм приложения. | System, UserApp, AppExt, VClass |
Таким образом, модули категорий System и Tools могут иметь зависимости только внутри самих модулей; модули категории VClass могут зависеть от модулей категории VClass или System; модули AppExt могут иметь зависимости от модулей категории AppExt, System и VClass и так далее.
Внутри каждой папки находится одноименный файл с расширением .pas, внутри которого находится только одна команда uses, описывающая уровни зависимости входящих в категорию модулей.
// Модули общего назначения
// 14.02.2024
uses
'System\Utils.pas',
'System\IniFile.pas',
'System\DB.pas',
'System\App.pas',
'System\Resource.pas';
begin
end.
Code language: Delphi (delphi)
Модуль | Описание | Зависит от |
---|---|---|
Utils | Процедуры и функции общего назначения | |
IniFile | Работа с файлом хранения данных settings.ini | |
DB | Сервисные функции для работой с БД | |
App | Общая функциональности приложений, созданных на платформе My Visual Database. Содержит также константы и переменные общего назначения. | Utils, IniFile |
Resource | Работа с текстовыми ресурсами | App, Utils, IniFile |
Такое устройство хранения данных позволяет в основном файле script.pas подключать только категории, не перечисляя все используемые в проекте модули:
uses
// сначала независимые, потом которые зависят от вышестоящих. НИКОГДА НЕ НАРУШАТЬ ИЕРАРХИЮ ССЫЛОК И НЕ ДЕЛАТЬ ПЕРЕКРЕСТНЫЕ И ЦИКЛИЧЕСКИЕ ССЫЛКИ!
'System\System.pas', // системные процедуры
'Tools\Tools.pas', // инструменты
'AppExt\AppExt.pas', // расширения
'VClass\VClass.pas', // виртуальные классы
'UserApp\UserApp.pas', // общие процедуры и функции приложения
'Forms\Forms.pas'; // формы приложения
begin
UserApp_Init;
end.
Code language: Delphi (delphi)
В каждом модуле имеется заголовок, содержащий список находящихся внутри процедур и функций. Это помогает в изучении библиотеки и поиске нужной функциональности для реализации тех или иных прикладных задач, но не заменяет полностью документацию.
// Процедуры и функции общего назначения
// 14.02.2023
// function AllreadyRun(AID:string):boolean; - возвращает, запущена ли программа с указанным ID
// function BoolToSQLValue(AValue:boolean):string; - преобразует логическое значение в строку для вставки в SQL-запрос
// function FindCF(AName:string): TComponent; - находит компонент по полному имени.
// function FloatMin(AValue1,AValue2:double):double; - минимальное значение. Пришлось писать свою, так как стандартная min всегда возвращает 0
// function FloatToSQL(AValue:Double):string; - число с плавающей запятой для использования в SQL-запросе
// function GetC(AForm: TForm; AName: string;):TComponent; - альтернативная функция Form.FindComponent
// function GetDesktopDPI: integer; - получение текущего разрешения экрана.
// function GetDesktopDPI: integer; - получение текущего разрешения экрана.
// function GetFormByName(AName: string): TForm; - получение формы по названию
// function GetTmpFilename(AExt: string): string; - возвращает случайное имя файла, которого ещё не существует во временной папке
// function InList( AValue:string; AList:string; ADelimiter:char = ','; ):boolean; - определяет вхождение строки в подстроку, которая является списком
// function RemoveDirEx(ADir: string):boolean; - удаление каталога с предварительной очисткой от файлов и вложенных каталогов
// function SaveWithoutClose(AButton: TdbButton): integer; - сохранение без закрытия формы, возвращает ID добавленной записи
// function StrToSQL(AData: string; AEmptyIsNull:boolean = False): string; - возвращает строку в кавычках c экранированием
// function WaitExternalExe( ACaption:string; AStartCount: integer = 10; AFinishCount:integer = 0; ACountDelay:integer = 10 ):integer; - ожидание завершения работы внешней программы
// procedure BClick(AButton: TdbButton); - клик без рекурсии в скриптах
// procedure CForm(AObject: TObject; var AForm: TForm); - определение формы компонента
// procedure FindC(AForm: TForm; AName: string; var AComponent: TComponent; ACheck: boolean = True); - поиск компонента на форме с контролем
// procedure SaveImageToFile(AImage: TdbImage; AFileName: string; AOriginalSize: boolean = False); - сохранение изображения с картинки в файл, в формате JPG
Code language: Delphi (delphi)
Интеграция
Разберем интеграцию на примере простого приложения Accounting ink cartridges, которое можно скачать с официального сайта Drive Software Company.
Для начала нужно проанализировать зависимости, чтобы понять, какие модули понадобится перенести в проект. Рассмотрим этот процесс для модуля License.pas, который отвечает за лицензирование.
Модуль лицензирования использует в своей работе все четыре модуля категории System, поэтому кроме файла License.pas необходимо скопировать папку System со всем её содержимым.
Чтобы не запутаться, я рекомендую сохранять структуру папок, используемую в проекте-доноре.
uses
// сначала независимые, потом которые зависят от вышестоящих. НИКОГДА НЕ НАРУШАТЬ ИЕРАРХИЮ ССЫЛОК И НЕ ДЕЛАТЬ ПЕРЕКРЕСТНЫЕ И ЦИКЛИЧЕСКИЕ ССЫЛКИ!
'System\System.pas', // системные процедуры
'Tools\Tools.pas', // инструменты
'AppExt\AppExt.pas'; // расширения
Code language: Delphi (delphi)
Примечание. На всякий случай я добавил в проект инструмент отладки (модуль Tools), который вам тоже может пригодиться. В этом инструменте реализована процедура dbg(AValue:TVariant), которая отображает отладочные сообщения в специальном окне.
Необходимо выполнить инициализацию модулей. Разместим необходимые вызовы в основной программе, в блоке begin end. В нашем случае сначала необходимо инициализировать системные переменные, затем модуль ресурсов и только потом – расширение лицензирования:
begin
App_InitSystemVar;
Resource_Init;
License_Init;
end.
Само расширение также необходимо необходимо настроить. Для этого необходимо отредактировать содержимое модуля License.pas. Убедитесь, что система лицензирования включена, количество модулей и их названия указаны верно, строки кодирования изменены на те, которые вы будете использовать в своем генераторе ключей:
const
LICENSE_TURN_ON = True; // включить систему лицензирования
LICENSE_SECRET_WORD = 'SecretWord'; // строка для кодирования ключа
LICENSE_MOD = 'Modules'; // строка для кодирования модульных лицензий
LICENSE_MODULE_COUNT = 2; // количество модулей лицензирования
LICENSE_BUY_LINK = 'https://k245.ru'; // ссылка на страницу покупки лицензии
LICENSE_MAX_REC_COUNT = 5; // максимальное число записей в таблице
//
LICENSE_MODULE_NAME_1 = 'Первый модуль';
LICENSE_MODULE_NAME_2 = 'Второй модуль';
//
LICENSE_FILENAME = 'license.txt'; // имя файла с текстом лицензии
LICENSE_DEMO_LABEL = 'DEMO'; // метка, отображаемая у незарегистрированной копии программы
Code language: PHP (php)
Для ограничения числа добавляемых в таблицу данных достаточно указать у кнопки сохранения обработчик License_btnSave_OnClick().
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_094326470.png)
Если вы захотите проверять наличие лицензии перед выполнением какого-то действия, то код обработчика нажатия такой:
procedure CheckLicense_OnClick (Sender: TObject; var Cancel: boolean);
begin
if not licensed then
begin
ShowMessage('Экспорт в Excel доступен только в зарегистрированной версии');
Cancel := True;
end;
end;
Code language: PHP (php)
Для реализации ограничений, связанных с модульностью, необходимо проверять наличие номера модуля в списке лицензированных модулей:
procedure CheckModule_1_OnClick (Sender: TObject; var Cancel: boolean);
begin
if pos('1',License_Modules)=0 then
begin
ShowMessage('Функция доступна при лицензировании первого модуля ');
Cancel := True;
end;
end;
Code language: PHP (php)
Для работы модуля лицензирования вам потребуется текстовый файл, в котором вы должны написать текст лицензионного пользовательского соглашения. Назовите файл license.txt и поместите его рядом с исполняемым файлом.
Результат
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_095334057.png)
В заголовке незарегистрированной программы появилась приставка DEMO (1), а при нажатии кнопки экспорта (2) высвечивается сообщение (2).
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_100039713.png)
При попытке распечатать отчеты (1) появляются сообщения о необходимости модульного лицензирования.
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_100159858.png)
На форме About появилась кнопка для регистрации.
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_100425545.png)
При сохранении записи (1) выскакивает сообщение о достижении лимита незарегистрированной версии (2).
Регистрация
После нажатия кнопки регистрации отображается окно с лицензионным соглашением. Для выполнения регистрации необходимо поставить чекер (1) и нажать кнопку “Далее” (2).
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_101009665.png)
В окне регистрации пользователь заполняет поле “Лицензиат”, содержимое поля “Код регистрации” передается разработчику для генерации лицензионного ключа. Это удобно сделать, скопировав код в буфер обмена (2). Полученный от разработчика ключ помещается в поле “Лицензионный ключ” (3), затем наживается кнопка “Активировать” (4). Кнопка “Купить” (5) открывает ссылку, которая была указана в константе LICENSE_BUY_LINK.
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_101224349.png)
После успешной регистрации в окне About отображается лицензиат, ключ и список лицензированных модулей, ограничения снимаются.
![](https://k245.ru/wp-content/uploads/2024/02/izobrazhenie_2024-02-15_101919755.png)
Генератор ключей
Генератор весьма прост по своему устройству, его код неоднократно публиковался мной в блоге. Алгоритм шифрования открытый, но используется закрытый ключ (1,2). Дополнительно можно ограничить лицензию номером версии (3) или модулем (4). Если модулей несколько, то они перечисляются через запятую.
![](https://k245.ru/wp-content/uploads/2024/02/image.png)
Код регистрации, полученный из формы регистрации приложения, помещается в поле (5), затем наживается кнопка “Создать” (8). Лицензионный ключ (7) можно скопировать в буфер обмена (6) для передачи его пользователю.
Скрипт генератора ключей содержит всего два обработчика:
procedure frmMain_btnCopyToClipboard_OnClick (Sender: TObject; var Cancel: boolean);
begin
frmMain.edtKey.SelectAll;
frmMain.edtKey.CopyToClipboard;
frmMain.edtKey.SelLength := 0;
end;
procedure frmMain_btnCreateKey_OnClick (Sender: TObject; var Cancel: boolean);
var
s: string;
begin
s := frmMain.edtHWKey.Text + frmMain.edtSecretWord.Text + frmMain.edtVersion.Text;
if frmMain.edtModuleList.Text <> '' then
s := s + frmMain.edtModulePrefix.Text + frmMain.edtModuleList.Text;
frmMain.edtKey.Text := StrToMD5 ( s )
end;
begin
end.
Code language: Delphi (delphi)
Ссылки
- DataKeeper 1.9 – исходные коды проекта
- Inc Cartridges – пример использования модуля лицензирования
- Модульное лицензирование
Дзен
Не знаю, что там случилось с монахами и где сейчас лежат золотые диски, но медитативная практика перекладывания кода реально приближает к просветлению.
![](https://k245.ru/wp-content/uploads/2024/02/prosvetlenie.jpg)
Рекомендую заниматься рефакторингом регулярно.
P.S. Не забудьте изменить системные константы в модуле System\App.pas, отвечающие за название приложения и номер версии, чтобы не получилась путаница, как на скриншотах в статье 🙂