Home
Syringe consists of two parts:
- Syringe dependency injection framework - based on XML/XSD configuration, now obsolete but used by Syringe Perspectives
- Syringe Perspectives - configuration framework build upon Syringe and Scala traits
The following text describes concepts of Syringe, if you want to dive in directly continue with the tutorial.
Syringe Perspectives is a configuration framework build on top of Syringe dependency injection framework. The motivation for it came from the fact that XML/XSD configuration may quickly become unmaintainable as the project grows. Perspectives uses Scala programming language since this language has several nice features suitable for modular programming, like traits, lambdas, natural singletons, implicit conversions etc. The name of the framework reminds the basic idea that a developer can look at an assembled application from several perspectives: usually design, decoration and configuration perspectives.
As was stated, Perspectives is built on top of Syringe dependency injection. Thus, a developer still creates config beans and uses annotations for marking injectable bean properties. However, instead of generating XSD files when building, the Syringe Maven Plugin generates a Scala source code file - aka palette - containing builders for all config bean components found in the module.
A module is defined as a Maven project containing config bean classes. The module's POM file contains the Syringe Maven plugin configured so as to generate the Scala file called palette.
A palette is a Scala trait consisting of builders for all components encountered in the module from which the palette was generated.
Component builders classes are implemented as inner classes within the palette trait (e.g. ComponentXBuilder
in the picture). For each builder class there is one factory method creating an instance of the component builder. This factory starts with new
and is meant to be overridden possibly several times in the hierarchy of subclasses. Each overriding factory method in a subclass contributes to the total configuration of the builder being created.
Important: Every property of a builder can be configured at most once. If more overriding methods attempt to configure the same property, Syringe will complain about it and will fail. Similarly, if a mandatory property is not configured at all, Syringe will fail and navigate you to the problematic property.
In addition to builder factory methods there is, for the sake of convenience, one lazy val (lazily initialized final field) for each component containing a reference to the component builder created by the factory method. This builder instance is suitable in case the component created by the builder is a singleton by nature and it can be used directly in the assembling code. If you need more instances of the component class configured differently you will have to create a new builder factory method calling the default component factory method. Doing it allows separate shared configuration from specific one. The configuration shared by all instances can be placed to the default factory method, while the specific ones can be placed to the new factory method.
Palette MyPalleteA
in the following figure declares a new independent builder of ComponentX
having fixed property2
(specific configuration) and at same time it fixes property1
to value 0 for all ComponentX
instances (shared configuration) by overriding the default factory method.
If an application uses MyPaletteA
it is no longer allowed to configure the fixed properties.
Each palette extends the Module
interface implementing the main method making the palette executable as a Scala main class. The default main in the Module
trait implementation does nothing so it is up to the palette or the application to implement its logic.
A builder is responsible for creating an instance of a component class according to its configuration. The configuration is held in the builder's properties exposed as one-parameter methods of the builder. Each property corresponds to a property in the component class (a field marked by @ConfigProperty
). The property setters on the builder are designed so as to allow the fluent syntax.
In very simple applications the properties can be set directly on a builder assigned to a val in the palette. However, in more complex applications assembling should be done by overriding builder factory methods and be separated to more traits, perspectives in other words.
An assembling code must set all mandatory properties of used builders (exactly once, not more) or install a so called property resolver (addPropertyResolver
) before the build
method is invoked.
In case the value assigned to the component instance must be changed, a value converter can be installed to the builder (setValueConverter
).
The instance created by a builder can be wrapped by another instance or even by more instances, so called decorators. In this case the build method returns the last decorator (the outermost). All decorators must have a property marked by @ConfigProperty(delegate=true)
that is meant to hold the wrapped instance and its type must be assignable to the wrapped instance. Since the type of the returning instance differs from the original class (it is the class of the outermost decorator), the caller of the build method should not rely on the Scala's type inference and declare explicitly the expected type of the variable to which the instance is assigned. Assume for example that the builder of ComponentX
returns an instance of class ComponentX
implementing Runnable
. If the builder is decorated, it is the decorator instance what is returned to the caller of build
. As decorators should implement the same interface they decorate, it is possible to explicitly declare the variable type as Runnable
, as shown in the following snippet:
val x: Runnable = componentX.build
x.run()
Note: This feature is under development at this moment, only component classes that implement some interface should be decorated. The type of the resulting instance should expect the interface instead of the original class.
As an application grows its assemblage and configuration becomes more and more tedious and messy. The key idea of this framework is to facilite the aforementioned tasks by separating them into several phases or perspectives. Each perspective represents one view of a developer on the assembled application. The practice has shown that there are basically three main perspectives present in complex applications: design, decoration and configuration.
By taking the design perspective the developer should recognize overall structure (a graph of interconnected components) of the application or its module. It is the coarsest grained view on the application and it should not be disturbed by finer information like host/port configuration at which the application is listening.
On the other hand, the configuration perspective should contain the finests pieces of the assemblage, which typically is setting configuration options and parameters. Defining it this way, everyone can go directly to this perspective if he needs to tweak some options in the application.
The decoration perspective lies in between the above-mentioned two. Its main purpose is to configure aspect features of the application, like decorating components, logging, security, transactions, applying filters and so on.
The following picture shows an application, whose assemblage is separated to the three perspectives: