11.2 What Is a Good Architecture?
So, a good architecture is important. But what constitutes a good architecture? Of course, a good architecture meets systemwide concerns such as performance and reliability. It must be understandable so that you can easily trace which part of the architecture realizes which requirement or use case. Each class—and consequently the packages it resides in—plays clearly defined roles and performs a set of responsibilities that fulfill those roles and nothing else. There is little or no duplication of responsibilities between classes.
A good architecture keeps concerns separate, which means that changes in one part of the system do not propagate to other parts of the system. Even if they do, you can clearly identify what changes are to be made. If there is a need to extend the architecture, the impact should be minimal. Everything that already works should continue to work. For a system that applies aspect orientation, the different concerns about a system can be kept separated effectively.
Separating Functional Requirements
In general, you want to keep functional requirements, whether expressed as features, use cases, or in other terms, separate from each other. After all, they address different end-user concerns and will evolve separately. You do not want changes in one to impact the other. The functional requirements are often expressed on top of the problem domain (e.g., hotel management, logistics, banking, insurance, etc.). You naturally want to keep what is specific to the functionality of the system separate from the domain. In this way, you can easily adapt a system to a similar domain. In addition, some functional requirements are defined as extensions of other functional requirements: you must keep these separate from each other as well.
Separating Nonfunctional from Functional Requirements
Nonfunctional requirements usually specify the desired quality attributes of the system: security, performance, reliability, and so forth. These are provided by some infrastructure mechanisms—for example, you need some authorization, authentication, and encryption mechanisms to achieve security; you need caching and load-balancing to achieve performance. Frequently, these infrastructure mechanisms require small bits of behavior that must be executed within many classes. This means that a change in the realization of an infrastructure mechanism often implies huge repercussions, so you want to keep these separate.
Separating Platform Specifics
Today's systems need to execute on top of many technologies. Even for a single infrastructure mechanism such as authorization, you still have many technologies (e.g., through HTTP cookies, session identifiers, etc.) to choose from. These technologies are often platform- and vendor-specific. When a vendor upgrades its technologies to a new and better version, it is not easy to upgrade your system accordingly if your implementation has been tightly coupled with the previous version of that technology. You most certainly do not want to be tied down to a particular technology. Thus, you need to keep platform specifics separate.
Separating Tests from the Units Being Tested
As part of implementing a test, you must perform some control and instrumentation (e.g., debugging, tracing, logging, etc.). Control is for the purpose of forcing the execution flow of the system to follow some test sequences. Instrumentation is for the purpose of extracting information to verify that the system does indeed follow the desired test sequence. Control and instrumentation usually require some behavior that must execute within the context of the system under test. Such control and instrumentation behavior have to be removed after the test is conducted. Thus, you want to keep the implementation of tests separate from the system under test.