1.4 Fleshing out the definitions

“One thing can be stated with certainty: components are for composition.” (Szyperski et al. 2002:3). By composition, Szyperski et al. refer to the process of assembling a system using a selection of compatible components. This is only possible if components have a shared understanding of the outside world, what it offers, and their responsibilities toward it. Such a set of standards is known as a component model, of which there are many incompatible models.1 Implementations of component models—that is, the accompanying runtime software and tools—are sometimes known as “component frameworks.” Without a component model implementation, components cannot be used.

Components have responsibilities to their environment, but also to other components and to the clients that make use of their services. A component must advertise its services somehow, and must let its clients know what it expects of them. This information is communicated using interfaces that may be realized by an arbitrary number of implementations that agree to abide by their rules, and should be seen as binding contracts. Classes, which are contained in components, are the implementation entities that realize interfaces by implementing them. An interface is a freestanding, independent entity that is neither part of the classes implementing it, nor part of the code that accesses functionality through it.

Incoming interfaces communicate what components are capable of, while outgoing interfaces communicate what is expected of clients that make use of their services. As components are encapsulated entities exposing no implementation details, interfaces are the sole means of accessing them. They consist of an arbitrary number of operations that for incoming interfaces correspond to actions that the component may undertake on behalf of a client. Operations in outgoing interfaces are called by components, typically to notify clients of events that occur. An operation is similar to a method in an object-oriented language, but additional constraints are often imposed by component models, related to the types used for operation arguments as well as error handling. Incoming and outgoing interfaces are also known as provided and required interfaces, respectively. They form the basis for connection-oriented programming, enabling components to be glued together.

When a statically linked procedural library is used, the implementation is known at compile-time (a priori). Interfaces to such libraries are known as direct interfaces, as the implementation is known beforehand. Object interfaces are indirect, in the sense that the actual implementation is only known at runtime. This is also known as dynamic dispatch or late binding (explored in detail in Chapter 3). Dynamic dispatch enables classes to have multiple personalities by implementing multiple interfaces—this is known as polymorphism in object-oriented programming.

It is hard to overemphasize the role of interfaces—they make it possible to program to a specification, and not to a concrete implementation. Interfaces play the same role as the standards that allow electrical outlets and the plugs they accept to work together—whether a vacuum cleaner or a power tool is connected to an electrical outlet is immaterial, as long as it can draw current in a standardized way.2 Two components that implement the same interfaces and exhibit the same runtime behavior are substitutable for one another (known as the principle of substitutability) (Eriksson et al. 2004). This is often cited as an important business case for component technology—enabling a company to replace a component with a less expensive one from a competing vendor, or replacing a component from a vendor that has gone out of business (Tracz 2001). (In practice, there are monumental challenges to overcome to make the behavior of all but the simplest components compatible to the degree required to make one component completely substitutable for another.)

A component is a self-standing, independent unit that may be deployed separately from the clients that use it. It has no observable state. The source code of a component may or may not be available; it may be shipped only in binary form. If a component is dependent on any entities other than the environment it is written for, including the component model, such dependencies should be explicitly noted in machine-readable form (Heineman and Councill 2001b). Components should be able to indicate their version, making it possible for a component model implementation to simultaneously load different versions of the same component. (In large systems with complex dependencies, it is common for components to indirectly depend on different versions of the same component.)

It must be possible to differentiate between different components, classes and interfaces, at compile-time and at runtime. For this reason, these entities are assigned names, one that is used at compile-time and one that is used at runtime. The compile-time name is for the benefit of human developers, and should thus be human-readable, but need not be unique. The name used at runtime, however, must have a very high probability of being globally unique. The runtime name assigned to a component or interface must be such that it is very probable that no such name has been created in the past, and that no such name will be created in the future. Component models use a variety of strategies to try to ensure that this property holds. Services are often provided that activate components and instantiate classes given their runtime names.

Component models standardize a host of other facets. These include error handling, memory management, the deployment file format for components and metadata (such as type information available at runtime).

In addition, component technology is also associated with a number of more complex concepts:

  • Programming language agnosticism. Component models dictate standards that make it possible to access components from a variety of programming languages, and also write components using different languages. Interacting with a component written in a different programming language should ideally feel as natural as interacting with code written in the native language.

  • Location-transparent invocations. Component technology may be used to enable a client to remain oblivious to whether a component is executing in the client’s operating system process, in another process on the same computer, or on a different computer altogether, perhaps halfway across the world. Component model implementations can make all components appear as though they are executing in the client’s process, regardless of whether this is actually the case. In an enterprise setting, this is one of the selling points of component technology, as it makes it possible for services running on different servers and on different platforms to interoperate (which is often known as distributed computing).

  • Declarative services. Some component models support associating components and objects with declarative attributes that help programmers manage such disparate things as concurrency, database transactions and security boundaries (Szyperski et al. 2002:430). Declarative attributes save the programmer from having to write error-prone code, serve as machine-readable documentation and make it possible to enforce architectural rules.

Footnotes

  1. There are often bridging solutions that help components written for one component model communicate with those written for an otherwise incompatible model (Weinreich and Sametinger 2001).
  2. A well-worn joke is that we love standards, that is why we have so many of them. Obviously, if there is one “standard” per implementation, nothing has been gained in the way of interoperability, and the large number of standards for electrical outlets helps to illustrate this.