An Asset
is the mandatory container element for both
LanguageModel
and Collaboration
instances. When defining
compositions, in a subsequent step, an Asset
is referenced (or,
bound) by a Composition
.
The running example roughly follows the storyline behind a Graph Product Line (GPL), as used in [FOSPL] (see, e.g., Chapter X for some background).
The Graphs
asset contains a language model Graph
and a
collaboration weighted
. From the point of view of a GPL
variability model (not shown), the language model maps to and
implements the root or base feature. The collaboration maps to and
implements the option feature weighted
.
Asset create Graphs {
LanguageModel create Graph {
:property name:alnum
:property -incremental edges:object,type=Edge,0..n
Classifier create A
Classifier create Node
Classifier create Edge {
:property -accessor public a:object,type=Node
:property -accessor public b:object,type=Node
}
}
Collaboration create weighted {
Classifier create Weight {
:property -accessor public {value:integer 0}
}
Role create A
Role create Edge -superclasses A {
:property -accessor public weight:object,type=Weight
}
}
}
A language model itself contains only Classifier
instances that
describe the main elements of the primary abstract syntax of a
given DSL (e.g., Node
and Edge
).
A collaboration contains both Classifier
instances (i.e.,
introducing new abstract-syntax elements such as Weight
) and
Role
instances. Roles are refinements of previously introduced
or already present abstract-syntax elements (e.g., Edge
). Roles
cannot be instantiated directly.
A Classifier
or a Role
instance can contain structural and
behavioural elements, i.e., properties and methods. From the
perspective of an abstract-syntax definition, methods are not
necessary. However, collaborations can be also be used also to
structure the behavioural implementations as optional (composable)
units that back a DSL (e.g., an interpreter or visitor-based
transformations).
Technically, this conceptual nesting between collaborations and classifiers/ roles is implemented as nesting the NX objects representing them. Watch:
% [Graphs new graph -name "g"] info class
::Graphs::Graph
% [Graphs new weighted] info class
unable to dispatch sub-method "weighted" of ::Graphs new; valid are: new graph
% [[Graphs new graph -name "g2"] new node] info class
::Graphs::Graph::Node
The definitional content of language models and collaborations is
open for extension using method contains
.
% Graphs::Graph contains {
Classifier create Label
}
::Graphs::Graph::Label
Standard NX introspection methods can be used to spell out the content of a container (assets or collaborations), for example:
% lsort [Graphs::Graph info children -type AssetElement]
::Graphs::Graph::A ::Graphs::Graph::Edge ::Graphs::Graph::Label ::Graphs::Graph::Node
However, the abstract-syntax content is defined, important consistency conditions are tested and established. For example, language models must not contain roles.
% Graphs::Graph contains {
Role create Label
}
Invalid element: Language models cannot contain roles.
Object nesting has a number of benefits. It facilitates product-bound
quantification, object cleanup, and it guarantees name-based
qualification for classifiers and roles (Graphs::Graph::Edge
vs. weighted::Edge
). Also note that each container
(collaboration or language model) provides factory methods (new
graph
, new weighted
) for its contained classifiers or roles.
A Composition
is defined to implement one product, mapping to
one configuration valid under the variability model. A composition
itself realises an asset and binds some assets as provides of
language models and collaborations (incl. other compositions!).
The GPL configuration for weighted graphs is implemented as follows:
Composition create WeightedGraphs -binds Graphs -base [Graphs::Graph] -features [Graphs::weighted]
A composition such as WeightedGraphs
can then be used then to
instantiate a configured language model (using a factory method
new graph
). This language model, in turn, provides factories for
creating configured abstract-syntax elements (e.g.,
weight-labelled edges):
set wg [WeightedGraphs new graph -name "wg"]
set n1 [$wg new node]
set n2 [$wg new node]
set e [$wg new edge -a $n1 -b $n2 -weight [$wg new weight -value 1]]
Basic NX introspection techniques such as
info
precedence
can be used to reveal the composition order between
roles and classifiers, starting from a given composition:
% $wg info precedence
::WeightedGraphs::Graph ::Graphs::weighted ::Graphs::Graph ::nx::Object
% $n1 info precedence
::WeightedGraphs::Graph::Node ::Graphs::Graph::Node ::nx::Object
% $e info precedence
::WeightedGraphs::Graph::Edge ::Graphs::weighted::Edge ::Graphs::weighted::A ::Graphs::Graph::Edge ::nx::Object
The pool of collaborations that implement optional features
(coloured
) can be organised across several separate assets
(Colours
):
Asset create Colours {
Collaboration create coloured {
Classifier create Color {
:property -accessor public {value 0}
}
Classifier create B
Role create Edge -superclasses B {
:property -accessor public colour:object,type=Color
}
:public method colored {} {return 1}
}
}
When defining a composition, all providing assets must be passed
as arguments to -binds
:
set ccomp [Composition new -binds [list [Graphs] [Colours]] -base [Graphs::Graph] -features [Colours::coloured]]
set cg [$ccomp new graph -name "cg"]
% $cg info precedence
${ccomp}::Graph ::Colours::coloured ::Graphs::Graph ::nx::Object
For a more general background on NX support for collaboration-based designs, see our paper presented at [FOSD]. Note that the implementation used as part of DjDSL goes beyond the fundamentals presented in this paper; and deviates in some details.
-
[FOSPL] Apel, S., Batory, D., Kästner, C., & Saake, G. (2013). Feature-Oriented Software Product Lines (1st). Springer.
-
[FOSD] Sobernig, S., Neumann, G., & Adelsberger, S. (2012). Supporting Multiple Feature Binding Strategies in NX. In Proc. 4th International Workshop on Feature-Oriented Software Development (FOSD'12) (pp. 45—53). ACM.
package req nx
nx::Class create Container {
:protected method __object_configureparameter {} {
set spec [next]
lreplace $spec[set spec {}] end end contains:alias,optional
}
::nsf::parameter::cache::classinvalidate [current]
:public method contains args {
namespace eval [self] { namespace path ::djdsl::lm }
next
}
}
nx::Class create Asset -superclasses Container
nx::Class create AssetElement -superclasses {Container nx::Class}
nx::Class create Role -superclasses AssetElement {
:public object method create args {
set container [uplevel [current callinglevel] {namespace current}]
if {[$container info has type LanguageModel]} {
throw {DJDSL LM INVALIDEL} "Invalid element: Language models cannot contain roles."
}
# TODO: overriding [create] breaks namespace setting for
# new/create calls, e.g., in contains. set r [next]; use
# [apply] for the time being to correct the namespace context.
set r [apply [list {} {next} $container]]
return $r
}
}
nx::Class create Classifier -superclasses Role
nx::Class create Collaboration -superclasses AssetElement {
:public method create args {
if {[:info class] eq [current class]} {
throw {DJDSL ABSTRACT} "Collaboration [self] cannot be instantiated directly"
}
next
}
}
nx::Class create LanguageModel -superclasses Collaboration {
:property {owning:object,type=Asset,substdefault "[:info parent]"}
:public method init {} {
set body "[self] new -childof ${:owning} {*}\$args"
${:owning} public object method "new [string tolower [namespace tail [self]]]" args $body
foreach c [:info children -type Role] {
:createFactory $c
}
next
}
:public method createFactory {nested:class} {
# Create accessors for the collaboration parts
set name [namespace tail $nested]
set self [self]::$name
set vargs [string cat "{*}" $ args]
:public method "new [string tolower $name]" args [subst -nocommands {
if {[:info lookup method mk$name] ne ""} {
:mk$name $self $vargs
} else {
$self new -childof [self] $vargs
}
}]
}
}
nx::Class create Composition -superclasses Asset {
:property binds:object,type=Asset,1..*
:property {base:class,required}
:property {features:0..n,type=Collaboration ""}
:private method computeExtensionHierarchy {} {
set baseClass ${:base}
set featureModules ${:features}
dict set d extension $baseClass ""
# Create an extension structure for the base class.
foreach childclass [$baseClass info children -type ::nx::Class] {
set name [$childclass info name]
dict set d extension $name ""
dict set d class $name $childclass
}
# For each collaboration (feature),
# (1) add the collaboration class to the extension list of the language model and
# (2) create/extend the refinements list for the nested role classes.
foreach collaboration $featureModules {
# puts stderr "dict lappend d extension $baseClass $collaboration"
# dict lappend d extension $baseClass $collaboration
dict with d extension { lappend $baseClass $collaboration }
foreach roleClass [$collaboration info children -type ::nx::Class] {
set name [$roleClass info name]
if {[dict exists $d class $name]} {
# known role class
dict with d extension { lappend $name $roleClass }
# dict set d extension $name $roleClass
} else {
# unknown role class
dict set d class $name $roleClass
# dict lappend d extension $name ""
dict set d extension $name ""
}
}
}
return $d
}
:private method patch {context ancestors compositionClasses} {
if {[lindex $ancestors 0] ne "::nx::Object"} {
if {[lindex $ancestors end] eq "::nx::Object"} {
set ancestors [lreplace $ancestors end end]
}
# patch any base-level sub-/superclass relationships using
# the corresponding composition classes.
set revMap [lreverse $compositionClasses]
set ancestors [lmap e $ancestors {
if {[dict exists $revMap $e]} {
string cat $context :: [$e info name]
} else {
set e
}
}]
return $ancestors
} else {
list
}
}
:private method weave {-baseClass -featureModules -context} {
set d [: -local computeExtensionHierarchy]
set collaborationClassNames [dict keys [dict get $d class]]
# Let the resulting language model (context) inherit from the extension classes and the base class.
set superclasses [list {*}[concat {*}[dict get $d extension ${:base}]] ${:base}]
nsf::relation::set $context superclass [list {*}$superclasses {*}[$context info superclasses]]
# batch create the composition classes, so they can be used
# directly in patching the generalisations below.
foreach name $collaborationClassNames {
Classifier create ${context}::$name
}
foreach name $collaborationClassNames {
set expansion [[dict get $d class $name] info superclasses -closure]
set expansion [: -local patch $context $expansion [dict get $d class]]
set extension [list]
foreach r [dict get $d extension $name] {
lappend extension $r
lappend extension {*}[: -local patch $context [$r info superclasses -closure] [dict get $d class]]
}
set supers [list {*}$extension [dict get $d class $name] {*}$expansion]
nsf::relation::set ${context}::$name superclass $supers
$context createFactory ${context}::$name
}
}
:public method init {} {
set ctx [LanguageModel create [self]::[namespace tail ${:base}]]
: -local weave -baseClass ${:base} -featureModules ${:features} -context $ctx
}
}
namespace export Asset AssetElement Composition Collaboration LanguageModel Classifier Role