Java reflection was introduced to enable container-component architectures.
Using reflection containers can discover the required and provided interfaces of added components and connect clients with providers.
Here are a few of the classes provided in java.lang and java.lang.reflect:
Assume the following declarations have been made:
class SomeClass { ... }
SomeClass someObject = new SomeClass();
There are two ways to get an instance of Class:
Class<SomeClass> sc = someObject.getClass();
Class<SomeClass> sc = SomeClass.class;
If we don't know the class being represented we can write:
Class<?> sc = someObject.getClass()
Class<?> sc = SomeClass.class;
We can see how to use reflection in ReflectionDemo.java.
A statistics calculator uses an ordinary calculator to compute the mean of a list of numbers.
Both the calculator and statistics calculator extend the container's Component class.
The statistics calculator implements the App interface to indicate that the container should call its main method. This method computes the mean of 1, 2, ..., 99.
The test driver simply executes the following lines:
Container container = new Container();
container.addComponent(new StatsCalculator());
container.addComponent(new Calculator());
container.launch();
Here's the actual code:
Some important observations:
· StatsCalculator contains no reference to Calculator. Its field arithmeticCalculator is never initialized. This is done by the container.
· To enable initialization, we must provide a setter method named setArithmeticCalculator.
· Except for declaring themselves extensions of the Component class, there is no additional overhead code that must be written.
Here's how we might represent the calculator app as a component diagram:
The component platform only contains two classes:
The source code is here:
The container class maintains two tables of type Map<Class, Component>: provided and required.
When a component is added to a container, it is added to the provided table indexed by all interfaces that it implements.
For example, when a calculator is added to a container, the row (ICalculator, calculator) is added to the provided table. This indicates that the calculator provides an implementation of the ICalculator interface.
Next, the component is added to the required table indexed by all interfaces it requires.
For example, when a stats calculator is added to a container, the row (ICalculator, statsCalculator) is added to the required table.
Finally, addComponent traverses the required table. If any of the interfaces also appear in the provided table, then the corresponding provider component is assigned to the corresponding client component in the required table and the row is removed.
For example, (ICalculator, statsCalculator)
is a row in the required table, and (ICalculator, calculator) is a row in the provided
table, so
statsCalulator.setProvider(ICalculator.class, calculator)
is called. Of course all of this is done using reflection and with no knowledge
of the actual identity of any components or interfaces.
For efficiency, a component maintains its sets of provided and required interfaces. These are actually computed by the Component constructor.
The provided interfaces of a component are:
provided = this.getClass().getInterfaces();
To compute the required interfaces we assume that if a component has a field of interface type, then this interface is required. We further assume that these are the only required interfaces. We also assume that such fields will have setter methods.
For example, StatsCalculator has a field called arithemeticCalculator of type ICalculator, an interface. Therefore, ICalculator must be required by StatsCalculator, which must have a method called setArithmeticCalculator.
The call statsCalulator.setProvider(ICalculator.class, calculator) uses reflection to set up
a call to statsCalculator.setArithmeticCalculator(calculator).
1. Add the following methods to Container:
addComponent(className)
This method loads and instantiates the class with the specified name. Instances can be passed to the original addComponent method.
2. Provide the container with a user interface that displays all of the components.
3. The example above works because there is a single ICalculator interface. What would happen if Calculator and StatsCalculator were implemented separately? How would the container figure out that the ICalculator interface provided by the calculator was the same ICalculator interface required by the stats calculator?
3. Declare the following message class:
class Message {
private Component sender;
private String text;
// etc.
}
Provide components with a protected message queue and the ability to send messages to other components.
Provide the container with a method for broadcasting messages to all components except the sender.
Component should implement Runnable. The run method perpetually reads messages from the message queue and reacts to them.
Implement the nuclear reactor example.