Carousel
 All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Properties Pages
Composite application overview

Composite application overview

What is a composite application?

CompositeAppOverview.png

Content

  1. Partition of an application into components Partition of an application into components;
  2. Component management: registering, discovering, loading and starting components;
  3. Communication between the components;
  4. Managing dependencies between the components (object factories);
  5. Life-time phases (how control some classes usage);
  6. Presentation and model responsibilities and organization;
  7. JavaScript support;
  8. Unit testing;
  9. Examples;

Partition of an application into components

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:

ByFeature.png

By service layers (shared, business logic, presentation layer):

ByLayer.png

A larger application may have components organized with combining approach – by features and by layers:

ByFeatureAndLayer.png

Component management

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.).

Communication between the components

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.

Todo:
example.

Managing dependencies between the components (object factories)

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.

Todo:
code for register a factory method for the specific interface type.

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.

Life-time phases (how control some classes usage)

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.

LifeTimePhases.png

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:

  1. Services, object factories and type mapping are consumed and registered
  2. Dependencies, new or obtained from the locator, are injected to roots of component model and to the new views and GUI interaction elements (menu items, tool bars, dialogs). They are passed to the constructors to create only ready-to-use objects
  3. New GUI is registered for permanent (views) or temporary usage (dialogs). Dialogs and views are mapped to the specified model types (which they are intend to show)

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.

Registration phase

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:

IComponentProvider *MyBootloader:: createComponentProvider ()
{
provider->addProvider(new DirectoryComponentProvider("./components"));
provider->registerComponent(new UndoComponent());
}

Configuration phase

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:

  1. The Component manager begins the components startup process, and loads and initializes the Interactivity component;
  2. In the startup of the Interactivity component, it registers the IDialogService with the locator;
  3. The Component manager then starts the CS management UI component (the order of component starting is based on the component dependencies);
  4. The CS management UI component registers the ComponentsDialog widget for the ComponentDefinitionsModel in the IDialogService, so then client code can visualize ComponentDefinitionsModel without knowing of concrete widget (the widget could be changed by other component in run-time).
ConfigurationPhase.png

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.

Presentation and model responsibilities and organization

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:

void ShowComponentsOperation::execute()
{
IDialogService *dialogService = m_serviceLocator->locate<IDialogService>();
dialogService->showDialog(model);
delete model;
}

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):

  1. There are two QAction sub-classes: Operation and Tool, which are mapped to the use cases/use case steps;
  2. Buttons and menu items are the simplest types of operations. Buttons generally appear as icons on toolbars and menu items appear in menus. A simple action is performed when the button or menu item is clicked;
  3. Tools are similar to operations but they also require interaction with the application's display. The Pan operation in the demo project is a good example of a tool - you click and drag a map to show another map content;
  4. Only one tool could active at the time, and all user interactions with working widgets will be dispatched to that tool using InputInterceptor class, so to create a new Tool you just should override default empty methods like ITool::onMouseDown(), ITool::onMouseMove(), ITool::onDoubleClick(), etc;
  5. As was said later, operations could use undoable commands to modify data;
  6. New menu items and toolbars with tools and operations can be registered and added to the GUI using IMenuCatalog, IOperationCatalog and IToolBarCatalog;

JavaScript support

TODO

Unit testing

  1. All classes take dependencies through constructors, so you can mock them;
  2. Classes, that have some default behavior, but it might be changed, have setters for the delegates, who encapsulates that behavior, so you cam mock them;
  3. All utility objects are created inside the class using virtual factory method, which returns a default utility instance, but you also can override it and return mock utility;
  4. As was said later, operations could use undoable commands to modify data;

Examples

  1. New application: demos/painter/app/main.cpp;
  2. New dialog: src/components/componentsystemui/ComponentSystemUIComponent.cpp;
  3. New widget: demos/painter/cartoUI/CartoUIInteractiveExtension.cpp;
  4. New tools and operations: demos/painter/navigationOperations/NavigationOperationsInteractiveExtension.cpp;