Advanced Usage
Writing Generic Code
So far our examples have been working in an ad-hoc fashion on our Aspect data which already gives you elegant, type safe and intent-explaining code. Still, such code partially also could be written directly on the domain model. Let’s now take a look at functionality, that is very complicated to write without the support of a Meta Model at hand.
We’re talking about generic code, i.e. code that knows how to work with data and its properties, but doesn’t care, what data it exactly is.
Let’s define a simple, but realistic example of such a generic piece of code.
Assume you’re modeling data structures that handle all sorts of technical components, maybe a device that is broken up into its sub-components. Each sub-component is modeled with different properties, but they all share one thing: their unique identifier.
For the application we’re developing it now is regularly needed, to retrieve a set of components and index them by their unique id, something like this:
Map<Integer, SubComponent> =
subComponents.stream()
.collect( Collectors.toMap( SubComponent::getId, Function.identity() ) );
However, we don’t only have this single kind of SubComponent
. We have multiple, on different levels of our domain model.
Additionally, we don’t have a clean inheritance hierarchy where we can assume that the unique id is always an Integer
and
always within the property getId()
. If we still want to be able to write a generic solution to this requirement,
our Static Meta Model comes to the rescue! All we need to know is any property (or property chain) which we use to perform
the indexing:
public <K, V> Map<K, V> index( List<V> components, StaticProperty<V, K> uniqueId ) {
return components.stream()
.collect( Collectors.toMap( uniqueId, Function.identity() ) );
}
...
Map<Integer, SubComponent> componentsById =
index( subComponents, MetaSubComponent.ID );
Now you have an algorithm that will work on any Aspect or Entity which does have an unique identifier.
Maybe you already spotted how easily such generic code can be pushed even further. Assume, that for some parts of the application you don’t want to index the component itself, but one of its properties (maybe the OEM). Again, using properties such an extension is elegant and simple:
public <K, T, V> Map<K, T> index( List<V> components, StaticProperty<V, K> uniqueId, Function<V, T> valueMapper ) {
return components.stream()
.collect( Collectors.toMap( uniqueId, valueMapper ) );
}
...
Map<Integer, SubComponent> componentsById =
index( subComponents, MetaSubComponent.ID, Function.identity() );
Map<Integer, String> componentOemsById =
index( subComponents, MetaSubComponent.ID, MetaSubComponent.OEM );
Filtering and Sorting with Properties
In the same way that properties can be used to access data, they can also be used to filter and sort data.
Filtering
To filter elements based on their properties (or nested properties, via a Property Chain), the convenient builder
PropertyPredicates
exists. It provides the following entry points:
Entry Point | Types | Description |
---|---|---|
|
|
For simple predicates on single-valued properties |
|
|
Used to build |
|
|
Used to build (partial) range predicates like |
|
|
For simple predicates on |
|
|
Used to build predicates on collections, like |
Let’s take a look and some examples using those builders:
List<Movement> movements = retrieveMovements();
// find all that are currently moving
List<Movement> currentlyMoving =
movements.stream()
.filter( PropertyPredicates.on( MetaMovement.IS_MOVING ).isEqualTo( true ) )
.toList();
// find all that have a speed within the range 10km/h <= speed <= 30km/h
List<Movement> withinSpeedRange =
movements.stream()
.filter( PropertyPredicates.compareOn( MetaMovement.SPEED ).withinClosed( 10.0f, 30.0f ) )
.toList();
Again assuming our example with a component and its subcomponents, let’s filter on collection-valued properties:
List<Components> components = retrieveComponents();
var componentOem = PropertyChain.fromCollection( MetaComponent.SUB_COMPONENTS )
.to( MetaSubComponent.OEM );
List<Components> withOemAcme =
components.stream()
.filter( PropertyPredicates.onCollection( componentOem ).contains( "ACME" ) )
.toList();
To conveniently build even more complex predicates with AND or OR semantics you can for example use
the Predicates utility from the Vavr library.
|
Sorting
Property-based sorting doesn’t require any special utilities at all, due to the fact that properties already implement
the Function
interface and thus can directly be used as a keyExtractor
with Comparator.comparing()
:
List<Movement> bySpeedAscending =
movements.stream()
.sorted( Comparator.comparing( MetaMovement.SPEED ) )
.toList();