COMP 310

Component-Framework Systems

    Current Home  Java Resources  Eclipse Resources

Quick links to the content below:

Introduction to Component-Framework Systems

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 abstraction process involves several steps:

  1. Eliminate all unnecessary details and get to the essential elements of the problem and express them clearly and concisely as invariants.
  2. Encapsulate that which vary and express them as variants.  The variants of abstractly equivalent behaviors are grouped into appropriate taxonomy of abstract classes and subclasses.
  3. Define the dynamics of the system in terms of the  interplay between the invariants and the variants.  Sometime the invariants/variants interplays can be straightforward, but more than often they can be subtle like the case when an invariant can be expressed in terms of many variants.

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:

 

Component-Frameworks

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]

Black-box vs. White-box Frameworks

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 and Black-box frameworks

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]

Frameworks vs. Libraries

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:

Library-based System

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.

Library-based system, refactored

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]

Designing Component-Framework Systems

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]

Frameworks as Entire Applications

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]

Component-driven Framework Customizations

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.

On-Demand Components

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]

Java JFC/Swing UI Framework Example

(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:

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:  

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]

References:

 

[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