Component Software

Beyond Object-Oriented Programming

Clemens Szyperski

Resumed by Et.

A. FOUNDATIONS

Components

I. What a component is and is not

In contrast with the notions of instantiation, identity and encapsulation of state and behaviour, which lead to the notion of object, the characteristic properties of components are: Obviously, a component is likely to come to life through objects and therefore would normally consist of one or more classes or immutable prototype objects. However, there is no need for a component to contain classes only, or even to contain classes at all. Instead a component could be realized by traditional procedures or functional programming.
The superclasses of a class do not necessarily need to reside in the same component. The inheritance relation between them crosses component boundaries. Is this a 'good' thing or not?
 

A Definition: software component

" A software component is an unit of composition with contractually specified interfaces and explicit context dependencies only. An software component can be deployed independently and is subject to composition by third parties."

Specification of interfaces should be market-oriented...Standardization!
Context dependencies are component world (CORBA, DCOM, Sun's Java) specific.
 

Components, interfaces, and re-entrance

II. Components and interfaces

The problem with object interfaces

One distinguishes between direct interfaces (procedural interfaces or static interfaces) and indirect interfaces (object interfaces, made available by the component). The problem with the latter one is that the dynamic binding concept, central in OOP., makes the resulting configuration of a component system harder to control. Dynamic binding can lead to the involvement of a third party, of which both client of the component and the interface-introducing component may be unaware.
In a versioned systems especially (evolving interfaces, but also different flavors of implementation),  care must be taken to avoid coupling of parties that are of incompatible versions.  Unlike for direct systems like procedural libraries, indirect coupling implies that version checks must occur in every place where an object reference crosses component boundaries.  Exisitng solutions for this problem are is that each interface, once published is frozen and never changed again. Too limited, if you ask me.
This versioning problem is of non-functional kind.  A meta-architecture approach , an AOP or a compositon filter approach can solve this problem more profoundly.. Reflective component-based systems are the future!
 

The contract of an interface

A contract of an interface states what the client needs to do to use the interface, and what the provider has to implement to meet the services promised by the interface.
For example, on the level of the individual operation of an interface, the two sides of contract can be captured by specifying invariants and pre- and postcondition for the operation.
Contracts are used to control the evolution of software implementation: Revised implementations must respect the contract.
The contractual specification not only consists of functional aspects  but may also specify non-functional requirements, concerning the implementation strategy. E.g., a meta-program may monitor and control the so called service-level of a component.
Contracts must be able to be specified in a disciplined manner, without sacrificing the flexibility. To minimize risks, a contract needs to maintain a balance between being precise and not being too restrictive.
Contracts must be platform-independent, for example execution time constraints must be expressed in terms of time complexity instead of seconds.

What belongs to a contract?

III. Callbacks and contracts

Callbacks  lead to exposing intermediate state of the component to clients, while executing an encapsulating operation on behalve of some client. It's important that this observable state remains valid as long as any callbacks are active.
If not care taken, callbacks may easily lead to broken contracts. As a callback's role is all about state and state change, most non-empty callbacks observe or change more component state. If no appropriate action taken, broken contracts occur, when the update of intermediate state resulting from executing the callback may leave the state resulting from the execution of the encapsulating operation invalid, i.e. in conflict with its post-condition. An other related problem is so called indirect recursion across abstractions caused by callbacks,
What is needed is a contract between component and callback implementations. What needs to be captured is the condition when a notified observer is (not) allowed to change the notifying observed object (i.e. calling state-updating operations).  It is however difficult to capture such a restriction using a strictly local contract based on pre- and postcondtions: An elegant middle ground is using callbacks with state test functions. These tell wether or not a particular operation is currently available, without revealing too much of the library's internal state. (Cfr the Correlate abstract state machine approach for imposing synchronisation constraints). A prominent example of test functions is the getInCheck() of the Java Security Manager.

IV. From callbacks to webs of interacting objects

Inter-object consistency

One of the main prowerful aspects but also problems of object orientation, is that object references introduce linkage across arbitrary abstraction domains. This means that every method invocation is potentially an up-call, every method pontentially a callback!
The flow of control against the layers of abstraction clearly may expose inconsistensies to arbitrary other objects. The real problem is observation of an object undergoing a state transition, with inconsistent intermediate states becoming visible. As objects encapsulate state, such observation is limited to what the object reveals. In other words, inconsistencies can only be observed by entering an object's method.

Object re-entrance

The situation is most intricate when considering object re-entrance. If, while in progress and before reaching consistency again, the intermediate state is observed - by means of re-entrance - maintaining correctness becomes difficault
It's difficult to maintain invariants in the face of self-recursion or cyclic-dependencies. One way to address these re-entrance problems is to weaken invariants conditionally and make the conditions available to clients through test functions.
A significant number of design and implementation errors -often hard to find and correct - go back to unexpected recursive re-entrance of objects.
 

Re-entrance in component systems

Recursion and re-entrance become an even more pressing problem when crossing the boundaries of components. Problems due to unexpected object re-entrance may be only solved when stepping back and inspecting the overall situation. With components, this can be impossible to do as a component system, does not have a final form. This demands that each component is independently verifiable based on the contractual specification of the interfaces it requires and those it provides.
 

Polymorphism

V. Substitutability

Self-standing contractually specified interfaces decouple client and providers.
It's legal to substitute a client or provider  for another if the contracts with the cooperating entities remain unbroken.
However, a client may establish more than is required by the precondition or expect less than is guaranteed by the postcondition. Likewise a provider may require less than is guaranteed by the precondition or establish more that is required by the postcondition.

VI. Subtyping

It would be higly desirable to have a compiler or other autmatic tool check clients and providers against the contract and reject incorrect ones. Szyperski states here that compile-time checking better is than load-time checking, and load-time checking better than runtime-checking. This is however not true for component-systems, because it's impossible to do so.

VII. Type checking

The most prominent example of compile time checks is type checking, fully eliminating memory errors.
In programming languages each interface has a certain type. A type can be seen as a simplified contractt: Interfaces which extend a certain base interface are said to be a subtype of the base interface's type.

An object implementing one type (i.e. provider) can be used in a context expecting another type, when it's a subtype and:

VIII. Types, interfaces and components

Some observations:

IX. The paradigm of independent extensibility

The principal function of component orientations is to support independent extensibility. A system is independently extensible if it is extensible and if independently developed extensions can be combined.
However isolation comes at a price, and frequent crossing of protection domain boundaries can severly affect performance, safety and robustness.  One way to solve this problem is to choose carefully the granularity of components - if most interactions stay within a component boundaries, the cost incurred when crossing component boundaries may be tolerable.
In the end, the design of componenent systems must strive between a balance between flexibility on the one hand and performance, safety, robustness on the other.

X. Safety by construction: viability of components

Besides hardware protection and compile-time type checking, additional measures are needed:

Module safety

A component has to specify explicitly which services it needs to access. Example: the import Java statement.
It should be impossible for a component to retrieve references to other components which it has not been granted access to.

Meta-programming

For component systems, where meta-interfaces exist, these need to be explicitly restricted  such that these services do not break module safety. Indeed a system may offer two metaprogramming interfaces: one that is module safe and open for general use and another that is module unsafe and restricted to trusted components, i.e.  the core of the system. Example is the BlackBox Component Framework (used with Component Pascal).

Multi-language environment

 

XI. Component frameworks

Dimensions of independent extensibility

Independently extensible systems require a clear statement of what can be extended (forcing specialization would endanger the interoperability of independent extensions). Each particular feature of an independently extensible system that is open for seperate extension is called a dimension of (independent) extensibility.
The theoretical ideal would be to form orthogonal dimensions of independent extensibility that together form an extension space that is complete with respect to extensibility requirements.

Bottleneck interfaces

A component framework must provide an infrastructure which lays an common ground for independent extensions to interoperate. Interfaces introduced to allow the interoperation between independently extended abstractions are sometimes called bottleneck interfaces.

Object versus class composition or how to avoid inheritance

There are three cardinal facets of inheritance: How to get classes right (correct) and robust (tolerate evolution and versioning) led to the formultation of the fragile base class problem

XII. The fragile base class problem

Two kinds:

XIII. From class to object composition

The idea is instead of relying on a callback to the superclass, forwarding a message to an inner object. An outer object does not reimplement the functionality of the inner object when it forwrds messages. Hence it reuses the implementation of the inner object. If the implementation of the inner object is changed, then this change will 'spread' to the outer object.  In this way the same advantages as implementation inheritance are achieved, but without the risk of re-entrant message sequence explodes. An other advantage is that dynamic composition is enabled with object composititon.
The difference with implementation inheritance is called the 'possession of a common self'. There is no common self to a composition of objects. This means however that when recursion across multiple objects is achieved, it  needs to be designed in, whereas in the case of implemenatation inheritance it can be patched in. This is however sometimes called planned versus unplanned reuse.
 

XIV. Forwarding versus delegation

Delegation = making object compositon as problematic as implementation inheritance.
A recent focus of research has been the disciplined use of delegation. As delegation can be used to form a common self across webs of objects, one could term such webs themselves as objects of a higher order. such 'objects' are often called split or fragmented objects.

Aspects of scale and granularity

This relates to the problems posed at IX. Various criteria are:

Reuse: Patterns, frameworks, architectures

Independenty extensibility clearly relates to effective design reuse.  In traditional integrated software design, design experience is probably the single most value in the basket of reuse ideas. Great designers are needed. However, the architecture of component-based systems is significantly more demanding than that of traditional monolithic integrated solutions. In the context of component software, full comprehension of established design reuse techniques is most important.
Design reuse can be understood as the attempt to share certain aspects of an approach across various projects. The following list names some of the established reuse techniques and for which sharing level they are beste needed:

 

B. STATE OF THE ART

The various approaches differ in to what an interface connects to. Those based on traditional object models define a one-one relation between interfaces and objects. (CORBA). Other approaches associate many interfaces with a single object(Java) or many interfaces with many objects in a componeny object (COM).

The OMG way: CORBA and OMA

The Microsoft way: DCOM, OLE and ActiveX

A COM component may for example provide three different interfaces and may use tow different objects to implement these. What is important, is that there is no single object identity that ever leaves the component and represents the entire COM object.
Two important questions are: Reference counting is done for the COM object in its entirety or separately for each of its interface nodes.

I. COM object reuse

No implementation inheritance. Support for reuse is provided by

II. Interfaces and polymorphism

The true nature of polymorpism has nothing to do with interface inheritance, but is the support of sets of interfaces by COM objects. The type of a COM object is the set of interface identifiers of the interfaces it supports. A subtype is a superset of interfaces.
COM defines categories to represent sets of interface indentifiers. Categories function as contracts. A caegory specifies not only wghich interfaces must at least be supported, but also which methods in these interfaces must at least be implemented. This sucks the idea of contracts.

Problem: who maintains the list of categories?