Tuesday, July 27, 2010

Decentralized Components

Software components are often central to other software components. A component is considered central to another component when the fabric of the larger component requires it. If you were to follow the chain of dependencies within a set of components that constitute a software system, you're bound to find one or more centralized components. Centralization in software systems is necessary to a certain extent because something needs to initialize the system. There are, however, areas where decentralized components are beneficial because the chain of command is better distributed, which leads to lower coupling. A component can be anything from a sub-system to a package. In object-oriented design, classes are often viewed as components, depending on your viewpoint. There are many ways that components become centralized in a system and many ways to decentralize them to some degree. A component can be centralized in a hierarchy as well as by composition. Low coupling, as a design principle, can lead to decentralization, but there are other factors that need to be taken into account such as the strength of relationships between components. A simple example sub-system will help to illustrate some of the issues with centralized components and how decentralization can improve the design.

Our sample system will be a printing sub-system. The system requires the ability to differentiate between, and print to both colour ink printers, and black ink printers. The most centralized component of the system is a printer abstraction. We want to design specialized abstractions for the two types of supported printers as well. For the purposes of this example system, all components are classes. A real printing sub-system's requirements go way beyond this example but we're not interested in anything other than the very high-level concepts of the system.

Compositional centralization in software is a whole-part relationship between two or more components. The parts are considered central to the whole because they don't serve any purpose outside of it. Looking at our example printing sub-system, Printer components have one or more colours associated with them. Our initial design shows Printer objects as composites and Colour components as centralized parts of the Printer component. This makes good design sense because colours represent a facet of printing capabilities and are strongly related to Printer components.

Now lets take a step back and look at this design decision at a higher level. Printer components have colours out of necessity. You can't print something that doesn't fall within the visible colour spectrum. Now that the relationship between Printer and Colour components has been firmly established, lets think about the strength of that relationship. Colour components are central to Printer components when they are currently printing something. That is, when they are in a printing state. In any other state, Printer components don't necessarily need Colour components. This means that Colour components are more closely related to the state of the Printer than the Printer itself.

The fact that Colour components are pertinent parts of Printer components only during a specific Printer state has notable design implications. We earlier determined that Printer and Colour components are closely related; colours are central to printers. Objects composed of other objects often directly instantiate their own parts because the whole has the necessary initialization information. We can, however, reduce direct coupling by using a factory to create Colour components when they are needed.

Suppose we have a ColourFactory component with static methods for constructing Colour component instances. This factory can be used by Printer components to create colours.
This is an indirect way to reduce coupling. The ColourFactory component creates colours upon request by the Printer component. Additionally, Colour components become decentralized to Printer components while maintaining a strong conceptual relationship. By introducing a colour factory into the design, the responsibility of creating colours has shifted away from Printer components. When a Printer component wants to create a Colour component, it needs to tell the factory which colour to create. It does this by providing the factory with enough information to construct the component. This could be as simple as specifying a string such as "red". The conceptual responsibilities of colour creation remain with the Printer component while the implementation responsibilities of colour creation are with the ColourFactory component.

Lets go back to the requirements of our printing sub-system for a moment. The system needs to support both colour printers as well as black printers. In our sub-system, the Printer component is abstract. There are two concrete implementations of the Printer component: ColourPrinter and BlackPrinter. The difference between the two printer types are the supported colours returned by the colours() method. The abstract Printer component provides the centralized printing behavior. This shared behavior should be low-level in nature, functionality that is expensive to replicate sub-classes. The key point here being that certain characteristics of the printer concept remain central to the Printer component. There is some behavior that is common to all printer components and cannot be decentralized, hence the generalization hierarchy.

Our two specialized printing components, ColourPrinter and BlackPrinter, have different implementations of the colours() method that will return the supported colours of the printer. The Printer component defines an abstract colours() method with the intention of being implemented by a specialized component. The ColourPrinter and BlackPrinter components each provide their own specialized implementations of the method. The colours() method is decentralized from the abstract Printer component by polymorphism. The common, concrete functionality offered by the Printer component uses the colours() method. Our system uses decentralized behavior to realize the centralized behavior found in our abstract Printer component.

Decentralization in component design can lower coupling between components which, in my opinion, should be sought after as a design goal. It does so by replacing persistent relationships with transient relationships where it makes sense to do so. Conceptually, the parts of a whole component may only exist during a specific time interval or state. Using factories to create these parts and use them when needed can decentralize them from their whole. In generalization hierarchies, specific functionality can be decentralized from a component by providing a specialized implementation of it in a child component. Some system facets are better off in a centralized form as they are shared among the various decentralized components.