Table of contents
As
As is a factory for providing adapters (in the meaning of the Adapter pattern) of an
object.
Terminology note: the object for which we are going to create an adapter will be called “datum” and the adapters “roles”. These terms are borrowed from the DCI architectural pattern (Data, Context and Interaction), even though
Asdoesn't need to be used in that way. But TheseFoolishThings does provide explicit support for DCI, as explained below.
Let's start from a model class, such as a Person entity. In a typical application we might need to display it in a user interface
and to save it to a file, for instance in the XML format. The first point is to decouple Person from the way we perform those two operations, also to comply
with the Dependency Inversion principle: we want the UI and the XML subsystem to depend on the
abstraction (Person), not the opposite way.
We introduce two small interfaces: Displayable for computing the display name and Marshallable to serialize an object to an XML stream.
interface Displayable
{
String getDisplayName();
}
interface Marshallable
{
void writeTo (Path path)
throws IOException;
}
These two interfaces are very simple, so they are also in compliance with the Single Responsibility principle and the Interface Segregation principle.
Making Person implement the two interfaces is not an option, because it would lead to tight coupling. Working with composition would slightly improve
things:
class Person
{
public Displayable getDisplayable() { ... }
public Marshallable getMarshallable() { ... }
}
even though a hardwired implementation of the two interfaces inside Person would still leave us not too far from the starting point. Introducing a
RoleLocator might be the next step:
class RoleLocator
{
public static RoleLocator getInstance() { ... }
public Displayable createDisplayableFor (Person person) { ... }
public Marshallable createMarshallableFor (Person person) { ... }
}
class Person
{
public Displayable getDisplayable()
{
return RoleLocator.getInstance().createDisplayableFor(this);
}
public Marshallable getMarshallable()
{
return RoleLocator.getInstance().createMarshallableFor(this);
}
}
Since in a real world application we are going to deal with multiple entities, RoleLocator must be generic:
class RoleLocator
{
public static RoleLocator getInstance() { ... }
public Displayable createDisplayableFor (Object datum) { ... }
public Marshallable createMarshallableFor (Object datum) { ... }
}
But it's no good to have a fixed, limited set of roles. Who knows what we are going to need in a user interface?
For instance, a Selectable role might be used to execute a task whenever a Person representation is double-clicked in a UI widget. RoleLocator
can be further generalised as:
class RoleLocator
{
public static RoleLocator getInstance() { ... }
public <T> T createRoleFor (Object datum, Class<T> roleType) { ... }
}
so Person becomes:
class Person
{
public Displayable getDisplayable()
{
return RoleLocator.getInstance().createRoleFor(this, Displayable.class);
}
public Marshallable getMarshallable()
{
return RoleLocator.getInstance().createRoleFor(this, Marshallable.class);
}
}
But, again, there is still too much coupling involving Person: any new role would require a new method and after all we don't want Person to depend even on the
RoleLocator infrastructure; it might be a legacy code as well that we can't or don't want to change. Let's move the responsibility of retrieving the
adapter from the adaptee class to the client code that requires the adapter (it does make sense):
class UserInterface
{
private final RoleLocator roleLocator = RoleLocator.getInstance();
public void renderPerson (Person person)
{
String displayName = roleLocator.createRoleFor(person, Displayable.class).getDisplayName();
}
}
So now we are back to the pristine Person totally unaware of the roles:
class Person
{
...
}
Now the design is good and we can introduce some syntactic sugar. Since the operation might be read like «given a Person treat it as
it were a Displayable» we can rename createRoleFor() to as() (short names with a proper meaning improve readability) and, with a bit of
rearranging methods and using static imports, get to this code:
import static RoleLocator.as;
class UserInterface
{
public void renderPerson (Person person)
{
String displayName = as(person, Displayable.class).getDisplayName();
}
}
If on the other hand we can apply a small change to Person (the bare minimum), we could think of an interface
interface As
{
public <T> T as (Class<T> roleType);
}
and have Person to implement that interface:
class Person implements As
{
...
}
So we now have another version of our code:
class UserInterface
{
public void renderPerson (Person person)
{
String displayName = person.as(Displayable.class).getDisplayName();
}
}
class Persistence
{
public void storePerson (Person person, Path path)
throws IOException
{
person.as(Marshallable.class).writeTo(path);
}
}
What about Tell Don't Ask?
According to Martin Fowler:
Tell-Don't-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data. It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do. This encourages to move behavior into an object to go with the data.
It's one of the ways we can make our design really strong and resistant to change. Unfortunately, in practice it is the exact opposite of what is commonly used in Java with the Java Beans idiom, which mandates getter and setter methods. Known libraries/frameworks (such as JPA, JAXB, GUI frameworks, etc.) are designed like that and inspire programmers to follow that way.
This is also due to the fact that TDA is more complex to implement, in particular when new behaviour must be added. For instance, given a Person provided
with standard getters such as getFirstName() and getLastName(), it's easy to use these properties in a plurality of contexts, such as:
var joe = new Person("Joe", "Smith");
System.out.printf("Name: %s last name: %s\n", joe.getFirstName(), joe.getLastName());
...
graphicContext.renderString(x, y, String.format("Name: %s last name: %s\n", joe.getFirstName(), joe.getLastName()));
How this would look like in TDA? Something such as:
var joe = new Person("Joe", "Smith");
joe.render("Name: %1$s last name: %2$s", System.out::println); // 1$ is first name, 2$ is last name, etc.
This assumes render(String, Consumer<String>) is implemented in Person; not a big deal since almost any object we can think of can be rendered as a string
and it can be done with facilities available in the standard Java library. But what about this?
joe.render(graphicContext, x, y, "Name: %1$s last name: %2$s");
render(GraphicContext, int, int, String) would introduce a dependency in Person, a model class, to GraphicContext, part of a graphical API: this
is not acceptable. As can come to the rescue. Since roles can be injected without touching the original object, a possible solution is:
joe.as(Renderable.class).render("Name: %1$s last name: %2$s", System.out::println);
joe.as(GraphicRenderable.class).render(graphicContext, x, y, "Name: %1$s last name: %2$s");
Now Person does not depend on GraphicRenderable; a concrete implementation of GraphicRenderable depends on Person (which is good and complies with the
Dependency Inversion Principle); the client code depends on both (as expected).
PENDING: more details about the implementation of roles, their “friendship” to owner classes and constraints imposed by Java 9 modules.
Injected DCI roles can also be useful for a business model designed with the TDA principle in mind, as adapters to an external world that follows the Java Beans idiom.
Some gory details
If you got up to here, you have understood what As is for. Now it's time to deal with implementation details. But before going on let's recap and give a couple
of definitions. Role implementations compatible with As can be:
- static, in the sense that the datum directly implements them. For instance,
class Person implements Displayable, Marshallable. This is totally against the decoupling thatAsfosters, but it's legal. - static, in the sense that roles are implemented in separate classes (this is much better from the design point of view), but they are still statically
bound to their datum, for instance in the datum constructor. The detail is explained below where
As.forObject()is introduced. While still a coupled approach, it might be meaningful for some corner case. - dynamic, that is the implementation is separate and it is not directly coupled to the datum; in other words, the datum depends neither on role implementations nor their interfaces. This is the best approach since it allows the highest decoupling: roles can be implemented later and independently of the datum, perhaps in a different library/module (indeed roles can be even designed after the datum has been implemented). In systems that can load code at runtime this means that features can be added when the application is running. Dynamic roles can be even bound to a datum in a temporary fashion, for instance while running a specific chunk of code. In this case it is said that roles are provided by a context (the ‘C’ in DCI). This requires a runtime capable of associating each datum with the relevant roles; usually this is done by annotating roles and taking advantage of a class scanner.
Note that even when static roles are used, dynamic ones can always be added later.
To be able to use As we need to learn three more things:
- how to implement
Ascapabilities for datum objects; - how to setup the runtime for binding roles;
- how to configure a context and declare roles with annotations.
Retrieving roles
As provides three methods to retrieve roles:
as(type)returns a role of the given type, or throwsAsExceptionif there is none;maybeAs(type)returns anOptionalcontaining a role of the given type, which is empty if there is none;asMany(type)returns all the roles of the given type, possibly an empty collection.
Each of them has a variant accepting an As.Type in place of a Class (see As and roles with generics below). When multiple roles of the requested type
are available, as() and maybeAs() return only one of them; static roles passed to As.forObject() (see below) have priority over the others.
Implementing objects with As support
Once an object is declared to implement As, how to write the code for the methods in the contract? The easiest way is by delegation:
class MyObject implements As
{
private final As delegate = As.forObject(this);
@Override @Nonnull
public <T> Optional<T> maybeAs (@Nonnull Class<? extends T> type)
{
return delegate.maybeAs(type);
}
@Override @Nonnull
public <T> Collection<T> asMany (@Nonnull Class<? extends T> type)
{
return delegate.asMany(type);
}
}
If Lombok is used, the code is even simpler:
@EqualsAndHashCode(exclude = "delegate") @ToString(exclude = "delegate")
class MyObject implements As
{
@Delegate
private final As delegate = As.forObject(this);
}
Remember in any case to exclude the delegate object from equals(), hashCode() and toString().
Note that this step only satisfies the implementation requirements of the object, while the runtime has not been initialised yet; this means that no dynamic role will ever be found. See the section Configuration of the runtime below for further details.
It is possible to call As.forObject() with extra arguments that are interpreted as static roles. If a role is an implementation of RoleFactory, it
actually acts as a factory of possibly dynamic roles. While this works, it is not the most powerful approach since it couples objects with their roles, while
the whole point of As is to make them totally decoupled.
With Lombok, if one accepts advanced features such as @ExtensionMethod, things can be further simplified: it is sufficient to put the annotation
@ExtensionMethod(AsExtensions.class) to the class in which you want to use As methods. In the code sample below Person is a POJO that doesn't implement
As, but the relevant methods are available on it:
@ExtensionMethod(AsExtensions.class) @Slf4j
public class DisplayableExample
{
public void run()
{
final var joe = new Person(new Id("1"), "Joe", "Smith");
final var luke = new Person(new Id("2"), "Luke", "Skywalker");
// approach with classic getter
log.info("******** (joe as Displayable).displayName: {}", joe.as(_Displayable_).getDisplayName());
log.info("******** (luke as Displayable).displayName: {}", luke.as(_Displayable_).getDisplayName());
// approach oriented to Tell Don't Ask
joe.as(_Renderable_).renderTo("******** (joe as Renderable): %1$s %2$s ", log::info);
luke.as(_Renderable_).renderTo("******** (luke as Renderable): %1$s %2$s ", log::info);
}
}
Note that this approach might have a performance impact: see issue TFT-301.
At last, it is possible to do without instance methods, using instead the static methods of AsExtensions:
import static it.tidalwave.util.AsExtensions.*;
...
Displayable d = as(joe, _Displayable_);
Also in this case there might be a performance hit.
As and roles with generics
As explained above, As.as() expects a Class as a parameter; this works well with roles that don't use generics. But what about ones that do? Let's for
instance assume to have the role:
interface DataRetriever<T>
{
public List<T> retrieve();
}
Because of type erasure, the expression as(DataRetriever.class) doesn't bear any information about the associated generic type. The As API has been designed
so that the following code compiles and works:
List<String> f1 = object1.as(DataRetriever.class).retrieve();
List<LocalDate> f2 = object2.as(DataRetriever.class).retrieve();
because DataRetriever.class is a raw type, so as() returns a raw DataRetriever and the compiler allows to assign the result of retrieve() to a List of
any type; but this raises an unchecked conversion warning. To work around this problem a specific As.Type has been introduced to be used as parameter in
place of Class:
private static final As.Type<DataRetriever<String>> _StringRetriever_ = As.type(DataRetriever.class);
private static final As.Type<DataRetriever<LocalDate>> _LocalDateRetriever_ = As.type(DataRetriever.class);
So the following code compiles with no warning:
List<String> f3 = object1.as(_StringRetriever_).retrieve();
List<LocalDate> f4 = object2.as(_LocalDateRetriever_).retrieve();
Note that As.type() doesn't check its argument at compile time: it's up to the programmer to declare an As.Type consistent with the class passed to it.
Note also that it's still not possible to have two roles with the same class and different generics associated to the same object: again because of type erasure the runtime would consider them two instances of the same role type. To differentiate them it is necessary to use two distinct subtypes.
Contexts and role annotations
Global context
After the runtime is instantiated, a global context is implicitly activated: it contains the roles that are not bound to a specific context (see
below), so they are always available. A simple code sample is given in the “DciDisplayableExample” module. The runtime searches for classes annotated with
@DciRole, which specifies which datum class (or classes) the role is associated to. The datum instance is also injected in the constructor and, typically,
the role implementation keeps a reference to it by means of a field.
@DciRole(datumType = Person.class) @RequiredArgsConstructor
public final class PersonDisplayable implements Displayable
{
@Nonnull
private final Person datum;
@Override @Nonnull
public String getDisplayName()
{
return String.format("%s %s", datum.firstName, datum.lastName);
}
}
Some things to keep in mind:
- Roles are searched for by type, which should be an interface: a role implementation is registered under all the interfaces that it implements, directly or by inheritance, and not under its own class.
- A role associated to a datum class is also available to its subclasses (or to its implementations, if the datum type is an interface).
- How many times a role is instantiated depends on where it comes from:
- a dynamic role, such as the one above, is instantiated every time it is searched for, and the same happens with roles created by a
RoleFactorythat is a dynamic role itself (see below), since it is invoked at every search (unless it performs some kind of caching in the factory method): so these role implementations shouldn't keep any state, apart from the references to the datum, to the context and to other injected objects; - static roles passed to
As.forObject()are always the same instances, and aRoleFactorypassed toAs.forObject()is invoked only once, when the object implementingAsis created; - when the datum itself implements the requested role, the datum is returned.
- a dynamic role, such as the one above, is instantiated every time it is searched for, and the same happens with roles created by a
- The constructor of a role can have parameters for the datum and for the context (if the role is bound to one, see below); with Spring, it can also have parameters for other beans of the application context (see With Spring below).
- A role implementation can also be a
RoleFactory: in this case it is invoked to create roles of the type that it declares.
Now everything is ready to use the role:
@ExtensionMethod(AsExtensions.class) @Slf4j
public class DisplayableExample
{
public void run()
{
final var joe = new Person(new Id("1"), "Joe", "Smith");
final var luke = new Person(new Id("2"), "Luke", "Skywalker");
// approach with classic getter
log.info("******** (joe as Displayable).displayName: {}", joe.as(_Displayable_).getDisplayName());
log.info("******** (luke as Displayable).displayName: {}", luke.as(_Displayable_).getDisplayName());
// approach oriented to Tell Don't Ask
joe.as(_Renderable_).renderTo("******** (joe as Renderable): %1$s %2$s ", log::info);
luke.as(_Renderable_).renderTo("******** (luke as Renderable): %1$s %2$s ", log::info);
}
}
In most cases a global context is everything needed for an application.
Local contexts
The example named “DciMarshalXStreamExample” illustrates how local contexts work. It uses the popular serialization framework named XStream to provide XML serialisation capabilities in form of roles.
Let's first introduce the model objects:
/* @Immutable */ @AllArgsConstructor @Getter @EqualsAndHashCode
public class Person implements Serializable // Serializable is not a requirement anyway
{
@Nonnull
public static Person prototype()
{
return new Person("", "");
}
public Person (@Nonnull final String firstName, @Nonnull final String lastName)
{
this(Id.of(UUID.randomUUID().toString()), firstName, lastName);
}
final Id id;
@Nonnull
final String firstName;
@Nonnull
final String lastName;
@Override @Nonnull
public String toString()
{
return firstName + " " + lastName;
}
}
@NoArgsConstructor @EqualsAndHashCode
public class ListOfPersons implements List<Person>
{
@Delegate
private final List<Person> persons = new ArrayList<>();
public static ListOfPersons empty ()
{
return new ListOfPersons();
}
@Nonnull
public static ListOfPersons of (@Nonnull final Person ... persons)
{
return new ListOfPersons(List.of(persons));
}
public ListOfPersons (@Nonnull final List<? extends Person> persons)
{
this.persons.addAll(persons);
}
@Override @Nonnull
public String toString()
{
return persons.toString();
}
}
ListOfPersons is basically an implementation of List<Person> that delegates all methods to an ArrayList. While it doesn't offer any specific
additional behaviour (apart from some factory methods), it is required to use dynamic roles as they are bound to a specific class; because of Java
type erasure a List<Person> cannot be distinguished from a List of any other kind, such as List<String>. Having a specific subclass fixes this
problem, acting as a sort of “reification”.
Now let's deal with XStream. The first thing to do is to set up a bag of configuration that instructs the framework how to manage our model objects. This configuration is encapsulated in a specific DCI context:
@DciContext
public interface XStreamContext
{
@Nonnull
public XStream getXStream();
}
@Getter @DciContext
public class XStreamContext1 implements XStreamContext
{
private final XStream xStream = new XStream(new StaxDriver());
public XStreamContext1()
{
// xStream.alias("person", PersonConverter.MutablePerson.class);
xStream.alias("person", Person.class);
xStream.aliasField("first-name", PersonConverter.MutablePerson.class, "firstName");
xStream.aliasField("last-name", PersonConverter.MutablePerson.class, "lastName");
xStream.useAttributeFor(PersonConverter.MutablePerson.class, "id");
xStream.registerConverter(new IdXStreamConverter());
xStream.registerConverter(new PersonConverter());
xStream.alias("persons", ListOfPersons.class);
xStream.addImplicitCollection(ListOfPersons.class, "persons");
xStream.addPermission(AnyTypePermission.ANY);
}
}
Details about XStream converters are not listed since they are specific to XStream. An alternate implementation could be:
@Getter @DciContext
public class XStreamContext2 implements XStreamContext
{
private final XStream xStream = new XStream(new StaxDriver());
public XStreamContext2()
{
// xStream.alias("person", PersonConverter.MutablePerson.class);
xStream.alias("PERSON", Person.class);
xStream.aliasField("ID", PersonConverter.MutablePerson.class, "id");
xStream.aliasField("FIRST-NAME", PersonConverter.MutablePerson.class, "firstName");
xStream.aliasField("LAST-NAME", PersonConverter.MutablePerson.class, "lastName");
xStream.registerConverter(new IdXStreamConverter());
xStream.registerConverter(new PersonConverter());
xStream.alias("PERSONS", ListOfPersons.class);
xStream.addImplicitCollection(ListOfPersons.class, "persons");
xStream.addPermission(AnyTypePermission.ANY);
}
}
Now, what if one wishes to use each of the two serialisation configurations in the same application, but in different circumstances? That's what DCI local contexts
are for: they can be activated only in specific portions of the code, bound and unbound to the current thread by specific calls to an instance of
ContextManager (it can be injected, for instance by Spring, or retrieved by calling ContextManager.getInstance()):
final var xStreamContext1 = new XStreamContext1();
try
{
contextManager.addLocalContext(xStreamContext1);
codeThatUsesMarshalling();
}
finally
{
contextManager.removeLocalContext(xStreamContext1);
}
The try/finally pattern to ensure that the context is unbound even in case of exception can be replaced by a shorter syntax using try-with-resources:
try (final var binder = contextManager.binder(new XStreamContext2()))
{
codeThatUsesMarshalling();
}
Variants accepting a lambda or a method reference are also available: runWithContexts() accepts a Runnable or a Supplier, while runEWithContexts()
accepts code that can throw a checked exception, which is propagated. When the code returns a value, it is returned by these methods:
contextManager.runEWithContexts(this::codeThatUsesMarshalling, new XStreamContext2());
final var s = contextManager.runEWithContexts(this::codeThatUsesMarshalling2, new XStreamContext2());
Local contexts are bound to the current thread only, so they are not seen by code running in other threads. Contexts that must be seen by all threads can be
registered by calling ContextManager.addGlobalContext() and unregistered by calling removeGlobalContext().
Now let's go with the implementation of roles. First we introduce a generic support for Marshallable as follows:
@RequiredArgsConstructor
public abstract class XStreamMarshallableSupport<T> implements Marshallable
{
@Nonnull
private final T datum;
@Nonnull
private final XStreamContext xStreamContext;
@Override
public final void marshal (@Nonnull final OutputStream os)
{
xStreamContext.getXStream().toXML(datum, os);
}
}
Two subclasses are required to bear the relevant annotations that bind them with their owners (Person and ListOfPersons).
@DciRole(datumType = Person.class, context = XStreamContext.class)
public final class PersonXStreamMarshallable extends XStreamMarshallableSupport<Person>
{
public PersonXStreamMarshallable (@Nonnull final Person datum, @Nonnull final XStreamContext context)
{
super(datum, context);
}
}
@DciRole(datumType = ListOfPersons.class, context = XStreamContext.class)
public final class ListOfPersonsXStreamMarshallable extends XStreamMarshallableSupport<ListOfPersons>
{
public ListOfPersonsXStreamMarshallable (@Nonnull final ListOfPersons datum, @Nonnull final XStreamContext context)
{
super(datum, context);
}
}
Note that in this case the @DciRole annotation explicitly refers to XStreamContext, since the role must be active only when either of the two contexts is
activated. The context instance is injected in the constructor together with the associated datum instance, so it can provide the XStream configuration.
The implementation of unmarshallers is similar:
@RequiredArgsConstructor
public abstract class XStreamUnmarshallableSupport<T> implements Unmarshallable
{
@Nonnull
private final T datum;
@Nonnull
private final XStreamContext xStreamContext;
@Override @Nonnull
public final T unmarshal (@Nonnull final InputStream is)
{
return (T)xStreamContext.getXStream().fromXML(is);
}
}
@DciRole(datumType = Person.class, context = XStreamContext.class)
public final class PersonXStreamUnmarshallable extends XStreamUnmarshallableSupport<Person>
{
public PersonXStreamUnmarshallable (@Nonnull final Person datum, @Nonnull final XStreamContext context)
{
super(datum, context);
}
}
@DciRole(datumType = ListOfPersons.class, context = XStreamContext.class)
public final class ListOfPersonsXStreamUnmarshallable extends XStreamUnmarshallableSupport<ListOfPersons>
{
public ListOfPersonsXStreamUnmarshallable (@Nonnull final ListOfPersons datum, @Nonnull final XStreamContext context)
{
super(datum, context);
}
}
Now everything is ready:
final var joe = new Person(new Id("1"), "Joe", "Smith");
final var luke = new Person(new Id("2"), "Luke", "Skywalker");
var marshalledPersons = "";
var marshalledPerson = "";
try (final var os = new ByteArrayOutputStream())
{
joe.as(_Marshallable_).marshal(os);
log.info("******** (joe as Marshallable) marshalled: {}\n", marshalledPerson = os.toString(UTF_8));
}
try (final var os = new ByteArrayOutputStream())
{
ListOfPersons.of(joe, luke).as(_Marshallable_).marshal(os);
log.info("******** (listOfPersons as Marshallable) marshalled: {}\n", marshalledPersons = os.toString(UTF_8));
}
For what concerns unmarshallers, since as() must be called on an instantiated object a “prototype” empty object must be created. It is immediately discarded,
as the relevant object is the one returned by the unmarshall() call.
try (final var is = new ByteArrayInputStream(marshalledPerson.getBytes(UTF_8)))
{
final var person = Person.prototype().as(_Unmarshallable_).unmarshal(is);
log.info("******** Unmarshalled person: {}\n", person);
}
try (final var is = new ByteArrayInputStream(marshalledPersons.getBytes(UTF_8)))
{
final var listOfPersons = ListOfPersons.empty().as(_Unmarshallable_).unmarshal(is);
log.info("******** Unmarshalled persons: {}\n", listOfPersons);
}
Global and local contexts can co-exist: local contexts just bind new roles in addition to those made available by the global context. Multiple local contexts
can be used at the same time. If the same role is bound by more than a single context, all of them are available by calling the method As.asMany().
For what concerns the As.as() or As.maybeAs() methods that return a single role, at the moment it is not deterministic which one is returned.
See issue TFT-192.
While the global context is immutable, local contexts can come and go; the lifespan of a typical owner object encompasses multiple activations and
deactivations of local contexts. So, to what instant of the owner lifespan do the set of roles returned by as() refer? Always at the creation time of
the owner object, even though roles are not necessarily instantiated at that moment. The runtime takes a snapshot of local contexts active a creation
time of a owner object and uses that snapshot every time it searches for a role.
While local contexts can come and go, the roles of an object are always searched for with the contexts that were active when the object was created: the runtime takes a snapshot of them at that moment and uses it every time it searches for a role. So an object created within a local context keeps finding the roles bound to that context even after the context has been unbound, or in another thread.
Note that the snapshot is taken when the object implementing As is created. When roles are retrieved by means of AsExtensions (or Lombok
@ExtensionMethod) from an object that doesn't implement As, a new implementation of As is created at each call, so the contexts active at the time of
the call are used.
Local contexts in Finders
When the results of a Finder are objects implementing As, the programmer might want to use the local contexts specified when the Finder is created,
rather than those active when it computes the results. In this case the local contexts can be activated inside the Finder implementation.
For this reason Finder provides the method withContext(Object), which makes the Finder aware of a context (it can be called multiple times, in which
case contexts are accumulated); implementations that don't support it throw UnsupportedOperationException. The class HierarchicFinderSupport implements
the accumulation and makes the contexts available to subclasses by means of the protected method getContexts().
PENDING: Show a code example.
Composite roles
A role can be implemented by referring to other roles. For instance, let's introduce two example roles that save/load an object to/from a Path:
public interface Savable
{
/** Shortcut for {@link it.tidalwave.util.As}. */
public static final As.Type<Savable> _Savable_ = As.type(Savable.class);
public default void saveTo (@Nonnull final Path path)
throws IOException
{
saveTo(path, UTF_8);
}
public void saveTo (@Nonnull final Path path, @Nonnull final Charset charset, @Nonnull OpenOption... openOptions)
throws IOException;
}
public interface Loadable
{
/** Shortcut for {@link it.tidalwave.util.As}. */
public static final As.Type<Loadable> _Loadable_ = As.type(Loadable.class);
public default <T> T loadFrom (@Nonnull final Path path)
throws IOException
{
return loadFrom(path, UTF_8);
}
public <T> T loadFrom (@Nonnull final Path path, @Nonnull final Charset charset, @Nonnull OpenOption... openOptions)
throws IOException;
}
They can be used as follows:
joe.as(_Savable_).saveTo(path1);
ListOfPersons.of(joe, luke).as(_Savable_).saveTo(path2);
final var p = Person.prototype().as(_Loadable_).loadFrom(path1);
final var lp = ListOfPersons.empty().as(_Loadable_).loadFrom(path2);
We can provide implementations relying upon the Marshallable / Unmarshallable roles, whose instances can be obtained by using as(). This could be done
directly on the datum, if it implements As; or by creating a delegate by means of As.forObject() as in the example below:
@DciRole(datumType = Object.class)
public class MarshallableSavable implements Savable
{
@Nonnull
private final As datumAsDelegate;
public MarshallableSavable (@Nonnull final Object datum)
{
this.datumAsDelegate = As.forObject(datum);
}
@Override
public void saveTo (@Nonnull final Path path, @Nonnull final Charset charset, @Nonnull final OpenOption ... openOptions)
throws IOException
{
assert charset.equals(UTF_8);
try (final var os = Files.newOutputStream(path, openOptions))
{
datumAsDelegate.as(_Marshallable_).marshal(os);
}
}
}
@DciRole(datumType = Object.class)
public class MarshallableLoadable implements Loadable
{
@Nonnull
private final As datumAsDelegate;
public MarshallableLoadable (@Nonnull final Object datum)
{
this.datumAsDelegate = As.forObject(datum);
}
@Override
public <T> T loadFrom (@Nonnull final Path path, @Nonnull final Charset charset, @Nonnull final OpenOption ... openOptions)
throws IOException
{
assert charset.equals(UTF_8);
try (final var is = Files.newInputStream(path, openOptions))
{
return datumAsDelegate.as(_Unmarshallable_).unmarshal(is);
}
}
}
Configuration of the runtime
Standalone
The implementation of As relies on a singleton named SystemRoleFactory that, given an object, returns all the roles associated to it.
The singleton is retrieved by means of the Java Service Provider Interface, based on the class
ServiceLoader. In short, a special file named
META-INF/services/it.tidalwave.role.spi.SystemRoleFactoryProvider is searched for at runtime and it must contain the name of a provider for
SystemRoleFactory.
The default implementation (without Spring) is unable to find any dynamic role. Applications can specify an overriding implementation, which takes precedence over the default one, such as in the example:
public class HardwiredSystemRoleFactoryProvider implements SystemRoleFactoryProvider
{
private static final List<Class<?>> ROLES = List.of(PersonJpaPersistable.class);
static class HardwiredRoleFactory extends SystemRoleFactorySupport
{
public void initialize()
{
scan(ROLES);
}
}
@Override @Nonnull
public SystemRoleFactory getSystemRoleFactory ()
{
final var h = new HardwiredRoleFactory();
h.initialize();
return h;
}
}
And the META-INF/services/it.tidalwave.role.spi.SystemRoleFactoryProvider file contains:
it.tidalwave.thesefoolishthings.examples.jpafinderexample.HardwiredSystemRoleFactoryProvider
Of course it is possible to provide more sophisticated implementations, such as a classpath scanner (with Spring this is provided out-of-the-box).
For testing
The standard Java SPI approach sets up the runtime once and for all, as it is appropriate for an application. But when running tests a specific runtime must be installed from scratch each time. For this purpose, the runtime should be reset before running a test (or a batch of tests):
SystemRoleFactory.reset();
Then, if the test needs some roles, a specific SystemRoleFactory can be installed, such as a mock or an implementation returning a fixed set of roles:
SystemRoleFactory.set(new FixedOwnerRoleFactory(r(delegateRole2)));
In a similar way, a specific ContextManager (for instance a mock) can be installed by calling ContextManager.set(); in this case ContextManager.reset()
must be called when the test is completed, so it doesn't affect other tests:
contextManager = mock(ContextManager.class);
ContextManager.set(() -> contextManager);
With Spring
To have As working with Spring another dependency must be added:
<dependency>
<groupId>it.tidalwave.thesefoolishthings</groupId>
<artifactId>it-tidalwave-role-spring</artifactId>
<version>5.0-ALPHA-4</version>
</dependency>
In the code it is sufficient to include the bean RoleSpringConfiguration in the application context, as in this example:
@Configuration
public class Main
{
@Bean
public DisplayableExample displayableExample()
{
return new DisplayableExample();
}
public static void main (@Nonnull final String ... args)
{
final var context = new AnnotationConfigApplicationContext(RoleSpringConfiguration.class, Main.class);
context.getBean(DisplayableExample.class).run();
}
}
PENDING: This probably is not strictly required, but it makes stuff such as the
ContextManageravailable with dependency injection.
If annotations are not used and beans.xml files are preferred, the value of RoleSpringConfiguration.BEANS must be included in the XML context.
The Spring adapter is able to scan the classpath to find annotated roles. Java classpath scanners need to work from a set of specified root packages;
the default ones are com, org and it. If custom packages are needed, they can be specified as follows, before the runtime is initialised:
it.tidalwave.util.spring.ClassScanner.setBasePackages("fr:es:de");
or by means of a system property:
-Dit.tidalwave.util.spring.ClassScanner.basePackages=fr:es:de
With Spring, roles can specify additional parameters in their constructor: the runtime will try to inject into them beans defined in the context.
PENDING: Injection qualifiers are not supported yet.
Available examples
| Example | Description | Runtime |
|---|---|---|
| JPAFinderExample | While the main focus of this example is a Finder, DCI is used to inject persistence-related roles. It also demonstrates a custom SystemRoleFactoryProvider. |
Standalone, with Lombok ExtensionMethod |
| DciDisplayableExample | A very simple DCI example. | Spring |
| DciMarshalXStreamExample | DCI used to persist entities with XStream. | Spring |
| DciPersistenceJpaExample | DCI used to persist entities with JPA/Hibernate. | SpringBoot |
| DciSwingExample | A little demonstration of DCI with a User Interface (Swing). | Spring |
TheseFoolishThings :: Utilities