Быть учителем, перестав быть учеником, невозможно

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

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

Итак, задача заключается в необходимости перераспределить содержимое отдельных библиотечных модулей, чтобы исключить циклическую зависимость и минимизировать связанность модулей. Самой последней и потому самой полной версией модулей я считаю ту, которая используется в проекте DataKeeper, поэтому над ней и будут проделаны все необходимые манипуляции.

И принялся я за “перекладывание” процедур и функций из одного файла модуля в другой. Заняло это на порядок больше времени, чем я планировал изначально. Ситуация напомнила одну замечательную историю.

В древнем храме бога Брахмы находились три алмазных стержня, на один из них было нанизано 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); - сохранение изображения с картинки в файл, в формате JPGCode 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().

Если вы захотите проверять наличие лицензии перед выполнением какого-то действия, то код обработчика нажатия такой:

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 и поместите его рядом с исполняемым файлом.

Результат

В заголовке незарегистрированной программы появилась приставка DEMO (1), а при нажатии кнопки экспорта (2) высвечивается сообщение (2).

При попытке распечатать отчеты (1) появляются сообщения о необходимости модульного лицензирования.

На форме About появилась кнопка для регистрации.

При сохранении записи (1) выскакивает сообщение о достижении лимита незарегистрированной версии (2).

Регистрация

После нажатия кнопки регистрации отображается окно с лицензионным соглашением. Для выполнения регистрации необходимо поставить чекер (1) и нажать кнопку “Далее” (2).

В окне регистрации пользователь заполняет поле “Лицензиат”, содержимое поля “Код регистрации” передается разработчику для генерации лицензионного ключа. Это удобно сделать, скопировав код в буфер обмена (2). Полученный от разработчика ключ помещается в поле “Лицензионный ключ” (3), затем наживается кнопка “Активировать” (4). Кнопка “Купить” (5) открывает ссылку, которая была указана в константе LICENSE_BUY_LINK.

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

Генератор ключей

Генератор весьма прост по своему устройству, его код неоднократно публиковался мной в блоге. Алгоритм шифрования открытый, но используется закрытый ключ (1,2). Дополнительно можно ограничить лицензию номером версии (3) или модулем (4). Если модулей несколько, то они перечисляются через запятую.

Код регистрации, полученный из формы регистрации приложения, помещается в поле (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)

Ссылки

Дзен

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

Рекомендую заниматься рефакторингом регулярно.

P.S. Не забудьте изменить системные константы в модуле System\App.pas, отвечающие за название приложения и номер версии, чтобы не получилась путаница, как на скриншотах в статье 🙂

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *