public interface IAtomBindable {
void Bind();
IEventSource getAtomEventSource();
}
Architecture That Stays Consistent
- Platform-independent architecture
- SOLID principles implementation
- Clean separation of concerns
- Consistent behavior across platforms
The Invariant Pattern Concept
Introduction
Invariant Pattern is a design pattern for creating client applications, that is, applications with a user interface (UI). Like any universal design pattern, it is independent of the programming language or the specific UI component library used. Applications built using this pattern are easy to extend and test. They are open to and adaptable for AI-based extensions, with the application's design evolving independently of its underlying logic.
The Invariant Pattern (or Orlov Pattern, after its creator) is based on the concept of logical atoms. The functionality of any application can be viewed as a set of logical atoms, each with a unique name, whose processing follows a unified workflow. The concept of an application atom is easiest to understand through the example of a typical Create-Read-Update-Delete (CRUD) application. Each atom naturally corresponds to a logical scope — creating a new product, reading a list of existing products, updating or deleting a product.
Atom
public enum LogicAtomEnum {
Undefined,
CreateEmployeeAtom,
ReadEmployeesAtom,
...
}
In this context, the term "atom" does not imply indivisibility but highlights the boundaries of related functionality within reasonable and justified limits. Typically, this should be guided by the Single Responsibility Principle (SRP). DrawLineAtom, SendMessageAtom, CheckInternetConnectionAtom, etc., are all typical atoms, though CheckInternetConnectionAtom is an extremely simple. Thus, the key concept of an atom has been introduced.
Let's take a fresh look at what we call a Logic Atom. An atom is essentially an identifier for a well-defined and standardized code scope. But what exactly does an atom like CreateEmployeeAtom represent? What are its key components?
At its core, an atom defines specific locations in the code where the following processes take place:
-
Data Collection from the UIThe application gathers input data from the client-side (UI) specific to this atom. For CreateEmployeeAtom, this would typically include fields like First Name, Last Name, Email, and so on.
-
Event Source CreationAn instance of an IEventSource interface is created. The simplest implementation might be a class like SimpleEventSource. This entity serves as the event trigger—a command that initiates the atom's core functionality.
-
Binding Data to the Business LogicThe collected data (from step 1) is passed into the business logic via a standard Binder class. This logic consists of two main activities: validation and execution.
-
Validation ProcessBefore executing the atom, the system performs checks to ensure it can proceed. For CreateEmployeeAtom, this might involve verifying internet connectivity, ensuring access to the database, or checking if a user with the same data (from step 1) already exists. If any of these conditions fail, the request to create a new employee may be rejected.
-
Execution of Core FunctionalityFinally, the core action of the atom takes place. In the case of CreateEmployeeAtom, this means saving the new employee’s data into the database.
These five steps define the Logical Atom concept within an application. Each atom follows this structured approach, ensuring consistency, modularity, and clarity in the application’s architecture.
Binder
The separation of logic from presentation is achieved by the Binder class, which has one key method—Bind—with a unified signature. Despite its apparent simplicity, this class handles a wide range of tasks. But before we dive into the details, let's rethink everything from scratch… and we'll do so using the example of the CreateProductAtom.
@Override
public void Bind() {
_Binder.BindSyncValidationSyncLogic(
_AtomEventSource,
_Specifier.GetAtomSpecification(
LogicAtomEnum.AddSomethingAtom, LogicAtomPlatformEnum.JavaFX),
// 1. Form parameters to logic
// ==========================
() -> {
var atomName = _View.getInputDataFromView(LogicAtomEnum.AddSomethingAtom);
return UIDataValidation.ValidateAddAtom(atomName.getAsType(String.class));
},
...
First, in order to process an atom, we need an Event Source — an abstract button (or an abstract mouse click), which, when pressed, will initiate the creation of a new product. Second, to create a new product, we need the product's data — such as its name, SKU, seller's phone number, and perhaps a description, etc. This data is obtained from the UI and should first be validated.
For example, we need to filter out obvious nonsense in the description, or disallow empty fields in the product description, phone number field, and so on. After this initial validation, the data must be passed to the logic layer and processed there. The processing may require user confirmation or could even end with an error. Finally, once the process of adding the new product has been completed in the logic layer, the UI needs to be notified about this, at least to update the list of products.
Now, back to the Binder class. All the tasks mentioned above (such as adding a new product) are unified into a single workflow, where Binder acts as the intermediary between the UI and the logic, facilitating the transfer back and forth. It's important to note that the Binder class is designed in such a way that it knows nothing about the buttons or the user interface platform itself. It's like a courier that simply carries packages or envelopes (without peeking inside) and places them on shelves according to certain internal rules.
Validation
Like all components of a Logic Atom, the validation block follows a standardized approach. Its primary role is to determine whether the atom’s main functionality can be executed or not.
@Override
public OperationResult ValidateAsyncObtainNews(CustomObject customObject) {
// 1. Check Internet connection
// ===========================
try {
URL url = new URL("https://www.company.com");
URLConnection connection = url.openConnection();
connection.connect();
...
There is one important nuance to keep in mind when implementing the validation method, and the best way to illustrate it is through an example.
Returning to CreateEmployeeAtom, consider this:
-
Checking whether a first name, last name, or email address is formatted correctly is not part of the logic atom’s validation process.
-
Such checks belong to an initial (UI-level) validation stage, which takes place closer to the user interface and ensures that input data meets basic formatting rules.
The logical (final) validation, on the other hand, is responsible for determining whether the operation itself can proceed—such as checking for internet connectivity, database availability, or ensuring that an employee with the same details does not already exist.
Logic
The most straightforward part of the infrastructure is the logic layer. Of course, this consists of methods that return results. These methods also have a unified signature, and according to internal conventions, their names include the name of the corresponding atom.
@Override
public CustomObject CommandAsyncLogicNoConfirmationObtainNews(CustomObject customObject) {
NewsItemDTO dto_1 =
new NewsItemDTO("SuperApp just released",
"1",
"Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
"https://www.company.com");
...
Any mid-level programmer will have no trouble understanding the code of the demo applications, which can be found via the provided template.