COMP 310 |
Component-Framework Systems |
Quick links to the content below:
The major
goal in software construction is to build software that are robust (i.e.
performs correctly and does not crash even in unforeseen cases), flexible (i.e.
can be used in many seemingly different applications), extensible (i.e. can be
extended with minimal if not no modification of existing code), easy to
maintain (i.e. to modify to improve performance or fix "bugs"). The main tool
of the trade of software developers can be summarized in one word:
Abstraction.
The effort spent in identifying and delineating the invariants from the variants usually leads to the construction of what is called a component-framework system where the invariants form the framework and the variants constitute the components. (Note that "component-framework systems" are often referred to more simply as just "frameworks") The seminal "Design Patterns" book (login req'd for link -- see reference below) defines a framework as follows:
"A set of cooperating classes that makes up a reusable design for a specific class of software. A framework provides architectural guidance by partitioning the design into abstract classes and defining their responsibilities and collaborations. A developer customizes the framework to a particular application by subclassing and composing instances of framework classes."
Component-framework systems are ubiquitous not only in object-oriented systems but in procedurally, functionally and declaratively modeled systems as well. The notions of component-frameworks transcend the boundaries of any particular programming language or paradigm and instead, speak to the deeper, unifying computer science principles that unite all languages and paradigms.
In essence, component-frameworks break the system down into variant "components" that represent abstract processing that the system supports. These components are generally thought of as "pluggable" in the sense that the system can be configured with a set of components to accomplish a particular task or set of tasks. In many systems, the components can dynamically swapped in and out. The framework, on the other hand, is the invariant "superstructure" that manages the components, providing services to each component that, when combined with the services/behaviors supplied by that component, create the net behavior defined by that component in that framework. The framework has the effect of decoupling or isolating the components from each other since it the component interactions are handled by the framework in a manner that is opaque to the components. Note that this does not preclude any ability of the components to communicate with each other outside of the confines of the framework, they are only decoupled with respect to their interactions through the framework.
One of the key and arguably, defining, characteristics of a component-framework system is its "inversion of control" or "Hollywood Principle": "Don't call us, we'll call you!" Since the framework portion of the system represents the invariant aspects of the system that mediates the dynamics of the variant components, control of the system must be delegated to the framework because the components are only providing services to the framework and are unaware of how those services interact with each other. In other words, it is the framework that is "running the show", directing the calls to the component services, not the other way around. Inversion of control virtually eliminates flow control in the components and thus reduces code complexity in the components. As discussed below, this inversion of control is one of the key distinguishing features between framework-based systems and library-based systems.
Component-framework systems have numerous software engineering advantages:
Frameworks can vary greatly in size, ranging from small data structures to enterprise-class distributed applications. Frameworks can even be nested inside of other frameworks, i.e. "layered frameworks", but that discussion is beyond the scope of this article.
[top]
Component-framework systems come in two basic "flavors": white-box and black-box. The two types differ in how the framework relates to the components.
In a white-box framework, the components have an inheritance relationship with the framework, i.e. are subclasses of the framework. That is, the framework services are concrete methods that each component inherits from their superclass(es). The implementation of the framework is thus in the component superclass(es). The framework calls to the components by calling abstract methods in the superclass from the concrete framework methods. The concrete component classes override these abstract methods with their specific behaviors. This calling of abstract methods from concrete methods is called the Template Design Pattern and is characteristic of white-box frameworks.
In a black-box framework, the components have a compositional relationship with the framework, i.e. are typically fields of the framework. Here, the framework and components are completely separate objects with no inheritance relationship whatsoever. Typically, the compositional relationship is bi-directional so that not only can the framework call on services provided by the component, but the component in turn, can call on services provided by the framework. Framework services manifest themselves as methods of the framework object while component service are methods of the component objects. Since the components are variant entities, the framework must reference the components as abstract entities with abstract behaviors (methods). This delegation by the framework to an abstract entity to fulfill some operational purpose is the Strategy Design Pattern and is characteristic of black-box frameworks. Often, a black-box framework is a specific, concrete entity but for systems that need upgradeability in the framework without disturbing the components, the framework may present itself to the components as an abstract entity.
White-box frameworks tend to create greater coupling between the components because they are all required to have the same inheritance ancestry. While a disadvantage in many systems, it is advantageous in systems where tightly regulated control of the component processes is required. Such systems include graphical user interfaces (GUIs) and security architectures. White-box frameworks also tend to be easier to understand and program because of the fewer objects involved.
Black-box frameworks promote maximum decoupling between the framework and its components, leading to increased flexibility and extensibility. For this reason, most large and/or highly-configurable systems are black-box frameworks. The disadvantage is that black-box systems can involve many abstract entities, making them more difficult to understand and program.
A variant of the black-box framework is one where the framework does not keep permanent references to its components but rather is handed a component to use dynamically whenever that component is needed. This "on-demand" variation is described in more detail below.
It should be noted that it is possible for a system to exhibit a combination of white-box and black-box characteristics, as the does the Java JFC/Swing example below. These sorts of systems are sometimes referred to as "grey-box" frameworks.
[top]
There is often a confusion as to the difference between component-framework systems and library-based systems where the system is configured with variant libraries that provide variant behaviors such as network connectivity, data processing, etc. Library-based systems look like black-box systems in that the libraries provide abstractly-defined services to the application-specific code. The key difference however is the lack of inverted control in library-based systems where the application code is firmly running the show. In addition, in library-based systems, the separation of the variant and invariant portions of the system can be problematic as illustrated in the following diagram.
Often, especially when multiple independent libraries are being used simultaneously, the application-specific code will contain encapsulating structures that are used to manage those libraries and to provide a consistent interface for the "business logic" portion of the application code. These encapsulating structures are often invariant across multiple application implementations because they effectively represent an invariant common library used to manage the other variant libraries. In addition, the application-specific code must also contain what is typically invariant system management code that controls the system startup/initialization, data and event flow, etc. In these situations, we see a very problematic scenario of the variant application-specific code now containing invariant common library code, as illustrated in the diagram below:
Here, we clearly see that the invariant library and system management portions of the code are buried inside of the variant application layer/business logic portion of the code. This prevents a clean variant-invariant separation in the system, leading to inflexibility, lack of extensibility and difficult code maintenance.
The answer seems simple: Excise the invariant common library code from application code and represent it as its own entity in the system. This is certainly a very reasonable thing to do and when one does this, one discovers that one ends up with a system diagram that looks almost the same as the component-framework diagram above where the main difference is that it doesn't implement the inversion of control used by frameworks.
Since the application code still retains control over the system operation, the invariant system management code must still reside in it, so the clean variant-invariant separation is still not achieved.
Libraries are useful when the variant nature of the library services are needed for a wide range of applications where there is little to no commonality in how the libraries are being used. This situation is one where the invariance of the system management part of the code is minimized because of the lack of commonality between applications. This enables a much cleaner variant-invariant separation in the system and minimizes the need for an invariant, over-arching framework to manage the overall system.
[top]
The first step in designing the architecture for any system is to create detailed Use Cases. Arguably the most common misstep in system architecture design is skipping the Use Case detailing entirely or believing that simple, vague Use Cases are sufficient. It cannot be stressed enough that understanding the problem from the user's perspective and creating specific and detailed articulations of that viewpoint are critical for project success. It is extremely important to create a Use Case diagram that unifies the Use Cases into a form where the overall picture of the system can be seen.
A lot of developers skip directly to the class architecture design step or worse, skip directly to the coding step, bypassing the above Use Case process. But skipping is essentially an attempt to solve a problem without knowing the question. Too many projects take that "shortcut" and end up with products that are useless to their customers because they solve an imaginary problem in the developers' minds and not the real-world problem facing the customer.
The emergence of a component-framework in the architecture of a system occurs during the process of converting the Use Case diagram into a System Block Diagram. This is the transition from the user perspective into the computer science perspective of the problem. It is here, now that the actual problem has been well-defined, that one can begin to separate the variant and invariant pieces of the system. In the end, the system block diagram should illustrate, amongst other things, the framework, the components and their relationships in terms of operations (use cases).
When translating Use Cases into method calls, it is important to maintain the decoupling that the variant-invariant separation process has created. This means writing methods strictly in terms of what any object needs from the objects with which it is communicating. This means that the critical questions in terms of designing the component-framework interfaces are:
A very common mistake in the process of translating Use Cases into method calls is to inadvertently couple one part of the system to another by writing method calls in terms of "what does the other object offer me to use?". The effect of this error is to force one entity to configure its behavior based on the variance of what services are or are not available on another entity rather than on the invariance of an abstract definition of the needs of the first entity from its environment.
It should be noted that in a component-framework system, not all components are required to conform to a single common interface. Different classes components may be used in different operational areas of the framework and thus implement different interfaces both on the component and framework sides to support those differences. Multiple inheritance can be used to represent and manage common aspects of interfaces across the system.
[top]
In an application framework, where the component-framework system is the entire application, the framework defines the invariant "shell" that manages the operations of a variety of derived applications. There is a tendency to believe that the application-specific code must be the one that starts the system because of its variant nature. However, since a component-framework runs with inverted control and since the framework is the entity that controls the interaction of the system components, it is the framework that starts the system, not the application code.
The application-specific code, aka the "business logic", is just another component in the system. This is variant code and thus is on the component side of the system. The application-specific code is presented as services to the framework that is called in response to events and other processes managed by the framework.
The startup main()
method of the application needs only to instantiate and start the framework.
Note that this implies that the framework MUST be able to start
up without any application-specific initialization. Note
that this does NOT mean that the framework will startup in a fully
operationally-ready mode. It just means that the framework starts up and
takes control of the operation of the system, establishing the inversion of
control.
The application-specific initialization of the system is just one of the services provided to the framework by the business logic component. The framework will call to the business logic component when it is ready to be initialized. The framework will be ready for full operation after this call to the provided initialization service is complete. More on how the business logic could go about the initialization process can be found in the next section.
[top]
Since the startup of the framework is invariant, where and when do the
variant components get installed? It is tempting to write a
custom main()
method that installs the concrete components as part
of the framework instantiations but if one were to do so and write such code in
a good, modular form, one would see that the code would break down into an
instantiation of the framework plus a call to some sort of "init()
"
method that encapsulates all the work needed to initialize the framework.
But what is this "init()
" method but just an abstraction of the
initialization process, i.e. a variant service being provided to the invariant
startup code to initialize the framework.
Since this "init()
" method is an abstract, variant service,
simply refactor it out into the location in which it belongs, namely the
business-logic component. This means that the business-logic component,
which contains the application-specific code and thus the knowledge of exactly
what concrete other components are needed in the system, is now responsible for
installing those components.
The framework needs to supply services that enable the dynamic installation of components. Such services are often referred to as "extensibility hooks". This has implications both for the existence of default components that can be used before initialization and for the framework's ability to dynamically initialize and startup any given component. The initialization service provided by the business logic component simply calls these component-installation services on the framework to install whatever other components it desires to customize the framework.
Dynamic component installation often requires that a "matched set" of components be installed as it is common that while the framework treats the components as decoupled, the components in fact may need to intercommunicate or otherwise be coordinated or supply corresponding behaviors in order to properly function together in the system. For instance, an incoming and outgoing serialization protocol may need to be matched when communicating with a remote entity. The issue is compounded by the fact that the business logic may not actually care which components it is installing other than that those components must be a matched set that corresponds to some defined semantic.
What is needed is a way to abstractly construct a set of matched entities (components, here). This is the Builder Design Pattern, which is essentially a giant Factory Design Pattern object that constructs a whole coordinated set of objects, not just one. Builders are very useful for initializing component-framework systems.
An interesting extension of the notion of component-configured frameworks is "on-demand" component installation and execution. In such a scenario, the framework does not have a permanent reference to the component but rather only receives that reference when the component is being used. This is a specialized form of an extensibility hook that simply runs the given component in the context of the framework without ever permanently installing it. On-demand systems offer tremendous dynamic flexibility at the cost of being able to establish run-time invariants based on the installed components. An example of "on-demand" component execution is the Visitor Design Pattern: the data structure host (framework) will "accept" the visitor (component) right when the processing done by that visitor is desired and no permanent relationship between the host and visitor is maintained. The Visitor Design Pattern is thus a black-box framework with no permanent component references and an extensibility hook that is the host's "accept" method. In such, visitors can be created and applied at run-time, even from within the visitors themselves, e.g. in traversal processes through complicated data structures.
In the absence of run-time subclassing, on-demand components would necessarily be found only in black-box frameworks.
[top]
(The following example is drawn from that
of the Java online tutorial with a distinctive modification in the
implementation of the ActionListener
.)
Say we want to write a JFC/Swing GUI application that has a button and when the user clicks on the button, the application makes a beeping sound. Here is what we must know about the JFC/Swing framework and think about in developing this application.
We need a window and a button: we use JPanel
for the window and
a JButton
for the button. The framework provides those classes with
all the standard capabilities of GUI components.
Let’s call our application, class Beeper
: It "is a" JPanel
and "has a" JButton
. Standard “is-a” (inheritance) and
“has-a” (composition) basic OO design stuff! The code will look something like:
public class Beeper extends JPanel { JButton button; … }
Within the JFC/Swing framework, a JButton
object will fire an
“event” called ActionEvent
object when clicked upon. This event
will go unnoticed and consequently un-handled if there is no “listener” for it.
Some things to consider:
JButton
object in this case, MUST “register” a “listener” for the
event via its addActionListener
method. ActionEvent
is specified abstractly by the framework as an interface called
ActionListener
, which has an abstract method called
actionPerformed
. The framework code will call the
actionPerformed
method of all (concrete)
ActionListener
objects that are registered to listen to that
particular event. This is the way the JFC/Swing framework works. This is an
invariant. The application programmer cannot
change it.ActionListener
with concrete code for the
actionPerformed
method are the variants.This is an example of the invariant code in the framework calling the variant code
of the plugged in component. Framework code
more than often are written this way. In here we can see the inversion of
control (the "Hollywood Principle" discussed above) that is characteristic of
frameworks.
The framework knows when and what to call,
provided the component code follows the naming and calling conventions imposed
by the framework. By letting the framework “run the show”, the complexity of
the application code is greatly reduced. Here, the
application code, the listener's actionPerformed
method body, only
has to worry about what to do when the button is clicked, not when
to do it.
What should the concrete ActionListener
for the button
of this application be? There are multiple ways to accomplish this.
We will use a technique in the following code that best illustrates the component-framework
concepts at work here.
What we need is a concrete implementation of the
ActionListener
interface that will be registered as the
listener for the button. An easy, convenient way to accomplish this
is to use an anonymous inner that is derived from
ActionListener
. This anonymous inner class therefore
must implement
ActionListener
's actionPerformed
method. And the
JButton
the Beeper
class contains must register the listener
object (the anonymous inner
class) with its addActionListener
method. The code then will look
something like the following.
public class Beeper extends JPanel { JButton button; public Beeper() { ... button = new JButton("Click Me"); ... button.addActionListener( new ActionListener(){ // register a listener object to the button’s click event // This anonymous inner class is a concrete implementation of an abstract ActionListener component. public void actionPerformed(ActionEvent e) { // concrete code to handle the button's click event Toolkit.getDefaultToolkit().beep(); } }; } … }
The JFC/Swing framework also provides utilities to initialize the GUI application and schedule the application for execution on the event dispatch thread. But we will not get into that here.
Interestingly, the JFC/Swing framework displays both white-box and black-box framework characteristics:
JPanel
and
JButton
classes, are part of a white-box framework where the
invariant management of the GUI components, including such services and
behaviors as screen painting, event management, look-and-feel, etc., are
handled by the components' superclasses. The framework is invoked from
the components through inherited, superclass-implemented method calls.
The framework invokes the components by calling abstract methods that are
overridden by the concrete GUI component classes. The white-box
framework enables the JFC/Swing system to more easily maintain the tight
coupling required to perform the complex display and management of the
graphical user interface. ActionListener
implementations which become part of a
compositional relationship with the framework through the registration
process, e.g. the addActionListener
method. This
black-box framework enables the JFC/Swing system to decouple the GUI from
the application-specific "business logic" that is typically in the event
handlers. Implementation note: The source material for this example from
Oracle (linked above) uses a different method for implementing the
ActionListener
where the Beeper
class itself is the
listener object. This is a very common technique and the discussion
on its advantages, disadvantages and design consequences relative to the above
implementation is unfortunately well beyond the scope of this article but well
worth the effort to explore.
[top]
[top]
(Thanks to Max Payton and Taixu Chen for their help in creating the diagram and finding the references for this page! And thanks to Dr. Dung "Zung" Nguyen for the JFC/Swing example!)
© 2017 by Stephen Wong