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:
-
A component is an unit of independent deployment. This implicates that
components must encapsulate all its constituent features.
-
A component ia a unit of third party composition. Therefore components
must be self-contained and be provided with good specifications of what
it requires at provides.
-
A component has no persistent state: in any given process, there
will be at most one copy of a particular component.
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?
-
Safety can be expressed using invariants
-
Progress conditions: the concept that something garantueed by a precondition
will lead to something guarantueed by a postcondition can be generalized.
-
Non-functional requirements, e.g. resource consuption, performance
-
Time and space requirements: the specification of time and space complexity
bounds in a contract could significantly add to the practical value of
that contract. To fill the gap with platform-specific bounds, a contract
should come with additional information required to determine absolute
bounds.
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:
-
The problem is that such a restriction is of transitive nature. A callback
may invoke any other operation that internally, for whatever reason, uses
the service of the component that orignally notified. As a consequence,
checking the restriction is a very dynamic and global process.
-
Asynchronously observable state, such as state observable by means of callbacks,
allows contracts based on pre- and postconditions to fit less well:
Asynchrony is more naturally dealt with by approaches developed to handle
concurrency, like process models Unfortunately the resulting contracts
are far less manageable.
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.
-
established by client => guaranteed by precondition => required by provider
-
estbalished by provider => guaranteed by postcondition => expected by client
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.
-
Mission-critical systems: some systems must provide 24h , 7 days
per week full-service. Extending such systems with adding/replacing new
components must be done dynamically, without stopping the system.
-
Source code of third partie components is not available
-
A lot of research needs to be done in this area...
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:
-
preconditions are specified by the types of the input parameters.
-
postcondtion are specified by the types of the output parameters and return
values.
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:
-
guaranteed by precondition (input parameter types) => required by provider
(supertypes): contravariance
-
guarantueed by postcondition (output parameter types, return types) <=
established by provider (subtypes): covariance
VIII. Types, interfaces and components
Some observations:
-
In component systems, a self-standing interface has to be fully and explicitly
typed to benefit from typechecking.
-
Structural subtyping leads to partial contracts. A named type refers
to the full contract. Components must have a name.
-
Components may support multiple interfaces: it is sometimes necessary to
specifiy a required set of interfaces and then accept any component that
provides at least these required interfaces. Such interface sets are sometimes
called categories. I think names are as important as in the previous
point.
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:
-
implementation inheritance (the 'how to avoid inheritance' promise refers
solely implementation inheritance.)
-
subtyping or interface inheritance
-
the promise of substitutability
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:
-
syntactic fragile base class problem: is about binary compatibility
of compiled classes with new binary releases of superclasses. The idea
is that a class should not need recompilation, just because purely syntactic
changes to its superclasses'interfaces have occured. For example, methods
may move up in the class hierarchy. However as long as they reamin on the
inheritance path, a subclass should not care. By initializing method dispatch
tables at load time, the above example syntactic FBC can be solved.
-
semantic fragile base class problem: How can a subclass remain valid
in the presence of different versions and evolution of the implementation
of its superclasses. The problem is very similar as the problem of callbacks
and re-entrance but now between base - and subclasses. Several approaches
to disciplined inheritance are described in the literature: specialization
interfaces, reuse contracts
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:
-
Units of abstraction
-
Units of accounting
-
Units of analysis
-
Units of compilation
-
Units of delivery
-
Units of dispute
-
Units of extension
-
Units of fault containment
-
Units of instantiation
-
Units of loading
-
Units of locality
-
Units of maintenance
-
Units of system management
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:
-
Sharing consistency: programming languages
-
Sharing concrete solutions fragments: libraries
-
Sharing individual contracts: interfaces
-
Sharing individual interaction architectures: patterns (see Coplien and
Schmidt, 1995; Vlissides et al., 1996)
-
Sharing architectures: frameworks
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:
-
How does a client learn about other interfaces? by calling the QueryInterface
operation with an GUID interface identifier (IID), uniquely identifying
the requested interface as an argument
-
How does a client compare the identity of COM objects? By a special interface,
IUnknown
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
-
Containment: inner - outer object relation
-
Aggregation: an inner object's interface is handed out directly to the
inner object, saving the cost of forwarding.
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?