Skip to content
Fabio Simeoni edited this page Aug 19, 2014 · 5 revisions

We use an embedded DSL to construct model instances and their changesets.

As a simple example:

Attribute a = attribute().name("..").value("...").in("..."), build();
Code code = code().name("...").attributes(a).build();
Codelist list = codelist().name(name).with(code).version("...").build();

These are the advantages:

  • our sentences are succinct and intention-revealing.

This is typical of DSLs and particularly true for test code, where we build sentences out of constants and literals. The DSL keeps our tests small and readable and we've put a lot of test-oriented support in the DSL. To a lesser extent, it also applies to production code.

  • we are guided towards building valid entities.

Another typical motivation for DSLs. The DSL forces us to provide mandatory parameters before we can close up a sentence. For example, we cannot create a new code without providing a name. Equally, there is enough flexibility in the DSL that we can build changesets under different rules. We can omit the name of a code when we want to modify one of its attributes. In the IDE, code completion guides towards towards forming correct sentences.

The DSL is crucial in keeping clients confined to public APIs. Without it client code would be divided in two halves: a 'creation' half that is exposed to private API and 'consumption' half that uses the public API. This would give us the worst of both worlds: the complexity of separate APIs and yet a partial results. The bean SPIs would not be SPIs at all, they would be APIs. And clients would need to be aware of the details of memory bean constructors.

DSL, separate APIs, beans SPIs, and memory beans are all necessary parts of our design. They work together.

There are three parts to the DSL:

  • the grammars, groups of correlated interfaces that define the shape of DSL sentences.
  • the builders, a set of classes that implement the grammars.
  • the Data class, a collection of static methods that serve as start clauses for DSL sentences.

As an example, the grammar of codelists is the following group of interfaces:

public class CodelistGrammar {

  public static interface CodelistNewClause extends NameClause<SecondClause> {}
	
  public static interface CodelistChangeClause extends NameClause<SecondClause>, SecondClause {}
	
  public static interface SecondClause extends AttributeClause<Codelist,SecondClause>, BuildClause<Codelist> {
		
	SecondClause definitions(AttributeDefinition ... defs);
	SecondClause definitions(AttributeDefinitionGrammar.OptionalClause ... defs);
	SecondClause definitions(Iterable<AttributeDefinition> defs);
	
        SecondClause links(LinkDefinition ... defs);
	SecondClause links(LinkDefinitionGrammar.OptionalClause ... defs);
	SecondClause links(Iterable<LinkDefinition> defs);
	  
        SecondClause with(Code ... codes);
	SecondClause with(CodeGrammar.OptionalClause ... codes);
	SecondClause with(Iterable<Code> codes);

        SecondClause version(String version);
   }
}

CodelistNewClause is a specialisation of NameClause, an external interface shared among many other grammars that let us set names in DSL sentences. The parametrisation of NameClause indicates that once we have set a name for codelists, we will be presented with the SecondClause. So names are mandatory on codelists, we need to set one before we can move on.

SecondClause has a number of methods specific to codelists (for adding codes, attribute definitions, link definitions, versions). It also specialises another external utility class AttributeClause, which let us set attributes in DSL sentences, and yet another, BuildClause, whereby we can clause DSL sentences. In this case, we customise the shared BuildClause so it will return a Codelist.

CodelistChangeClause let us build changesets by merging NameClause and SecondClause. Names are no longer mandatory and we are to construct codelists partially in this context.

All these interfaces are then implemented by a CodelistBuilder:

public class CodelistBuilder implements CodelistNewClause, CodelistChangeClause {...}

In some cases we need to be creative, but here and in mot cases a fairly standard application of the builder pattern will do. The builder simply keeps a memory bean, here MCodelist:

private final MCodelist state;
		
public CodelistBuilder(MCodelist state) {
  this.state = state;
}

and configures it as we build the sentence:

@Override
public SecondClause name(QName name) {
  state.qname(name);
  return this;
}

When we get to the BuildClause and build() is invoked, the builder asks the bean to produce a Codelist:

public Codelist build() {
 return state.entity();
}

The final piece of the jigsaw are the following methods of the Data class:

public static CodelistNewClause codelist() {
   return new CodelistBuilder(new MCodelist());
}
	
public static CodelistChangeClause modifyCodelist(String id) {
  return new CodelistBuilder(new MCodelist(id,MODIFIED));
}
	
public static CodelistChangeClause modify(Codelist list) {
  notNull("codelist",list);
  return modifyCodelist(list.id());
}

which we can statically import and invoke to start DSL sentence to build codelists. This is where builders and memory beans are instantiated in normal or changeset mode.

note: the DSL does not let us delete codelists. As top-level entities, we do so through the Repositories. For any other entity, Data has corresponding delete() methods.

Clone this wiki locally