There is a principle in object-oriented design to prefer composition over inheritance. This applies also to modeling in MPS.

Example Problem

Let’s say that as part of your language you need to describe an input for a certain transformation. The input has a name and can come from two sources: a named file or standard input (stdin).

Inheritance-Based Solution

Here is how you could model this requirement. First, let’s create a base concept for inputs:

abstract concept Input : INamedConcept

The input has a name (by virtue of implementing INamedConcept) and is an abstract concept because it’s missing any information about where it comes from. This is the responsibility of concrete subconcepts.

The subconcepts would look as follows:

concept StdinInput extends Input
concept FileInput extends Input { fileName : string; }

There’s a StdinInput that doesn’t need any further information, and another concept, FileInput, which requires the user to specify the file name.

(A side note: I’m using pseudo-definition snippets instead of screenshots of actual concept definitions, I hope the concepts are still clear enough.)

Composition-Based Solution

Let’s see how a composition-based solution would look like. We define a concrete concept for the input and an abstract concept for just the input source:

concept Input : INamedConcept { source: InputSource }
    
abstract concept InputSource

The Input concept still implements INamedConcept so it has a name, but has a separate child for the source.

The different input sources would now extend InputSource:

concept FileInputSource extends InputSource { fileName : string; }
concept StdinInputSource extends InputSource

Editing Experience

Why is the composition-based solution better? One argument in its favor is that it makes for a better editing experience out of the box.

Let’s say you create an input named foo that comes from standard input, and later you want to change the source to be foo.json. You can’t actually do this without replacing the entire Input node which has two unintended consequences:

  1. The name of the input will be cleared when replacing it with a new instance.
  2. The identifier of the new node will be different from the identifier of the old node, breaking any references to the old node.

While the first issue can be fixed using a custom node factory, the second issue is impossible to fix.

With the composition-based solution you only need to replace Input.source, the Input node remains along with its name and any incoming references.