It is impossible to be a teacher without being a student

Communication with readers of the blog and telegram channel adds new colors to my life: I feel like a fifth-grade student who has to rewrite almost half of the notebook, working on mistakes. In this case we are talking about errors made when dividing procedures and functions into modules. These bugs prevent individual modules from being integrated into other applications due to circular relationships in the uses clause, which has frustrated some of my readers.

To be fair, I would like to note that if I had received feedback on publications immediately, I would have made corrections immediately. But, as they say, better late than never.

So, the task is to redistribute the contents of individual library modules in order to eliminate cyclic dependency and minimize module coupling. I consider the latest and therefore most complete version of the modules to be the one used in the DataKeeper project, so all the necessary manipulations will be done on it.

And I set about “shifting” procedures and functions from one module file to another. This took an order of magnitude longer than I originally planned. The situation reminded me of a wonderful story.

In the ancient temple of the god Brahma there were three diamond rods, one of which had 64 golden discs strung on it. Once, at the very beginning of time, the monks of the temple offended Brahma, and he condemned them to shifting discs. Now, as soon as all the disks are on the other rod, the walls of the temple will turn to dust and the end of the world will come.

Differentiation

Correctly divide the code into separate files. The task at first glance is simple: group yourself into categories to which certain functions or procedures belong. But at the same time, dependencies must be taken into account, avoiding cyclic references. And therein lies the devil of manual differentiation, which can drive anyone crazy who does not adhere to clear rules. For example:

  • Scripts are grouped by categories and types.
  • The type of script determines where it is stored – a file with the *.pas extension, which is also called a module.
  • The script category is implemented as a folder containing individual files with the *.pas extension.
  • Categories have a hierarchy that defines their dependencies on each other. Categories are divided into basic and dependent.
  • Modules in base categories cannot link to modules in other categories, but can have hierarchical links within the category.
  • Modules in dependent categories can also have hierarchical links within the category, as well as links to modules in other categories.

Each folder (category) contains files (modules) within which procedures and functions are stored.

CategoryDescriptionDepends on
SystemGeneral purpose modules whose implementation does not use any other categories of modules.
ToolsModules that implement individual auxiliary tools that are not related to the main functionality of the program: debugging, class browser, etc.
VClassVirtual classroom modules.System
AppExtModules that expand the capabilities of the application.System, VClass
UserAppModules for storing procedures and project specific functionSystem, AppExt, VClass
FormsModules that store application form event handlers.System, UserApp, AppExt, VClass

Thus, modules of the System and Tools categories can only have dependencies within the modules themselves; modules of the VClass category may depend on modules of the VClass or System category; AppExt modules can have dependencies on AppExt, System and VClass category modules, and so on.

Inside each folder there is a file of the same name with a .pas extension, inside of which there is only one uses command, which describes the dependency levels of the modules included in the category.

// General purpose modules
// 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)
ModuleDescriptionDepends on
UtilsGeneral Purpose Procedures and Functions
IniFileWorking with the settings.ini data storage file
DBService functions for working with the database
AppGeneral functionality of applications created on the My Visual Database platform. Also contains constants and general purpose variables.Utils, IniFile
ResourceWorking with text resourcesApp, Utils, IniFile

This data storage device allows you to include only categories in the main script.pas file, without listing all the modules used in the project:

uses
  // first independent, then dependent on their superiors. NEVER VIOLATE THE HIERARCHY OF LINKS AND DO NOT MAKE CROSS OR CYCLIC LINKS!
  'System\System.pas', // system procedures
  'Tools\Tools.pas', // tools
  'AppExt\AppExt.pas', // extensions
  'VClass\VClass.pas', // virtual classes
  'UserApp\UserApp.pas', // general procedures and functions of the application
  'Forms\Forms.pas'; // application forms

begin
  UserApp_Init;
end.Code language: Delphi (delphi)

Each module has a header containing a list of procedures and functions contained within. This helps in studying the library and finding the necessary functionality for implementing certain application tasks, but does not completely replace documentation.

// General purpose procedures and functions
// 02/14/2023
// function AllreadyRun(AID:string):boolean; - returns whether the program with the specified ID is running
// function BoolToSQLValue(AValue:boolean):string; - converts a boolean value into a string for insertion into an SQL query
// function FindCF(AName:string): TComponent; - finds a component by full name.
// function FloatMin(AValue1,AValue2:double):double; - minimum value. I had to write my own, since the standard min always returns 0
// function FloatToSQL(AValue:Double):string; - floating point number to use in SQL query
// function GetC(AForm: TForm; AName: string;):TComponent; - alternative function Form.FindComponent
// function GetDesktopDPI: integer; - getting the current screen resolution.
// function GetDesktopDPI: integer; - getting the current screen resolution.
// function GetFormByName(AName: string): TForm; - getting a form by name
// function GetTmpFilename(AExt: string): string; - returns a random file name that does not yet exist in the temporary folder
// function InList( AValue:string; AList:string; ADelimiter:char = ','; ):boolean; - determines the occurrence of a string in a substring, which is a list
// function RemoveDirEx(ADir: string):boolean; - deleting a directory with preliminary clearing of files and subdirectories
// function SaveWithoutClose(AButton: TdbButton): integer; - saving without closing the form, returns the ID of the added record
// function StrToSQL(AData: string; AEmptyIsNull:boolean = False): string; - returns a quoted string with escaping
// function WaitExternalExe( ACaption: string; AStartCount: integer = 10; AFinishCount: integer = 0; ACountDelay: integer = 10 ):integer; - waiting for the external program to complete
// procedure BClick(AButton: TdbButton); - click without recursion in scripts
// procedure CForm(AObject: TObject; var AForm: TForm); - determining the shape of the component
// procedure FindC(AForm: TForm; AName: string; var AComponent: TComponent; ACheck: boolean = True); - search for a component on a form with control
// procedure SaveImageToFile(AImage: TdbImage; AFileName: string; AOriginalSize: boolean = False); - saving an image from a picture to a file in JPG formatCode language: Delphi (delphi)

Integration

Let’s look at the integration using the example of a simple Accounting ink cartridges application, which can be downloaded from the official website of Drive Software Company.

First, you need to analyze the dependencies to understand which modules will need to be transferred to the project. Let’s look at this process for the License.pas module, which is responsible for licensing.

The licensing module uses all four modules of the System category in its work, so in addition to the License.pas file, you need to copy the System folder with all its contents.

To avoid confusion, I recommend keeping the folder structure used in the donor project.

uses
  // first independent, then dependent on their superiors. NEVER VIOLATE THE HIERARCHY OF LINKS AND DO NOT MAKE CROSS OR CYCLIC LINKS!
  'System\System.pas', // system procedures
  'Tools\Tools.pas', // tools
  'AppExt\AppExt.pas'; // extensions
Code language: Delphi (delphi)

Note. Just in case, I added a debugging tool (Tools module) to the project, which you may also find useful. This tool implements the dbg(AValue:TVariant) procedure, which displays debug messages in a special window.

It is necessary to initialize the modules. Let’s place the necessary calls in the main program, in the begin end block. In our case, we first need to initialize the system variables, then the resource module, and only then the licensing extension:

begin
  App_InitSystemVar;
  Resource_Init;
  License_Init;
end.

The extension itself also needs to be configured. To do this, you need to edit the contents of the License.pas module. Make sure that the licensing system is enabled, the number of modules and their names are correct, and the encoding strings are changed to those that you will use in your key generator:

const
  LICENSE_TURN_ON = True; // enable licensing system
  LICENSE_SECRET_WORD = 'SecretWord'; // string to encode the key
  LICENSE_MOD = 'Modules'; // string for encoding modular licenses
  LICENSE_MODULE_COUNT = 2; // number of licensing modules
  LICENSE_BUY_LINK = 'https://k245.ru'; // link to the license purchase page
  LICENSE_MAX_REC_COUNT = 5; // maximum number of records in the table
  //
  LICENSE_MODULE_NAME_1 = 'First module';
  LICENSE_MODULE_NAME_2 = 'Second module';
  //
  LICENSE_FILENAME = 'license.txt'; // file name with license text
  LICENSE_DEMO_LABEL = 'DEMO'; // label displayed for an unregistered copy of the programCode language: PHP (php)

To limit the number of data added to the table, just specify the License_btnSave_OnClick() handler at the save button.

If you want to check for a license before performing some action, the click handler code is as follows:

procedure CheckLicense_OnClick (Sender: TObject; var Cancel: boolean);
begin
  if not licensed then
  begin
    ShowMessage('Export to Excel is only available in the registered version');
    Cancel := True;
  end;
end;
Code language: PHP (php)

To implement restrictions related to modularity, it is necessary to check the presence of the module number in the list of licensed modules:


procedure CheckModule_1_OnClick (Sender: TObject; var Cancel: boolean);
begin
  if pos('1',License_Modules)=0 then
  begin
    ShowMessage('The function is available when licensing the first module');
    Cancel := True;
  end;
end;Code language: PHP (php)

For the licensing module to work, you will need a text file in which you must write the text of the license user agreement. Name the file license.txt and place it next to the executable file.

Result

The prefix DEMO (1) appears in the title of the unregistered program, and when you press the export button (2), a message (2) is displayed.

When I try to print reports (1), messages appear about the need for modular licensing.

A button for registration has appeared on the About form.

When saving a record (1), a message appears indicating that the unregistered version limit has been reached (2).

Registration

After clicking the registration button, a window with a license agreement is displayed. To complete the registration, you need to put a checker (1) and click the “Next” button (2).

In the registration window, the user fills out the “Licensee” field, the contents of the “Registration Code” field are sent to the developer to generate a license key. This can be conveniently done by copying the code to the clipboard (2). The key received from the developer is placed in the “License Key” field (3), then the “Activate” button (4) is clicked. The “Buy” button (5) opens the link that was specified in the LICENSE_BUY_LINK constant.

After successful registration, the licensee, key and list of licensed modules are displayed in the About window, and restrictions are removed.

Key generator

The generator is very simple in its design; its code has been published on my blog several times. The encryption algorithm is open, but a private key (1,2) is used. Additionally, you can limit the license by version number (3) or module (4). If there are several modules, they are listed separated by commas.

The registration code received from the application registration form is placed in the field (5), then the “Create” button (8) is clicked. The license key (7) can be copied to the clipboard (6) for transmission to the user.

The key generator script contains only two handlers:

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)

Links

Zen

I don’t know what happened to the monks there and where the golden disks are now, but the meditative practice of code shifting really brings you closer to enlightenment.

I recommend refactoring regularly.

P.S. Don’t forget to change the system constants in the System\App.pas module, which are responsible for the application name and version number, so that you don’t get confused, like in the screenshots in the article 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *