In one of my latest articles on CDI, one of the lessons I learned was to use factory methods in order to reduce the number of classes. In this article, I will go into the detail since I think it is of some importance. More basic informations on CDI can be found in some previous posts: overview part 1 and overview part 2.
The naive approach
During school, you were probably taught to create a class for each component. For example, if your application has 4 buttons, North, East, South and West, you’ll have 4 classes. Each class configures its label, its icon and its action, either in the constructor or in some other method.
In CDI, your can annotate such method with @PostConstruct in order for the container to call it just after the constructor.
|
This produces the following code, when taking Swing as an example:
public class NorthButton extends JButton {
public NorthButton() {
// Initializes label
// Initializes icon
// Initializes action
}
...
}
Common behaviour and attributes may be factorized into one superclass, thus you end up with 5 classes.
Using the right number of classes is very important in any object-oriented language:
- If you use too few classes, you run the risk that each class you develop has too many responsabilities and are too big.
- On the contrary, if you use too much classes, you’ll explode code into many places and it will be a maintenance nightmare.
Moreover, in the Sun (Oracle ?) JVM, classes are loaded into the PermGen space which has a fixed size.
Who has never seen the dreaded
java.lang.OutOfMemoryError: PermGen space
?
IMHO, both are good reasons not to create a single class for each instancied component.
The factory approach
The second logical step is to create methods that produce the right instance. In our previous example, that would mean a factory class, with 4 methods to produce buttons, one for each button and probably a 5th method with common behaviour:
public class ComponentFactory {
@North
@Produces
public JButton createJButton() {
JButton button = new JButton();
// Set label
// Set icon
// Set action
return button;
}
...
}
Notice that you’ll still need an annotation for each single component:
@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD,METHOD,PARAMETER,TYPE})
@Qualifier
public @interface North {}
In our case, that makes @North
, @East
, @South
and @West
.
If these components are found throughout your application, fine (such as a Cancel button).
If not, you still have a class for each component (annotations are transformed into classes at compile-time).
The "generic" annotations approach
CDI let you qualify injection not only from the annotation type but also from some (or all) their elements. We can thus greatly diminish the number of needed annotations. In order to do this, just create an annotation on this model:
@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD,METHOD,PARAMETER,TYPE})
@Qualifier public @interface TypeName {
Class value();
String name();
}
Now the factory method becomes:
public class ComponentFactory {
@TypeName(value = JButton.class, name = "North")
@Produces public JButton createJButton() {
JButton button = new JButton();
// Set label
// Set icon
// Set action
return button;
}
...
}
Although this method meets our requirements regarding the number of classes (and annotations), it still has one major drawback: now, it’s the number of methods that is vastly increased. Though not nearly as great a disadvantage as a great number of classes, it makes the code less readable and thus less maintainable.
The fully "generic" approach
CDI let you use an injection point parameter for producer method. This parameter has much information about, guess what… the injection point. Such informations include:
- the reflection object, depending on injection’s type, respectively
Field
,Method
andConstructor
for field, method parameter and constructor parameter injection - the required type to inject
- annotations relative to the injection point
The latter point is what drives what comes next. It means you can annotate your injection point, then get those annotations during producer method execution. In our case, we could have an annotation for each attribute we want to set (text, icon and action). Let’s create such an annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD,METHOD,PARAMETER,TYPE})
public @interface Text { String value(); }
Such annotation could be used in the producer method in the following manner:
public class ComponentFactory {
@Produces
@TypeName(JButton.class)
public JButton createJButton(InjectionPoint ip) {
JButton button = new JButton();
Annotated annotated = ip.getAnnotated();
if (annotated.isAnnotationPresent(Text.class)) {
Text textAnnotation = (Text) annotated.getAnnotation(Text.class);
String text = textAnnotation.value();
button.setText(text);
}
// Set icon
// Set action
return button;
}
}
Now, the only thing you have to do is annotate your button field like so:
public class Xxx {
@Inject
@TypeName(JButton.class)
@Text("North")
private JButton northButton;
...
}
This will get us a button with "North" as label. And nothing prevents you to do the same with icon and action.
Morevover, with just text, you could go further and manage internationalization:
@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD,METHOD,PARAMETER,TYPE})
public @interface Text {
// Key in the properties file
String value();
// Path to the resource bundle
String resourceBundle() default "path/to/resource/bundle";
}
This annotation could also be reused of other components that manage text, such as labels.
Conclusion
There’s no right or wrong approach in using CDI. However, the latter approach has the merit of being the most powerful. Additionally, it also let you write code relative to a component type in one single place. This is the road I’ve taken so far, and I’m very satisfied with it.