![]() |
Carousel
|
What is a composite application?
When application is developed in a composite manner, it is divided into separate client components that can be individually developed, tested, and deployed. Each component should encapsulate a set of related concerns and have a distinct set of responsibilities. By features:
By service layers (shared, business logic, presentation layer):
A larger application may have components organized with combining approach – by features and by layers:
Different kinds of component providers are used for component registering (statically in code, at start time and at run time, by demand). They are loaded through proxy component, using QLibrary loader. Then after loading, their starting order is resolved so, that parents start before their children, and components are started by the IComponentManager. During the start components register services and object factories that are consumed by other components, undoable commands and build interactive structure (menu or/and tool bar items, docking widgets, etc.).
Composite application, based on many loosely coupled components, should provide some way to communicate between the components. They need to interact to contribute model content and receive notifications based on user actions. There are few ways of providing such communication – shared services, publisher/subscriber notifications and commanding.
Main method of communication between the components is shared services, obtained from the Service Locator. During startup components register their services on the central registry, called IServiceLocator. It is passed as one of the arguments to the startup method. Although you can add a concrete class, it is recommended that services are registered and retrieved from the locator by the abstract pure interfaces. This allows client code to use services without reference to the concrete implementation. In case where interfaces for shared services are in the one single library, it even is not required a static reference (.lib on Windows; on UNIX-like system it does not required static reference as long as components consume only pure interfaces) to the component library.
Another way to communicate between loosely coupled components is a mechanism based on the notifications. It allows publishers and subscribers to communicate through notifications (for example, using already implemented message delivering – Qt Events) and still do not have a direct reference to each other. But, although it could end with a big mess when one notification generated another and so on, it still could be useful to communicate between business logic, like presenters and controllers.
Use commands, based on business logic (like InstallComponentsCommand or EnableComponentCommand), in response of the user actions, such as clicking on a command trigger (for example, menu item or dialog/tool bar button). You could instantiate required command using service locator in response on the specific slot (or other user input handler), setup it according to the user inputs and execute it through undo stack.
Shared services work good when components just want to communicate each other. But only communication is not enough, components have to instantiate objects from other components. Object factories go here to avoid tight coupling. Using factories you can map interface type to the concrete type or, even, register a factory method for the specific interface type. Then, when we have such factories it is easier to instantiate some object with large amount of nested dependencies. It allows us to create object without any knowing of it dependencies and create ready-to-user object.
Also, all types could be remapped by other components (for example, dialogs could be easily redesigned and remapped), and old code should not be changed. It also allows creating and using objects from the scripts. For example, you can create InstallComponentsCommand command inside the script, set up it properly and push it to the undo stack.
Architecture should also declare a set of limitations: how, when and where can we use some of the described above mechanisms. Service locator and objects factory could be dangerous in using, when you decide that it is very convenient to have a reference to the locator in every object. Main purpose of the locator is to fill-in dependencies between components during their starting time. So I’ve introduced the concept of life-time phases.
There are two main phases: configuration and execution. An execution phase is an ordinal application state, in which you response on the user interaction. It is almost does not matter whether that application is composite or not. So on the execution phase I try to limit service locator using, because main controllers, handlers and other classes are created, configured, dependencies are injected, UI and domain model trees are built and so on.
But during the configuration phase service locator is actively used by the components:
Because services are registered at the component start up time and unregistered at its shutdown time, their lifetime is at least not shorter than component other objects lifetime. Also because neither component class itself nor GUI classes are not covered by the unit testing, the limitation is using service locator (not services!) only at the configuration phase on the infrastructure level, and on the execution phase on the presentation layer: operations/dialogs/presenters/views (GUI), components and so on.
All other domain objects should not have a dependency from the service locator, but they could have dependencies from the other services. And just because you already know what exactly other objects need, you should not get them whole locator, but just dependencies.
A sub-class of Bootloader starts registration and configuration phases. It creates and registers service locator itself and widely spread services, like logger façade, component management and, optionally, main window for the GUI applications. It is implemented as a sequence of pairs “create<>() - configure<>()” methods, and each of them could be overridden. For example, to use your own logger system it is just needed to override createLoggerEngine() method.
To start new application you also should to override createComponentProvider() or configureComponentProvider() method to determine way in which your application will be populated by the components. For example, here is a component provider which will load components from the "./components" directory at the start-time and which also has four built-in components, configured statically at the compile-time:
After registration all basic shared services and components are ready to start. They are started on the configuration phase. Here is a simple example of configuration phase:
Then you can use Show components Operation right from scripting or as a response on the user interaction (as were said later, presentation layer could has a dependency from the service locator to use registered factories). Moreover, someone could re-register Show components Operation to show new dialog.
Every component could extend UI with widgets, dialogs and operations (QAction like – menu items, toolbar buttons), so one common approach is needed to control all that stuff.
Dialogs creating. Dialog service is responsible for modal dialog showing: it can create and show dialog for registered Model type. The specified model will be passed to the dialog, so client code just has to instantiate Model object, without any knowing about concrete type about dialog widget:
Data rendering (classic MVC): Qt View-Model, where each Qt Model has dependency on business model (Data). So, Model can render Data objects, subscribes to Data changing. To change Data Commands should be used, because commands could be undo-able, could contains complicated logic and could be used from the other places. Also they could be created and used from the different places (e.g. from scripting). Model creates specified command with all dependencies using factory (because model is in a presentation layer), sets it up and pushes to the QUndoStack. Note, that such approach is used only for the different kinds of dialogs and other data widgets, but not for drawing (2D/3D) and other stuff.
Components could provide new dock widgets for rendering new information, that can be registered on the IDockWidgetCatalog; New dialogs can be mapped on Model types using IDialogService;
Interaction approach (menu items and tool handlers):
TODO