10 KiB
Overview
The framework layer's most basic responsibility is allowing individual software components to communicate. The software components it recognizes are:
- Extensions: Individual units of functionality that can be added to or removed from Open MCT Web. Extension categories distinguish what type of functionality is being added/removed.
- Bundles: A grouping of related extensions (named after an analogous concept from OSGi) that may be added or removed as a group.
The framework layer operates by taking a set of active bundles, and exposing extensions to one another as-needed, using dependency injection. Extensions are responsible for declaring their dependencies in a manner which the framework layer can understand.
#direction: down
[Open MCT Web|
[Dependency injection framework]-->[Platform bundle #1]
[Dependency injection framework]-->[Platform bundle #2]
[Dependency injection framework]-->[Plugin bundle #1]
[Dependency injection framework]-->[Plugin bundle #2]
[Platform bundle #1|[Extensions]]
[Platform bundle #2|[Extensions]]
[Plugin bundle #1|[Extensions]]
[Plugin bundle #2|[Extensions]]
[Platform bundle #1]<->[Platform bundle #2]
[Plugin bundle #1]<->[Platform bundle #2]
[Plugin bundle #1]<->[Plugin bundle #2]
]
The "dependency injection framework" in this case is AngularJS. Open MCT Web's framework layer is really just a thin wrapper over Angular that recognizes the concepts of bundles and extensions (as declared in JSON files) and registering extensions with Angular. It additionally acts as a mediator between Angular and RequireJS, which is used to load JavaScript sources which implement extensions.
[Framework layer|
[AngularJS]<-[Framework Component]
[RequireJS]<-[Framework Component]
[Framework Component]1o-*[Bundles]
]
It is worth noting that no other components are "aware" of the framework component directly; Angular and Require are used by the framework components, and extensions in various bundles will have their dependencies satisfied by Angular as a consequence of registration activities which were performed by the framework component.
Application Initialization
The framework component initializes an Open MCT Web application following a simple sequence of steps.
[<start> Start]->[<state> Load bundles.json]
[Load bundles.json]->[<state> Load bundle.json files]
[Load bundle.json files]->[<state> Resolve implementations]
[Resolve implementations]->[<state> Register with Angular]
[Register with Angular]->[<state> Bootstrap application]
[Bootstrap application]->[<end> End]
- Loading bundles.json. A file named
bundles.json
is loaded to determine which bundles to load. Bundles are given in this file as relative paths which point to bundle directories. - Load bundle.json files. Individual bundle definitions are loaded; a
bundle.json
file is expected in each bundle directory. - Resolving implementations. Any scripts which provide implementations for extensions exposed by bundles are loaded, using RequireJS.
- Register with Angular. Resolved extensions are registered with Angular, such that they can be used by the application at run-time. This stage includes both registration of Angular built-ins (directives, controllers, routes, constants, and services) as well as registration of non-Angular extensions.
- Bootstrap application. Once all extensions have been registered, the Angular application is bootstrapped.
Architectural Paradigm
[Extension]
[Extension]o->[Dependency #1]
[Extension]o->[Dependency #2]
[Extension]o->[Dependency #3]
Open MCT Web's architecture relies on a simple premise: Individual units (extensions) only have access to the dependencies they declare that they need, and they acquire references to these dependencies via dependency injection. This has several desirable traits:
- Programming to an interface is enforced. Any given dependency can be swapped out for something which exposes an equivalent interface. This improves flexibility against refactoring, simplifies testing, and provides a common mechanism for extension and reconfiguration.
- The dependencies of a unit must be explicitly defined. This means that it can be easily determined what a given unit's role is within the larger system, in terms of what other components it will interact with. It also helps to enforce good separation of concerns: When a set of declared dependencies becomes long it is obvious, and this is usually a sign that a given unit is involved in too many concerns and should be refactored into smaller pieces.
- Individual units do not need to be aware of the framework; they need only be aware of the interfaces to the components they specifically use. This avoids introducing a ubiquitous dependency upon the framework layer itself; it is plausible to modify or replace the framework without making changes to individual software components which run upon the framework.
A drawback to this approach is that it makes it difficult to define "the architecture" of Open MCT Web, in terms of describing the specific units that interact at run-time. The run-time architecture is determined by the framework as the consequence of wiring together dependencies. As such, the specific architecture of any given application built on Open MCT Web can look very different.
Keeping that in mind, there are a few useful patterns supported by the framework that are useful to keep in mind.
The specific service infrastructure provided by the platform is described in the Platform Architecture.
Extension Categories
One of the capabilities that the framework component layers on top of AngularJS is support for many-to-one dependencies. That is, a specific extension may declare a dependency to all extensions of a specific category, instead of being limited to declaring specific dependencies.
#direction: right
[Specific Extension] 1 o-> * [Extension of Some Category]
This is useful for introducing specific extension points to an application. Some unit of software will depend upon all extensions of a given category and integrate their behavior into the system in some fashion; plugin authors can then add new extensions of that category to augment existing behaviors.
Some developers may be familiar with the use of registries to achieve similar characteristics. This approach is similar, except that the registry is effectively implicit whenever a new extension category is used or depended-upon. This has some advantages over a more straightforward registry-based approach:
- These many-to-one relationships are expressed as dependencies; an extension category is registered as having dependencies on all individual extensions of this category. This avoids ordering issues that may occur with more conventional registries, which may be observed before all dependencies are resolved.
- The need for service registries of specific types is removed, reducing the number of interfaces to manage within the system. Groups of extensions are provided as arrays.
Composite Services
Composite services (registered via extension category components
) are
a pattern supported by the framework. These allow service instances to
be built from multiple components at run-time; support for this pattern
allows additional bundles to introduce or modify behavior associated
with these services without modifying or replacing original service
instances.
#direction: down
[<abstract> FooService]
[FooDecorator #1]--:>[FooService]
[FooDecorator #n]--:>[FooService]
[FooAggregator]--:>[FooService]
[FooProvider #1]--:>[FooService]
[FooProvider #n]--:>[FooService]
[FooDecorator #1]o->[<state> ...decorators...]
[...decorators...]o->[FooDecorator #n]
[FooDecorator #n]o->[FooAggregator]
[FooAggregator]o->[FooProvider #1]
[FooAggregator]o->[<state> ...providers...]
[FooAggregator]o->[FooProvider #n]
[FooDecorator #1]--[<note> Exposed as fooService]
In this pattern, components all implement an interface which is standardized for that service. Components additionally declare that they belong to one of three types:
- Providers. A provider actually implements the behavior (satisfies the contract) for that kind of service. For instance, if a service is responsible for looking up documents by an identifier, one provider may do so by querying a database, while another may do so by reading a static JSON document. From the outside, either provider would look the same (they expose the same interface) and they could be swapped out easily.
- Aggregator. An aggregator takes many providers and makes them behave as one. Again, this implements the same interface as an individual provider, so users of the service do not need to be concerned about the difference between consulting many providers and consulting one. Continuing with the example of a service that looks up documents by identifiers, an aggregator here might consult all providers, and return any document is found (perhaps picking one over the other or merging documents if there are multiple matches.)
- Decorators. A decorator exposes the same interface as other components, but instead of fully implementing the behavior associated with that kind of service, it only acts as an intermediary, delegating the actual behavior to a different component. Decorators may transform inputs or outputs, or initiate some side effects associated with a service. This is useful if certain common behavior associated with a service (caching, for instance) may be useful across many different implementations of that same service.
The framework will register extensions in this category such that an aggregator will depend on all of its providers, and decorators will depend upon on one another in a chain. The result of this compositing step (the last decorator, if any; otherwise the aggregator, if any; otherwise a single provider) will be exposed as a single service that other extensions can acquire through dependency injection. Because all components of the same type of service expose the same interface, users of that service do not need to be aware that they are talking to an aggregator or a provider, for instance.