Creating, maturing, evolving, and sustaining an architecture are guided by the design choices made by architects and developers. All choices are not equal. Some are downright trivial and have no bearing on the architecture; others are quite profound. All software developers, and architects in particular, must hone their ability to recognize better design alternatives. This means, of course, that we must have criteria for evaluating design alternatives and that we must apply them as best we can. The following principles for good architectural design have stood the test of time.
The architecture is organized around separate and relatively independent pieces that hide internal implementation details from each other. A good example is a telephone billing system. One part of the system, such as the switch, is responsible for creating, managing, and tracking phone calls. Ultimately, detailed transaction data are created for every phone call (who called whom, for how long, and so forth). These data are fed into separate billing systems, which manage the complexities of calculating a precise fee based on the contract between the telephone company and the customer.
Within each of these broadly encapsulated systems are other encapsulations that make sense. For example, the billing system is likely to have separate modules for calculating the basic bill, calculating any discounts , calculating appropriate taxes, and so forth.
The ways that subsystems within a larger design interact are clearly defined. Ideally, these interactions are specified in such a way that they can remain relatively stable over the life of the system. One way to accomplish this is through abstractions over the concrete implementation. Programming to the abstraction allows greater variability as implementation needs change.
Consider a program originally designed to write output to a file. Instead of programming directly to the interface provided by a file, program an output mechanism to the abstract interface, which in many languages is referred to as an output stream. This allows the program to direct output to a file stream, the standard output stream, an in-memory string stream, or any other target consistent with this interface, even, and especially , those not yet envisioned by the original developer. Of course, what is most important is defining the initial interface, something that can almost only be done well through experience.
Another area in which the principle of interfaces influences system design is the careful isolation of aspects of the system that are likely to experience the greatest amount of change behind stable interfaces. ODBC and related APIs provide an example of this principle in action. By programming to ODBC, a developer insulates the system from a common dimension of change: the specific selection of a database. Using ODBC a developer can switch relatively easily from one SQL-based database to another. Bear in mind that this flexibility comes with its own special costyou give up the " goodies " that are present in vendor-specific APIs.
Coupling refers to the degree of interconnectedness among different pieces in a system. In general, loosely coupled pieces are easier to understand, test, reuse, and maintain, because they can be isolated from other pieces of the system. Loose coupling also promotes parallelism in the implementation schedule. Note that application of the first two principles aides loose coupling.
One of the key challenges associated with loose coupling concerns component granularity. By granularity I mean the level of work performed by a component. Loosely coupled components may be easy to understand, test, reuse, and maintain in isolation, but when they are created with too fine of a granularity, creating solutions using them can be harder because you have to stitch together so many to accomplish a meaningful piece of work. Appropriate granularity is determined by the task(s) associated with the component.
Cohesion describes how closely related the activities within a single piece (component) or among a group of pieces are. A highly cohesive component means that its elements strongly relate to each other. We give the highest marks for cohesion to components whose elements contribute to only one task.
Components can be encapsulated, but this does not mean that they perform their work without some kind of parameterization or instrumentation. The most effective components perform an appropriate amount of work with the right number and kind of parameters that enable their user to adjust their operation. A sophisticated form of parameterization, referred to as "inversion of control," occurs within frameworks and plug-in architectures. This is where one component hands over processing control to another.
Many times the development team is faced with a tough decision that cannot be made with certainty . Sometimes this is because of technical reasons, as when the team is trying to choose a library to perform a specific function and are not certain which vendor can provide the best performance. Sometimes it is because of business reasons, as when the product management team is negotiating with two or more technology providers for the best possible terms.
By deferring these decisions as long as possible, the overall development team gives themselves the best chance to make a good choice. While you can't defer a decision forever, you can quarantine its effects by using the principles of good architectural design.