Main Module
The main data module contains several objects associated with a session:
The connection to the database (TFDConnection)
Tables or queries to access the database (TFDTable, TFDQuery)
Data sources for accessing the database objects from data-bound controls
A list of actions (TActionList)
A list of images (TUniNativeImageList) with an adapter (TUniImageListAdapter) for using the images from the action list
Using the previous components and its internal code, the MainModule encapsulates most of the business logic of this simple application.
Create / Destroy
Each session has its own database connection. The connection string is already available from the server module.
procedure TUniMainModule.UniGUIMainModuleCreate(Sender: TObject);
begin
Conn.ConnectionString := UniServerModule.ConnString;
Conn.Open;
end;
procedure TUniMainModule.UniGUIMainModuleDestroy(Sender: TObject);
begin
Conn.Close;
end;Login process
Depending on how the user access the application, the application will instantiate the desktop or touch login form. After entering the username and password, pressing OK will trigger the login validation process.
{ Login validation and session platform selection }
Login encapsulates two business rules: how to identify a valid user (combination username/password), and what kind of user can see the action "Edit Users".
Platform selection
Each login form (dLoginForm and mLoginForm) reports to the MainModule the selected platform when attempting a login.
{ Platform selection }
After selecting a platform, the session will know how to interact with the user on that platform. The solution used in this application is to group different implementations of common actions behind a facade/interface and instantiate the correct one for the selected platform.
The interface is IService:
There are two implementations (for desktop and touch respectively).
Desktop
Touch
It is now clear that both implementations are practically identical, because the only differences are the classes implementing each method in the interface. That is a significant limitation in this solution, as there is duplicated code which is a bad programming practice. One of the possible solutions is to use Dependency Injection for locating the correct classes for a given platform and avoid code duplication.
Implementing the launching pad with actions
One of the best practices for separating the user interface from the business logic is to use actions. The main data module has an action list with the following actions:
Edit Users (only available to administrators)
Edit Orders
Report Sales
No matter what kind of visual control is used by the target platform, if it supports actions, it will be able to trigger the selected actions. For example, the desktop main form exposes these actions in a TUniMainMenu and in a TUniToolBar, while the touch main form exposes them in large buttons.
The user interface is not explicitly requesting anything from the MainModule and the MainModule is not aware of the user interface in any way.
On the other hand, the business logic requires executing the requested actions, which is the reason for creating the interface Services.
{ Launching pad business logic actions }
Following the best practices, only when asking for a report the corresponding data module is created and used for exporting the report to PDF format.
Moving the business logic from the user interface to the data module
Explicit requests from the user were already mapped to actions and handled in the data module. Other user requests arrive to the data module as data-related events which should be handled according to the business logic of the application. But there is an implicit business logic which needs an explicit request: opening and closing datasets used by concrete forms.
In this application we will not use any registration system (which could relate concrete forms or interfaces to resources), but just explicit calls requesting for data module support.
Resource handling
{ Resource handling for forms }
Each form requiring data module resources will call UniMainModule.OnCreate
(Self) and OnDestroy(Self) so that the data module set up and tear off any required resources. In the previous code the data module opens and closes the datasets required by each form.
This particular code is just server-side business logic, and it could be activated by using Dependency Injection. It can be done by registering resources with their respective OnCreate and OnDestroy methods.
In more complicated situations, it could be possible to "inject" components into the concrete form. For example, the application MainMenu could be defined in the data module, and some generic method could be able to create the correct equivalent for the Sender form based on its type.
The goal of this kind of programming is achieving greater disconnection between the user interface and the business logic. The results will come later when looking at the implementation of each form.
Data-related event handling
Most of the business logic belongs to this category. It is typical to validate changes in OnBeforePost, refresh information or trigger other actions in OnAfterPost, calculate virtual fields in OnCalcFields, etc.
{ Data access event handlers }
Executing actions
Some actions are not standard for a data-related control and they are exposed on buttons, menu items, etc.
{ Business logic actions }
The last action raises a concern. If the business logic mandates that at least one user must be admin, why this validation only happens when the user presses the button "Toggle Admin Status"? What will happen if the same action happens by clicking the Admin checkbox in the user interface?
There are two options for solving the previous issue: making the column Admin read-only, or executing this validation in both places (here and in tblUsers.OnBeforePost).