Forms Are Not Your Application
Delphi makes it wonderfully easy to put code behind a button. The trouble starts when the code behind the button quietly becomes the business logic, persistence layer, validation engine and application state.
One of Delphi's greatest strengths is how quickly you can build a working application.
Drop a button on a form, double-click it, write a few lines of code and you have behaviour.
That immediate connection between interface and implementation is productive, intuitive and one of the reasons Delphi has always been so effective for desktop development.
It also creates one of the easiest architectural traps to fall into.
The code starts on the form because that is where the event occurred.
Then a little validation is added.
Then some file handling.
Then a database update.
Then an HTTP request.
Then printing.
Then configuration logic.
Eventually the form is not really a form any more.
It is the application.
A form is a user interface, not a convenient place to put everything the user interface happens to trigger.
Why This Happens So Easily in Delphi
Event-driven programming naturally encourages code to live close to the event which caused it.
The user presses Save, so the save code goes in btnSaveClick.
The user selects a file, so the parsing code goes in btnOpenClick.
The user changes a setting, so the configuration code goes in chkOptionClick.
At first this feels entirely reasonable.
In small applications, it may even remain reasonable.
The problem appears when the operation outgrows the event that started it.
Consider a save button:
procedure TfrmCustomer.btnSaveClick(Sender: TObject);
begin
ValidateFields;
BuildCustomerObject;
SaveCustomerToDatabase;
WriteAuditLog;
UpdateRecentCustomerList;
SendCustomerUpdate;
ShowMessage('Customer saved');
end;
None of those lines looks particularly alarming.
But the form now knows about validation, domain data, persistence, auditing, application history, communications and user notification.
It has accumulated several completely different responsibilities simply because one button happened to initiate them.
The Event Is Not the Responsibility
A useful distinction is this:
The UI event explains why an operation started. It does not define where that operation belongs.
The fact that a button initiates customer registration does not mean customer registration is presentation logic.
The fact that a menu item exports a document does not make exporting a responsibility of the form.
The fact that a checkbox changes application behaviour does not mean the form should know how that behaviour is implemented.
Forms should respond to user interaction.
Other classes should usually perform the work that interaction requests.
Forms Should Coordinate
A useful architectural rule is:
Forms should coordinate; other classes should do the work.
That does not mean forms should contain no code.
A form still has real responsibilities.
It needs to:
- read values from controls;
- display application state;
- enable and disable actions;
- respond to selections and clicks;
- show validation feedback;
- navigate between views;
- translate application results into something useful for the user.
Those are all UI responsibilities.
A form generally should not need intimate knowledge of:
- SQL statements;
- HTTP protocols;
- JSON or XML formats;
- licence algorithms;
- registry layout;
- business rules;
- complex file formats;
- how a remote API authenticates or stores data.
The distinction is not absolute, but it is a useful default.
A Better Button Click
Suppose saving a customer requires validation, persistence and synchronisation with a server.
The form does not need to perform those operations directly.
It can coordinate them:
procedure TfrmCustomer.btnSaveClick(Sender: TObject);
var
Customer: TCustomer;
begin
Customer := CustomerFromForm;
if FCustomerService.Save(Customer) then
ShowSaveSucceeded
else
ShowSaveFailed;
end;
The form still has useful work to do.
It gathers data from the UI.
It initiates the operation.
It presents the outcome.
But it does not need to know how the customer is validated, where it is stored or how it is transmitted.
Those details are encapsulated elsewhere.
The Immediate Payoff Is Reuse
The benefit becomes obvious the moment the same operation is needed somewhere else.
Perhaps customer saving must now happen from an import process.
Or from another form.
Or from a command-line tool.
Or from an automated test.
If the logic lives inside TfrmCustomer, there is suddenly a problem.
The tempting solutions are usually poor ones:
Call a method on the form.
Move the method to public.
Create the form invisibly just to use its code.
Copy and paste the operation somewhere else.
None of those fixes addresses the underlying issue.
The behaviour never belonged to the form in the first place.
If it lives in a service or domain class, the problem largely disappears.
UI Reuse and Business Reuse Are Different Things
Delphi developers are often comfortable with visual inheritance, frame reuse and shared form ancestors.
Those can all be useful for reusing presentation.
But reusing a form hierarchy is not the same as separating application behaviour.
If several forms inherit from a common ancestor containing database access, validation, HTTP code and application rules, the logic has not really been decoupled from the UI.
It has simply been moved to a different UI class.
That can make the structure look cleaner while leaving the same architectural dependency underneath.
Data Modules Can Become Forms Without Controls
Data modules are another common Delphi destination for code that no longer fits comfortably on a form.
Used well, they are excellent places for non-visual components and related infrastructure.
Used carelessly, they can become enormous global service locators containing every dataset, connection, configuration object and unrelated piece of business logic in the application.
Moving code from a form to a data module does not automatically improve the architecture.
The same question still applies:
What is this object actually responsible for?
If the answer requires several unrelated sentences, the responsibilities probably still need separating.
The Form Should Not Be Your Domain Model
Another warning sign is when application state exists only inside controls.
The customer's name is whatever happens to be in edtName.Text.
The selected product is whatever happens to be selected in a combo box.
The document state is scattered between edits, checkboxes, grids and hidden control properties.
That makes the form both the presentation and the data model.
It also means any code needing that information must reach into the form to get it.
A better approach is usually for the UI to represent an underlying object or state.
The form displays that state and allows the user to edit it.
The state itself exists independently of the controls.
That separation makes code easier to test, easier to reuse and considerably easier to reason about.
Global Forms Make the Problem Worse
Delphi's traditional global form variables make it especially easy for one form to reach directly into another.
formMain.lblStatus.Caption := 'Processing...';
formSettings.SaveSettings;
formCustomer.ReloadCustomer;
This is convenient.
It is also hidden coupling.
The calling code now depends on those forms existing, being initialised and exposing the expected controls or methods.
Over time, forms begin talking directly to forms, data modules talk directly to forms, and background operations reach into whichever window happens to own the state they need.
The application becomes difficult to untangle because the user interface is no longer just displaying the system.
It is the system.
Thin Forms Are Easier to Change
Keeping forms relatively thin produces a useful side effect: UI redesign becomes much less frightening.
Suppose a workflow currently uses a modal dialog.
Later, that dialog needs to become a page inside the main window.
If all the behaviour lives inside the dialog class, the change is substantial.
If the dialog merely coordinates a service, most of the application behaviour already exists independently of the UI.
The new page can use the same service.
This is one of the clearest signs of healthy separation:
You can change how an operation is presented without rewriting how the operation works.
Testing Exposes the Difference
Testability is a useful architectural diagnostic.
Ask:
Can I test this behaviour without creating the form?
If the answer is no, consider why.
Sometimes the behaviour genuinely is visual and belongs to the form.
But if the thing being tested is pricing logic, validation, file parsing, registration, persistence or data transformation, requiring a form is usually a sign that responsibilities have become mixed.
A well-separated service can often be instantiated and tested with ordinary objects.
No controls.
No window handles.
No application message loop.
Just the behaviour being tested.
This Is Not an Argument for Empty Forms
There is a danger of taking any architectural rule too far.
A form does not need a presenter, view model, controller, service and command object merely to close itself when the user presses Cancel.
Code which is genuinely about the interface belongs in the interface.
For example:
procedure TfrmCustomer.edtSearchChange(Sender: TObject);
begin
btnClearSearch.Enabled := edtSearch.Text <> '';
end;
There is little value in abstracting that away.
The goal is not to remove code from forms.
The goal is to remove code that has no reason to depend on a form.
A Useful Test for Form Code
When adding behaviour to a form, ask a simple question:
Would this code still make sense if the user interface were replaced tomorrow?
If the answer is yes, it may belong somewhere else.
Validation rules would still exist.
File parsing would still exist.
Database access would still exist.
HTTP communication would still exist.
Business rules would still exist.
The exact position of a button would not.
That is a useful dividing line.
Don't Replace One God Object With Another
Once a large form starts being broken apart, there is another trap waiting.
Everything gets moved into:
TApplicationManager
The form becomes beautifully small.
Unfortunately, the new manager now contains validation, persistence, printing, networking, configuration and every other responsibility the form used to have.
Nothing important has changed.
The god object has merely moved.
The same architectural rule still applies:
Don't make one thing do two things.
Extract responsibilities, not just methods.
Refactor Gradually
None of this means a mature Delphi application needs to be redesigned from scratch.
In fact, that is usually the worst way to approach it.
When working in an existing application, useful boundaries can be introduced gradually.
The next time a large event handler needs significant modification, identify the piece of behaviour that is actually changing.
Move that responsibility into a class with a clear purpose.
Let the form call it.
The next time another feature needs the same behaviour, reuse that class rather than the form.
Over time the application's boundaries improve without requiring a dangerous rewrite.
Keep the UI at the Edge
The user interface is important.
It is how people experience the software.
But architecturally, it is usually best treated as an edge of the system rather than its centre.
The form should translate user intent into application operations.
It should display the resulting state.
It should not need to contain the entire mechanism in between.
Delphi makes it incredibly easy to write code directly into a form.
Use that convenience for what forms are good at.
Presentation.
Interaction.
Coordination.
Let the rest of the application live somewhere that does not disappear the moment you redesign the window.
Your forms are how users interact with your application.
They should not have to be the application.