mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 02:29:24 +00:00
Compare commits
13 Commits
vue-table-
...
open462pro
Author | SHA1 | Date | |
---|---|---|---|
2939267f2f | |||
fda6608264 | |||
60784aa56f | |||
7326320954 | |||
5dbb7658ac | |||
336b76f28c | |||
5bb255dabf | |||
6a0bc1c48e | |||
28741297c1 | |||
585355cd66 | |||
1adbacd70b | |||
366d467315 | |||
70a4c7d624 |
424
docs/src/design/proposals/Registration.md
Normal file
424
docs/src/design/proposals/Registration.md
Normal file
@ -0,0 +1,424 @@
|
||||
# Developer Use Cases
|
||||
|
||||
1. Extending and maintaining Open MCT itself.
|
||||
2. Adapting and customizing Open MCT for use in specific missions.
|
||||
3. Developing features for use with Open MCT across multiple different
|
||||
missions.
|
||||
|
||||
# Scope
|
||||
|
||||
As demonstrated by the existing APIs, writing plugins is sufficient to
|
||||
satisfy the three use cases above in the majority of cases. The only feature
|
||||
which is known to be unsatisfiable by plugins is plugin support itself.
|
||||
|
||||
As such, prefer to keep "plugin-external" components small and simple, to
|
||||
keep the majority of development in plugins (or plugin-like components.)
|
||||
|
||||
The "registration API" described in this document is limited to that scope:
|
||||
It describes classes and patterns that can allow plugins to interact,
|
||||
while making minimal assumptions about what specific functionality is to
|
||||
be implemented in these plugins.
|
||||
|
||||
# Problems to Address
|
||||
|
||||
1. Dependencies between plugins are implicit; a plugin may fail if its
|
||||
required dependencies are not included, without any clear indication
|
||||
of why this failure has occurred. Significant familiarity with
|
||||
Open MCT is typically required to debug in these circumstances.
|
||||
2. Extension points are often implicit; no specific plugin is
|
||||
identifiably responsible for defining any specific extension category
|
||||
or composite service. These are instead paired by string matching.
|
||||
This makes it difficult to follow how the application is initialized
|
||||
and how objects are passed around at run-time, and creates an
|
||||
additional documentation burden, as these named extension points do not
|
||||
fit into more standard API documentation.
|
||||
3. Reuse of components between plugins is limited. Exposing base classes
|
||||
from one plugin and reusing them from another is overly-difficult.
|
||||
|
||||
# Principles
|
||||
|
||||
1. The Registration API is exposed as a set of classes in one or more
|
||||
namespaces. This supports reuse and extension using standard,
|
||||
well-known object-oriented patterns. A composition-oriented style
|
||||
is still supported and encouraged, but not enforced.
|
||||
2. Extension points should be expressed as objects with known interfaces.
|
||||
(More specifically, they should be _expressible_ in this fashion;
|
||||
it is out of scope for the Registration API to expose any specific
|
||||
extension points.)
|
||||
3. Dependencies that are intended for injection should be expressed as
|
||||
arguments to a constructor. The Registration API does not need to
|
||||
stipulate this, but should at least be compatible with this. This
|
||||
is both to allow for compatibility with existing code, and to allow
|
||||
for clear documentation of the dependencies of specific components
|
||||
("to construct one of these, you need one of those.")
|
||||
4. The Registration API should accept minimal responsibility in order
|
||||
to impose minimal constraints. For instance, it should not be
|
||||
responsible for performing script-loading. Its sole responsibility
|
||||
is to facilitate communication between components.
|
||||
5. The Registration API should continue to support ubiquitous use of
|
||||
patterns that have proven useful for plugin-based extensibility
|
||||
(the Registry, Composite, and Decorator patterns, specifically.)
|
||||
However, it should be designed such that knowledge of these patterns
|
||||
is only required when it is appropriate to the specific task or
|
||||
activity at hand. (For instance, you should not need to be familiar
|
||||
with decorators in order to simply register something.)
|
||||
|
||||
# Interfaces
|
||||
|
||||
In keeping with the scope of the Registration API, the interfaces
|
||||
described here are sufficient to:
|
||||
|
||||
* Describe the set of plugins in use for a particular instance of
|
||||
Open MCT, and initiate their behavior.
|
||||
* Support common patterns by which plugins can utilize and expose
|
||||
defined extension points.
|
||||
|
||||
Notably, there is no interdependency between these two sets of
|
||||
behavior; one could use the base classes for extension points
|
||||
independently of the plugin mechanism, and vice versa. This both
|
||||
ensures loose coupling within the Registration API, and also
|
||||
allows for greater flexibility for developers implementing plugins.
|
||||
|
||||
## Application-level Interfaces
|
||||
|
||||
```nomnoml
|
||||
[Application |
|
||||
install(plugin : Plugin)
|
||||
uninstall(plugin : Plugin)
|
||||
run()
|
||||
]
|
||||
|
||||
[Plugin |
|
||||
initialize()
|
||||
start()
|
||||
]
|
||||
|
||||
[Application]<:-[MCT |
|
||||
core : Plugin
|
||||
ui : Plugin
|
||||
policy : Plugin
|
||||
...etc
|
||||
]
|
||||
[Application]-o[Plugin]
|
||||
```
|
||||
|
||||
Summary of interfaces:
|
||||
|
||||
* `Application` represents a complete piece of software that has been
|
||||
composed of plugins.
|
||||
* `install(plugin)` adds a plugin to this application.
|
||||
* `uninstall(plugin)` removes a plugin from this application.
|
||||
* `run()` starts the application. This will first initialize all
|
||||
plugins, then start all plugins.
|
||||
* `Plugin` represents a unit of functionality available for use within
|
||||
applications. It exposes methods to be triggered at various points
|
||||
in the application lifecycle. A plugin is meant to be "single-use";
|
||||
multiple calls to `initialize()` and/or `start()` should have no
|
||||
effect.
|
||||
* `initialize()` performs any configuration and/or registration
|
||||
associated with this plugin.
|
||||
* `start()` initiates any behavior associated with this plugin that
|
||||
needs to run immediately after the application has started. (Useful
|
||||
for a "bootstrap" plugin.)
|
||||
* `MCT` is an instance of an `Application` that self-installs various
|
||||
plugins during its constructor call. It also exposes these same
|
||||
plugins as public fields such that other applications may access
|
||||
them (to `uninstall` them, for instance, or to pass them into other
|
||||
plugins.)
|
||||
|
||||
Rationale for various interfaces:
|
||||
|
||||
* `Application` separates out a core responsibility of Open MCT (plugin
|
||||
composition) from the specific details of Open MCT (the set of plugins
|
||||
which compose it.)
|
||||
* `install` allows plugins to be added (central to plugin support.)
|
||||
* `uninstall` allows plugins to be removed in circumstances where they
|
||||
are unwanted; have observed practical cases where this is desirable.
|
||||
* `run` separates instantiation of the application from the initiation
|
||||
of its behavior.
|
||||
* `Plugin` provides an interface for `Application` to use when accepting
|
||||
plugins, and a base class for plugin developers to extend from.
|
||||
* `initialize` is useful to `Application`, which wants to implement
|
||||
`run` in a manner which wholly separates initialization (the wiring
|
||||
up of various services/registries) from bootstrapping.
|
||||
* `start` is useful to `Application`, to start any run-time behavior
|
||||
once the application is fully-configured.
|
||||
* `MCT` is useful to producers of software built on Open MCT, who would
|
||||
like a baseline set of functionality to build upon.
|
||||
|
||||
Applications built on Open MCT are expected to be exposed as classes
|
||||
which extend `MCT` and add/remove plugins during the constructor
|
||||
call. (This is a recommended pattern of use only; other, more
|
||||
imperative usage of this API is equally viable.)
|
||||
|
||||
## Extension Points
|
||||
|
||||
```nomnoml
|
||||
[Provider<X,S> |
|
||||
get() : S
|
||||
register(factory : (function () : S))
|
||||
decorate(decorator : (function (S) : S))
|
||||
compose(compositor : (function (Array<X>) : S))
|
||||
]
|
||||
|
||||
[Provider<X,S>]<:-[Provider<T,Array<T>>]
|
||||
[Provider<T,Array<T>>]<:-[Registry<T>]
|
||||
[Provider<X,S>]<:-[Provider<S,S>]
|
||||
[Provider<S,S>]<:-[ServiceProvider<S>]
|
||||
```
|
||||
|
||||
Omitted from this diagram (for clarity) are `options` arguments to
|
||||
`register`, `decorate`, and `compose`. This argument should allow,
|
||||
at minimum, a `priority` to be specified, in order to control ordering
|
||||
of registered extensions.
|
||||
|
||||
* `Provider<X, S>` is responsible for providing objects of type `S` based
|
||||
on zero or more instances of objects of type `X` which have been registered.
|
||||
In practice, a `Provider` instance is an extension point within the
|
||||
architecture.
|
||||
* `get() : S` provides an instance of the architectural component, as
|
||||
constructed using the registered objects, along with the highest-priority
|
||||
compositor and and decorators.
|
||||
* `register(factory : (function () : X), [options] : RegistrationOptions)`
|
||||
registers an object (as returned by the provided `factory` function)
|
||||
with this provider. Evaluation of the provided `factory` will be
|
||||
deferred until the first `get()` call to the `Provider`.
|
||||
* `compose(compositor : (function (X[]) : S), [options] : RegistrationOptions)`
|
||||
introduces a new strategy for converting an array of registered objects
|
||||
of type `X` into a single instance of an object of type `S`. The
|
||||
highest-priority `compositor` that has been registered in this fashion
|
||||
will be used to assemble the provided object (before decoration)
|
||||
* `decorate(decorator : (function (S) : S), [options] : RegistrationOptions)`
|
||||
augments behavior of objects provided by `get`, in priority order.
|
||||
* `ServiceProvider<S>` provides analogous support for the _composite services_
|
||||
pattern used throughout Open MCT (which, in turn, is a superset of the
|
||||
functionality needed for plain services.)
|
||||
* `Registry<T>` provides analogous support for _extension categories_, also
|
||||
used ubiquitously through Open MCT.
|
||||
|
||||
# Examples
|
||||
|
||||
The following examples are provided to illustrate the intended usage of
|
||||
the Registration API. Particular attention is given to obeying and
|
||||
utilizing the "dependency injection as code style"
|
||||
[decision from the API Redesign](APIRedesign.md#decisions).
|
||||
|
||||
## Building Applications on Open MCT
|
||||
|
||||
Applications built using Open MCT are expected to extend the `MCT` base
|
||||
class and should typically self-install distinguishing plugins during
|
||||
the constructor call. Any pre-installed plugins that are undesirable
|
||||
should also be uninstalled at this point (to support such usage,
|
||||
`MCT` should expose instances of any installed plugins that are
|
||||
considered optional.)
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
define(['mct', './plugins'], function (mct, plugins) {
|
||||
var MCT = mct.MCT;
|
||||
|
||||
function Variant() {
|
||||
MCT.call(this);
|
||||
|
||||
this.install(new plugins.SomePlugin());
|
||||
this.install(new plugins.SomeOtherPlugin(this.core));
|
||||
|
||||
this.uninstall(this.plugins.persistence.localStorage);
|
||||
}
|
||||
|
||||
Variant.prototype = Object.create(MCT.prototype);
|
||||
|
||||
return Variant;
|
||||
});
|
||||
```
|
||||
|
||||
Running an application build using Open MCT then typically looks like:
|
||||
|
||||
```
|
||||
define(['./Variant], function (Variant) {
|
||||
new Variant().run();
|
||||
});
|
||||
```
|
||||
|
||||
## Writing Plugins
|
||||
|
||||
A plugin for Open MCT should inherit from the `Plugin` base class.
|
||||
|
||||
Plugins will typically use extension points exposed by other plugins;
|
||||
put another way, plugins will typically _depend_ upon other plugins.
|
||||
Consistent with "dependency injection as a code style," the preferred
|
||||
way for plugins to acquire these references is via constructor
|
||||
arguments. (Put another way, in order to use a plugin, you are expected
|
||||
to supply its dependencies.) Note that this is not a requirement,
|
||||
as Open MCT only ever interacts directly with plugin _instances_;
|
||||
any other way of assembling an object with the `Plugin` interface
|
||||
should be compatible.
|
||||
|
||||
Plugins will also typically expose extension points. The preferred
|
||||
way to do this is to expose `Provider` instances as public fields
|
||||
of plugins, but this is a matter of code style, and is not enforced
|
||||
or expected by the Registration API.
|
||||
|
||||
For example, if a plugin depends on the `core` plugin of MCT:
|
||||
|
||||
```
|
||||
define(['mct', './SomeAction'], function (mct, SomeAction) {
|
||||
var Plugin = mct.Plugin,
|
||||
ServiceProvider = mct.ServiceProvider;
|
||||
|
||||
function ExamplePlugin(core) {
|
||||
Plugin.call(this, function () {
|
||||
core.actionRegistry.register(function () {
|
||||
return new SomeAction();
|
||||
});
|
||||
});
|
||||
|
||||
this.someServiceProvider = new ServiceProvider();
|
||||
this.someServiceProvider.register(function () {
|
||||
return new SomeService();
|
||||
});
|
||||
}
|
||||
|
||||
ExamplePlugin.prototype = Object.create(Plugin.prototype);
|
||||
|
||||
return ExamplePlugin;
|
||||
});
|
||||
```
|
||||
|
||||
Using this plugin then looks like:
|
||||
|
||||
```
|
||||
define([
|
||||
'mct',
|
||||
'./ExamplePlugin',
|
||||
'./OtherPlugin'
|
||||
], function (mct, ExamplePlugin, OtherPlugin) {
|
||||
var MCT = mct.MCT;
|
||||
|
||||
function MyApplication() {
|
||||
MCT.call(this);
|
||||
|
||||
this.examplePlugin = new ExamplePlugin(this.core);
|
||||
this.otherPlugin = new OtherPlugin(this.examplePlugin);
|
||||
|
||||
this.install(this.examplePlugin);
|
||||
this.install(this.otherPlugin);
|
||||
}
|
||||
|
||||
MyApplication.prototype = Object.create(MCT.prototype);
|
||||
|
||||
return MyApplication;
|
||||
});
|
||||
```
|
||||
|
||||
## Using Extension Points
|
||||
|
||||
The services and extensions exposed by providers are retrieved via
|
||||
`get` calls to those providers. These calls are expected to occur
|
||||
during registration of other extensions, _or_ in the `start` phase
|
||||
of the plugin lifecycle.
|
||||
|
||||
There are effectively four distinct stages for a plugin:
|
||||
|
||||
* __Pre-initialization__. This is the plugin immediately after its
|
||||
constructor call. Any extension points exposed by the plugin are
|
||||
expected to be defined during this stage.
|
||||
* __Initialization__. Triggered by calling `initialize`; invokes
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
define(['mct', './SomeAction'], function (mct, SomeAction) {
|
||||
var Plugin = mct.Plugin;
|
||||
|
||||
function MyPlugin(core, notificationPlugin) {
|
||||
var notificationServiceProvider =
|
||||
notificationPlugin.notificationServiceProvider;
|
||||
|
||||
Plugin.call(this, function initialize() {
|
||||
// During this stage, extensions may be installed and other
|
||||
// general plugin configuration should occur.
|
||||
// Calls to get should be avoided at this stage, as
|
||||
// providers may not be fully configured.
|
||||
core.actionRegistry.register(function () {
|
||||
// In factory functions, however, get calls are expected;
|
||||
// this is when dependencies actually get injected.
|
||||
// Calls which register/configure extensions should be
|
||||
// avoided at this point.
|
||||
return new SomeAction(notificationServiceProvider.get());
|
||||
});
|
||||
}, function start() {
|
||||
// Any behavior that should occur when the application starts.
|
||||
// All providers should be fully-configured at this point; `get`
|
||||
// calls may be issued freely at this point, and no more
|
||||
// registration should occur. This stage is not useful to most
|
||||
// plugins and this argument would typically be omitted.
|
||||
|
||||
notifications.notificationServiceProvider.get()
|
||||
.notify("Hello world!");
|
||||
});
|
||||
|
||||
// Code in the constructor is run when the plugin is instantiated;
|
||||
// any extension points exposed by the plugin should be declared
|
||||
// here, typically as providers.
|
||||
this.someRegistry = new mct.Registry();
|
||||
}
|
||||
|
||||
MyPlugin.prototype = Object.create(Plugin.prototype);
|
||||
|
||||
return MyPlugin;
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
# Evaluation
|
||||
|
||||
[Identified problems](#problems-to-address) are addressed by this solution:
|
||||
|
||||
1. Dependencies between plugins can be made explicit; a `Plugin` may
|
||||
impose dependencies on other specific `Plugin` subclasses as constructor
|
||||
arguments, disambiguated with JSDoc. The software does not take part
|
||||
in dependency management among plugins; rather, this responsibility is
|
||||
plainly communicated to developers.
|
||||
2. Extension points are made explicit; `Provider` instances must be
|
||||
reachable for plugins to configure, and may be made available as
|
||||
public fields of `Plugin`s. Their types can be clearly documented,
|
||||
usages and interactions can be followed with standard developer tools
|
||||
(e.g. breakpoints), and so on.
|
||||
3. Reuse of classes between plugins is neither facilitated nor impeded
|
||||
by the registration API. If, however, plugins are written following the
|
||||
"expose classes in namespaces" approach, then it is trivially to
|
||||
expose additional classes in these same namespaces.
|
||||
|
||||
This solution offers further benefits:
|
||||
|
||||
* The `Provider` API is robust enough to support the various existing
|
||||
extensions of Open MCT, but its usage is opt-in; plugins are free
|
||||
to expose other (potentially wildly different) means of extension.
|
||||
Usage of `Provider`s is _encouraged_ to promote ubiquitous
|
||||
extensibility, but no limitation is exposed.
|
||||
* By moving everything into classes which accept dependencies, a
|
||||
degree of inflexibility is removed from the architecture. In principle,
|
||||
it should be possible to run multiple instances of `MCT` (with
|
||||
their own service instances, etc.) within the same environment. While
|
||||
this is not specifically desirable, it reflects a generally looser
|
||||
coupling between the software and it environment (no expectation of a
|
||||
`bundles.json`, no usage of global state at the language level or
|
||||
effectively-global state at the RequireJS level, etc.) and implies
|
||||
greater flexibility of the application's components.
|
||||
|
||||
There are some problems with this approach:
|
||||
|
||||
* It is highly sensitive to ordering; does not address the problem of
|
||||
[separating configuration from use](http://www.martinfowler.com/articles/injection.html#SeparatingConfigurationFromUse),
|
||||
but instead leaves this as a problem to solve with code style
|
||||
(requiring familiarity with the system.) This is particularly
|
||||
true with `Provider#get` (don't want to invoke before configuration
|
||||
is finished), but also true for `Application` and `Plugin`.
|
||||
* One approach to mitigate this would be to throw `Error`s when
|
||||
calls are made out-of-order (e.g. configuration after use.)
|
||||
* Some redundancy among interfaces (`Plugin` and `Application` both
|
||||
look a lot like a `Provider`, but of what?)
|
||||
* But, don't want to over-exercise commonalities and end up with
|
||||
unclear interfaces.
|
28
src/mct/Application.js
Normal file
28
src/mct/Application.js
Normal file
@ -0,0 +1,28 @@
|
||||
define([], function () {
|
||||
'use strict';
|
||||
|
||||
function Application(plugins) {
|
||||
this.plugins = plugins || [];
|
||||
}
|
||||
|
||||
Application.prototype.install = function (plugin) {
|
||||
this.plugins.push(plugin);
|
||||
};
|
||||
|
||||
Application.prototype.uninstall = function (plugin) {
|
||||
this.plugins = this.plugins.filter(function (p ) {
|
||||
return p !== plugin;
|
||||
});
|
||||
};
|
||||
|
||||
Application.prototype.run = function () {
|
||||
this.plugins.forEach(function (plugin) {
|
||||
plugin.initialize();
|
||||
});
|
||||
this.plugins.forEach(function (plugin) {
|
||||
plugin.activate();
|
||||
});
|
||||
};
|
||||
|
||||
return Application;
|
||||
});
|
38
src/mct/LegacyPlugin.js
Normal file
38
src/mct/LegacyPlugin.js
Normal file
@ -0,0 +1,38 @@
|
||||
define(['./Plugin'], function (Plugin) {
|
||||
function LegacyPlugin(
|
||||
bundleDefinition,
|
||||
registryLocator,
|
||||
serviceLocator
|
||||
) {
|
||||
function resolve(extension) {
|
||||
var depends = extension.depends || [],
|
||||
dependencies = depends.map(function (name) {
|
||||
return serviceLocator.locate(name);
|
||||
});
|
||||
return extension.implementation ?
|
||||
_.spread(_.partial)(
|
||||
[extension.implementation].concat(dependencies)
|
||||
) : extension;
|
||||
}
|
||||
|
||||
function initializer() {
|
||||
var extensions = bundleDefinition.extensions || {};
|
||||
|
||||
Object.keys(extensions).forEach(function (category) {
|
||||
var registry = registryLocator.locate(category);
|
||||
|
||||
function register(extension) {
|
||||
registry.register(function () {
|
||||
return resolve(extension);
|
||||
}, { priority: extension.priority });
|
||||
}
|
||||
|
||||
extensions[category].forEach(extension);
|
||||
});
|
||||
}
|
||||
|
||||
Plugin.call(this, initializer);
|
||||
}
|
||||
|
||||
return LegacyPlugin;
|
||||
});
|
10
src/mct/Plugin.js
Normal file
10
src/mct/Plugin.js
Normal file
@ -0,0 +1,10 @@
|
||||
define(['lodash'], function (_) {
|
||||
'use strict';
|
||||
|
||||
function Plugin(initializer, activator) {
|
||||
this.initialize = _.once(initializer || _.noop);
|
||||
this.activate = _.once(activator || _.noop);
|
||||
}
|
||||
|
||||
return Plugin;
|
||||
});
|
34
src/mct/Provider.js
Normal file
34
src/mct/Provider.js
Normal file
@ -0,0 +1,34 @@
|
||||
define(['lodash'], function (_) {
|
||||
function Provider(compositor) {
|
||||
function invoke(factory) {
|
||||
return factory();
|
||||
}
|
||||
|
||||
this.compositor = compositor;
|
||||
this.instantiated = false;
|
||||
this.factories = [];
|
||||
this.decorators = [];
|
||||
|
||||
this.get = _.memoize(function () {
|
||||
return this.decorators.reduce(function (instance, decorator) {
|
||||
return decorator(instance);
|
||||
}, this.compositor(this.factories.map(function (factory) {
|
||||
return factory();
|
||||
})));
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Provider.prototype.register = function (factory, options) {
|
||||
this.factories.push(factory);
|
||||
};
|
||||
|
||||
Provider.prototype.decorate = function (decorator, options) {
|
||||
this.decorators.push(decorator);
|
||||
};
|
||||
|
||||
Provider.prototype.compose = function (compositor, options) {
|
||||
this.compositor = compositor;
|
||||
};
|
||||
|
||||
return Provider;
|
||||
});
|
9
src/mct/Registry.js
Normal file
9
src/mct/Registry.js
Normal file
@ -0,0 +1,9 @@
|
||||
define(['./Provider', 'lodash'], function (Provider, _) {
|
||||
function Registry() {
|
||||
Provider.call(this, _.identity);
|
||||
}
|
||||
|
||||
Registry.prototype = Object.create(Provider.prototype);
|
||||
|
||||
return Registry;
|
||||
});
|
9
src/mct/ServiceProvider.js
Normal file
9
src/mct/ServiceProvider.js
Normal file
@ -0,0 +1,9 @@
|
||||
define(['./Provider', 'lodash'], function (Provider, _) {
|
||||
function ServiceProvider(compositor) {
|
||||
Provider.call(this, compositor || _.head);
|
||||
}
|
||||
|
||||
ServiceProvider.prototype = Object.create(Provider.prototype);
|
||||
|
||||
return ServiceProvider;
|
||||
});
|
Reference in New Issue
Block a user