Plug-in architectures do not have to be designed and implemented from scratch since already some sophisticated frameworks exist. The architecture of a plug-in framework shall work with all applications of the intended domain. The Test Suite product is in the domain of Windows-based applications. It is very difficult to design a framework architecture that is applicable for a variety of applications. This means that flexibility and extensibility are essential for a framework design [GHJV95, p. 27]. Additionally, low coupling between the framework and the application is important. Modifications on the framework should not bring much migration work for the application. Gamma et al. write the following about these issues:
A framework that addresses them using design patterns is far more likely to achieve high levels of design and code reuse than one that doesn't. Mature frameworks usually incorporate several design patterns. The patterns help make the framework's architecture suitable to many different applications without redesign [GHJV95, p. 27].
Basic knowledge of the most important design patterns and concepts in the field of plug-in architectures help to evaluate different plug-in frameworks. Particularly, it is easier to estimate how a framework will affect the whole application design. The previous chapter gives a short introduction into the Plugin pattern. Whereas this chapter addresses some further important design patterns and concepts used in the field of plug-in architectures.
The Dependency Injection (DI) pattern arose from the Java community when they tried to find alternatives to the high complex enterprise Java world. This pattern helps to wire components of different layers together. The components are often developed by different teams with minor knowledge of each other. A well-known task for an architect is to compose the components into a coherent overall application. A number of design patterns, such as Factory Method, Abstract Factory, Builder, etc. [GHJV95], are already devoted to deal with this issue. An alternative for implementing these design patterns is to use a reliable framework. Some frameworks, which deal with the wiring of components, are known as Inversion of Control (IoC) container. They are also referred as lightweight containers because of the minor performance impact and the lower application complexity compared to other container technologies (e.g. Microsoft .NET Framework Enterprise Services) [Caprio05].
Inversion of Control is a general principle which is often used to characterize frameworks [Fowler05]. It is also known as Hollywood principle "Don't call us, we'll call you". It means that the framework takes control over the program and calls the code of the client. For example, a GUI framework calls a method of the client if a button is pressed. Fowler writes that this term is too general and does not suite as a description for the pattern used by IoC containers [Fowler04a]. Thus the name Dependency Injection is used for this particular pattern.
In Dependency Injection a client object (Birthday printer
) declares its
dependencies (Address book
). Dependencies are objects (Address book
Implementation
) which are required by the client to fulfill its tasks. The
client is not responsible to get the dependent objects. This is done by an
external mechanism which is known as Assembler
. The specific characteristic of
this pattern is that the client does not have any dependencies to the Assembler
or any other object for locating the dependent objects. The resulting
dependencies between the classes can be seen in Figure 2.
Figure 2: UML class diagram for dependency injection [Fowler04].
The tasks of the Assembler
are:
Birthday printer
).Address book Implementation
) Birthday printer
)setAddressBook
). This process is shown in Figure 3, except of reading the dependency information.
Figure 3: UML sequence diagram for dependency injection
[Fowler04].
The Dependency Injection pattern does not define in which way the
dependencies have to be declared. A popular approach is to write the
dependencies in an external file, particular in an XML file. Another possible
solution seen in DI frameworks is the using of associated meta-data direct in
the programming language like Attributes
in .NET or Annotations
in Java.
How the Assembler
locates the dependent object is not specified. The Plugin
pattern can be used for this task. A common way for the configuration of the
Plugin factory is the use of an XML file. XML files can be easily changed for
different deployment scenarios. Nevertheless, other approaches can be useful too
like retrieving the configuration dynamically from a server.
The injection can be done in various ways. Fowler writes that there are three main styles of dependency injection [Fowler04a]:
An example for Setter Injection can be seen in Figure 3. The Assembler
calls
the setter method setAddressBook
of the object Birthday printer
to inject an
implementation of the Address book interface
.
An alternative to the previous approach is that the Birthday printer
class
does not provide the setter method. Instead, it requires the Address book
implementation already in the constructor (Listing 1). The Assembler
passes the
implementation of the Address book
interface to the Birthday printer
constructor. This procedure is called Constructor Injection.
1 public class BirthdayPrinter
2 {
3 private AddressBook _book;
4
5 public BirthdayPrinter(AddressBook book)
6 {
7 _book = book;
8 }
9
10 ...
Listing 1: Extract of the client class which is configured by constructor injection.
Interface Injection is not relevant for this diploma thesis because the investigated solutions do not support this type of injection. Most of the lightweight containers do not promote this approach. According to Fowler, the reason is the more invasive nature of Interface Injection since many interfaces are required to get it working [Fowler04a].
An alternative to Dependency Injection is the Service Locator pattern
[Sun02]. Basically, it uses a central object (Service locator
) that knows how to
locate the dependent objects (Address book Implementation
). The dependent
objects are referred as services in this context. The client (Birthday printer
)
requests the concrete implementation of the Address book
interface from the
Service locator
. In contrast to the Dependency Injection pattern the client
takes an active role in retrieving the concrete implementation. Thus, it has a
dependency to the Service locator
(Figure 4).
Figure 4: UML class diagram for a service locator [Fowler04].
This time the tasks of the Assembler
are (Figure 5):
Address book Implementation
).initAddressBook
).
Figure 5: UML sequence diagram for a service locator [Fowler04].
How the Assembler
is going to find the right implementation is neither
specified by this pattern nor specified by the Dependency Injection pattern. In
this case the Plug-In pattern is a possible solution too.
The Service locator
class can be realized as a Singleton [GHJV95, p. 127]. If
the Service locator
should provide an implementation depending on the
application context, the Registry pattern [Fowler03, p. 480] is a good
alternative. For example, the Registry is able to provide a separate database
connection service for every thread which simplifies the development of
multi-threaded applications.
The class diagram (Figure 4) shows the Service locator
class with the service
specific methods initAddressBook
and getAddressBook
. These methods can be
written in a more general way, so that different services can be registered and
retrieved. The example code (Listing 2) shows how generics can be used to write
a general ServiceLocator
class.
1 public class ServiceLocator
2 {
3 public static T get<T>() { ... } // Instead of getAddressBook
4
5 public static void register<T>(T service) { ... } // Instead of
6 // initAddressBook
7 public static void deregister<T>() { ... }
8 }
Listing 2: A generic service locator implementation.
In Listing 2 the type T
is used to identify the service. Alternatively, a
string or integer value could be used as an identifier. Using the types has the
advantage that the refactoring and error checking capabilities of the IDE still
works. The disadvantage is that only one service of the same type can be
registered. Thus, this approach is not as flexible as using string or integer
values as an identifier [Nilsson06, p. 373].
The .NET Framework provides Attributes
for adding meta-data to an assembly, a
type, a type member or other targets. The Attributes
can rather be used to
declare information in the code than creating external configuration files. This
is also known as declarative programming. The meta-data can be read by an
application through the reflection API of the .NET Framework.
Declarative programming is an interesting alternative for configuring
frameworks to the classic configuration files. The main advantage is that the
Attributes
are associated directly with a target. This can save a lot amount of
configuration as it is shown in Listing 3 and Listing 4.
1 [ServiceDependency]
2 public IMovieFinder MovieFinder
3 {
4 set { _movieFinder = value; }
5 }
Listing 3: Configuration of setter injection with an
Attribute
.
1 <objects>
2 <object id="MyMovieLister" type="MovieLister.MovieLister,
3 MovieLister">
4 <property name="MovieFinder" ref="MyMovieFinder" />
5 </object>
6 ...
7 </objects>
Listing 4: Configuration of setter injection with an external XML file.
These both code examples list a setter injection configuration for the same
component. Listing 3 uses a ServiceDependency
attribute for the configuration.
The configuration is minimal as it consists only of the Attribute
type name. The
Attribute
is directly above the MovieFinder
property and thus, the meta-data is
attached to this property. Listing 4 configures a setter injection for another
Dependency Injection implementation in an external XML file. Here the
configuration consists of the lines 2, 3 and 4. In this case, more information
is necessary to configure the injection. Most of this information is necessary
to address the MovieFinder
property. If the configuration has to refer to the
code, the approach with Attributes
needs less amount of information.
Furthermore, the maintenance is simplified because the code and configuration is
at the same place. This makes in many cases of component refactoring,
modifications to the configuration unnecessary. For example, the renaming of the
MovieFinder
property does not require a change to the information declared by
the Attribute
.
Attributes
also have a few drawbacks. The main weakness is that they do not
physically separate the configuration from the code. If the Attributes
are
overused, the source code can become messy [Sosnoski05]. Additionally the code
requires a reference to the assembly that provides the Attributes
. This
reference can be a problem if the code should be independent of the framework or
the library (Framework dependencies, p. 38). The use of a configuration file
does not have these drawbacks.
Sosnoski [Sosnoski05] writes in more detail about the differences of using
meta-data inlined with the code and configuration files. He uses the term
Annotations
instead of Attributes
as it is the Java keyword for the same
concept. Declarative programming and external configuration files are widespread
for framework configuration. Understanding the impact of these concepts on the
application design helps to evaluate the frameworks.
The Dependency Injection and the Service Locator patterns are two possible ways to wire different components together. Wit the Service Locator pattern the client retrieves its dependent objects by requesting a central object. In this case, the client has an active role to get hold of the needed objects. In contrast, the client in the Dependency Injection pattern has a passive role. An external mechanism is responsible that the client gets the dependent objects. This mechanism is known as injection.
These patterns are often seen in plug-in architectures. Understanding them can help to evaluate the different architectures and frameworks. Dependency Injection has a minor impact for the application design whereas Service Locator is easier to understand and to debug. In the Service Locator pattern it is also possible to provide different objects depending on the application context. Which pattern should be preferred is dependent on the requirements.
A Dependency Injection implementation requires some kind of configuration.
The most popular ways for configuring are the use of Attributes
and the use of
external configuration files. Both concepts have different advantages and
drawbacks. Which of them should be preferred depends on the requirements defined
for the application.