Problem Description
We have customers interested in combining the behaviors of multiple tasks. For example, as part of a single task, users may need to edit an attribute resource as well as run an operation. The task’s style may make both the attribute editor panel and the operation parameter-editor panels visible, but with the current design a new subclass of smtk::task::Task
would be required to properly compute the task state (i.e., to indicate when both the operation has succeeded and the attributes have been validated).
Because the logic for state computation is in different subclasses, there is no way to re-use this logic in the new task; code from both smtk::task::FillOutAttributes
and smtk::task::SubmitOperation
would have to be cribbed together by the new task subclass (call it smtk::task::CompositeTask
for the purposes of this discussion). We wish to avoid this duplication.
Potential Solutions
All of the solutions we consider would split subclasses of smtk::task::Task
to separate the logic for linting/validation/state-computation into a separate class. There are many ways to accomplish this, but we will consider two that are representative. This “linter” or “validator” class may be
- multiply inherited by task subclasses in order to re-use the configuration and validation logic; or
- instantiated and owned by instances of the base
smtk::task::Task
class (i.e., there would be no more need to subclass tasks).
We’ll call the first option “multiple inheritance” and the second option “no inheritance” as we consider the implications of these designs in regards to the issues below. Before we do that, let’s sketch what each option might look like.
The multiple-inheritance option might look like
class CompositeTask
: public smtk::task::Task
, public AttributeValidator
, public OperationValidator
{
public:
/// Pass portions of configuration to each inherited validator.
void configure(const Configuration& config) override;
};
while the no-inheritance option might add internal storage to the base task class like so:
class Task : public smtk::resource::Component
{
protected:
/// Every task has run-time-configurable validators.
std::vector<std::unique_ptr<ValidatorBase>> m_validators;
/// By default, the task will call this function when any validator's
/// internal state changes. If needed, we can hold a lambda that
/// will serve as a replacement.
static Static combineValidatorStates(
const std::vector<std::unique_ptr<ValidatorBase>>&);
};
Issues to consider
We can summarize the issues and differences between the designs in this table. Some more detailed notes then follow in the sections below.
Issue | Multiple Intheritance | No Inheritance |
---|---|---|
Task State | Task subclasses would use the public API that each of its inherited validators provides to call Task::internalStateChanged() as needed. |
Validators call a method on the task when their internal state changes and the task calls combineValidatorStates() |
Ports | Task subclasses would be responsible for passing notifications to/from their validators as they receive updates about port-data changing or requests for data from a port. | The base task class would iterate over validators and notify each one of changes or request port-data from each. For now, only one validator would produce data for any given output port. In the future, data from multiple validators may be combined. |
Children | If a task subclass needs children, it would be responsible for re-implementing the API provided by the base Task class. |
The base task class would now provide a vector of child tasks. An empty vector would be the default. See notes below. |
Style | Minimal changes would be required since dynamic casting to a validator provides equivalent functionality. | Style responders would need to iterate the task’s validators to fetch information needed. |
Creation | The existing task factory would be sufficient. | The task-manager would need an additional factory to construct validators. |
Ports
In the “multiple inheritance” design, the relationship between task ports and inherited validators must be manually managed by the author of the task subclass.
In the “no inheritance” design, we would require each validator’s configuration to explicitly mention the ports from which it should draw configuration and to which it should broadcast information. Thus the base validator class would need to have the same virtual methods for ports that tasks currently provide; tasks just forward these calls to each iterator. The base class for all validators could also provide an interface allowing the task to identify which ports have data consumed/produced by it, but this would not be necessary in an initial implementation.
Children
There are additional considerations for the “no inheritance” design. GroupTask
would no longer be required since all tasks are allowed to have child tasks. All child tasks would be required to be completable before the parent task becomes completable. However, we may relax this requirement in the presence of a validator that claims control of child tasks.
For example, a parent “Define Simulation Domain” task might have a child “Load Geometry” task and a child “Create Geometry” task. Normally, both would be required to be completed, but the parent task might provide a ChildValidator
that only requires one to be complete in order to proceed.
Style
Currently, the style for the attribute-editor panel and operations parameter-editor panels look like this:
{
"styles": {
"operation_style_tag": {
"operation-panel": {
"display": true,
"hide-items": ["/item1", "/item5" ]
}
},
"attribute_style_tag": {
"attribute-panel": {
"attribute-editor": "ViewName"
}
}
}
}
In each case above, the style is applied by the panels themselves; they monitor the active task and when it changes, they apply any relevant style from the task-manager. However, in order to do this, they dynamically cast the active task to a fixed type (SubmitOperation
or FillOutAttributes
) to obtain information needed (the operation or attribute resource containing the “ViewName” view configuration).
In the “no inheritance” design, panels would need to iterate over validators owned by the task. In the future, the style might be extended to call out specific validators (either by index since they are held in a vector or by name) that data should be fetched from. For example,
{
"styles": {
"operation_style_tag": {
"operation-panel": {
"validator": "MeshRunner",
"display": true,
"hide-items": ["/item1", "/item5" ]
}
},
"attribute_style_tag": {
"attribute-panel": {
"tabs": [
{ "validator": "Thermal",
"view-name": "ThermalBCs" },
{ "validator": "Flow",
"view-name": "FluidBCs" }
]
}
}
}
}
adds validator names and, in the case of the attribute panel, provides a way to generate a toplevel view configuration that is a tabbed group-view; each tab of the group-view would be a view configuration from that validator’s attribute resource.
In the “multiple inheritance” design, the panels could continue to do this but casting to validator classes instead. While the same validator class may be inherited multiple times, this cannot happen in a meaningful way and even if it is done, the style-responder would not be able to distinguish from among them.
Proposed Solution
We propose using the “No Inheritance” model described above because it removes the need for subclassing tasks. All validators would inherit a common base that requires/provides the following:
class Validator
{
public:
using State = smtk::task::State;
virtual State internalState();
virtual void configure(const Configuration& config) override;
virtual std::shared_ptr<PortData> portData(const Port* port) const;
virtual void portDataUpdated(const Port* port) { (void)port; }
protected:
// Validators must have a parent task in order to notify it
// of state changes.
smtk::task::Task* m_parent;
// Notify the task that it needs to recompute state because this
// validator's state has changed.
void stateChanged(State prev, State next);
};
Subclasses would include:
AttributeValidator
– fromFillOutAttributes
OperationValidator
– fromSubmitOperation