28. Principles of Package and Component Design

image

© Jennifer M. Kohnke

Nice package.

—Anthony

As software applications grow in size and complexity, they require some kind of highlevel organization. Classes are convenient unit for organizing small applications but are too finely grained to be used as the sole organizational unit for large applications. Something “larger” than a class is needed to help organize large applications. That something is called a package, or a component.

Packages and Components

The term package has been overloaded with many meanings in software. For our purposes, we focus on one particular kind of package, often called a component. A component is an independently deployable binary unit. In .NET, components are often called assemblies and are carried within DLLs.

As vitally important elements of large software systems, components allow such systems to be decomposed into smaller binary deliverables. If the dependencies between the components are well managed, it is possible to fix bugs and add features by redeploying only those components that have changed. More important, the design of large systems depends critically on good component design, so that individual teams can focus on isolated components instead of worrying about the whole system.

In UML, packages can be used as containers for groups of classes. These packages can represent subsystems, libraries, or components. By grouping classes into packages, we can reason about the design at a higher level of abstraction. If those packages are components, we can use them to manage the development and distribution of the software. Our goal in this chapter is to learn how to partition the classes in an application according to some criteria and then allocate the classes in those partitions to independently deployable components.

But classes often have dependencies on other classes, and these dependencies often cross component boundaries. Thus, the components will have dependency relationships with each other. The relationships between components express the high-level organization of the application and need to be managed.

This begs a large number of questions.

1. What are the principles for allocating classes to components?

2. What design principles govern the relationships between components?

3. Should components be designed before classes (top down)? Or should classes be designed before components (bottom up)?

4. How are components physically represented? In C#? In the development environment?

5. Once created, to what purpose will we put these components?

This chapter outlines six principles for managing the contents and relationships between components. The first three, principles of package cohesion, help us allocate classes to packages. The last three principles govern package coupling and help us determine how packages should be interrelated. The last two principles also describe a set of dependency management metrics that allow developers to measure and characterize the dependency structure of their designs.

Principles of Component Cohesion: Granularity

The principles of component cohesion help developers decide how to partition classes into components. These principles depend on the fact that at least some of the classes and their interrelationships have been discovered. Thus, these principles take a bottom-up view of partitioning.

The Reuse/Release Equivalence Principle (REP)


The granule of reuse is the granule of release.


What do you expect from the author of a class library that you are planning to reuse? Certainly, you want good documentation, working code, well-specified interfaces, and so on. But there are other things you want, too.

First, to make it worth your while to reuse this person’s code, you want the author to guarantee to maintain it for you. After all, if you have to maintain it, you are going to have to invest a tremendous amount of time into it, time that might be better spent designing a smaller and better component for yourself.

Second, you are going to want the author to notify you of any changes planned to the interface and functionality of the code. But notification is not enough. The author must give you the choice to refuse to use any new versions. After all, the author might introduce a new version just as you are entering a severe schedule crunch or might make changes to the code that are simply incompatible with your system.

In either case, should you decide to reject that version, the author must guarantee to support your use of the old version for a time. Perhaps that time is as short as 3 months or as long as a year; that is something for the two of you to negotiate. But the author can’t simply cut you loose and refuse to support you. If the author won’t agree to support your use of older versions, you may have to seriously consider whether you want to use that code and be subject to the author’s capricious changes.

This issue is primarily political. It has to do with the clerical and support effort that must be provided if other people are going to reuse code. But those political and clerical issues have a profound effect on the packaging structure of software. In order to provide the guarantees that reusers need, authors organize their software into reusable components and then track those components with release numbers.

Thus, REP states that the granule of reuse, a component, can be no smaller than the granule of release. Anything that we reuse must also be released and tracked. It is not realistic for a developer to simply write a class and then claim that it is reusable. Reusability comes only after a tracking system is in place and offers the guarantees of notification, safety, and support that the potential reusers will need.

REP gives us our first hint at how to partition our design into components. Since reusability must be based on components, reusable components must contain reusable classes. So, at least some components should comprise reusable sets of classes.

It may seem disquieting that a political force would affect the partitioning of our software, but software is not a mathematically pure entity that can be structured according to mathematically pure rules. Software is a human product that supports human endeavors. Software is created by humans and is used by humans. And if software is going to be reused, it must be partitioned in a manner that humans find convenient for that purpose.

What does this tell us about the internal structure of a component? One must consider the internal contents from the point of view of potential reusers. If a component contains software that should be reused, it should not also contain software that is not designed for reuse. Either all the classes in a component are reusable, or none of them are.

Further, it’s not simply reusability that is the criterion; we must also consider who the reuser is. Certainly, a container class library is reusable, and so is a financial framework. But we would not want them to be part of the same component, for many people who would like to reuse a container class library would have no interest in a financial framework. Thus, we want all the classes in a component to be reusable by the same audience. We do not want an audience to find that a component consists of some classes that are needed, and others that are wholly inappropriate.

The Common Reuse Principle (CRP)


The classes in a component are reused together. If you reuse one of the classes in a component, you reuse them all.


This principle helps us to decide which classes should be placed into a component. CRP states that classes that tend to be reused together belong in the same component.

Classes are seldom reused in isolation. Generally, reusable classes collaborate with other classes that are part of the reusable abstraction. CRP states that these classes belong together in the same component. In such a component, we would expect to see classes that have lots of dependencies on each other. A simple example might be a container class and its associated iterators. These classes are reused together because they are tightly coupled. Thus, they ought to be in the same component.

But CRP tells us more than simply what classes to put together into a component. It also tells us what classes not to put in the component. When one component uses another, a dependency is created between the components. It may be that the using component uses only one class within the used component. However, that doesn’t weaken the dependency. The using component still depends on the used component. Every time the used component is released, the using component must be revalidated and rereleased. This is true even if the used component is being released because of changes to a class that the using component doesn’t care about.

Moreover, it is common for components to live in DLLs. If the used component is released as a DLL, the using code depends on the entire DLL. Any modification to that DLL, even if that modification is to a class that the using code does not care about, will still cause a new version of the DLL to be released. The new DLL will still have to be redeployed, and the using code will still have to be revalidated.

Thus, I want to make sure that when I depend on a component, I depend on every class in that component. To say this another way, I want to make sure that the classes that I put into a component are inseparable, that it is impossible to depend on some and not the others. Otherwise, I will be revalidating and redeploying more than is necessary and will be wasting significant effort.

Therefore, CRP tells us more about what classes shouldn’t be together than what classes should be together. CRP says that classes that are not tightly bound to each other with class relationships should not be in the same component.

The Common Closure Principle (CCP)


The classes in a component should be closed together against the same kinds of changes. A change that affects a component affects all the classes in that component and no other components.


This is the Single-Responsibility Principle (SRP) restated for components. Just as SRP says that a class should not contain multiple reasons to change, CCP says that a component should not have multiple reasons to change.

In most applications, maintainability is more important that reusability. If the code in an application must change, you would prefer the changes to occur all in one component rather than being distributed through many components. If changes are focused into a single component, we need redeploy only the one changed component. Other components that don’t depend on the changed component do not need to be revalidated or redeployed.

CCP prompts us to gather together in one place all the classes that are likely to change for the same reasons. If two classes are so tightly bound, either physically or conceptually, that they always change together, they belong in the same component. This minimizes the workload related to releasing, revalidating, and redistributing the software.

This principle is closely associated with the Open/Closed Principle (OCP). For it is “closure” in the OCP sense of the word that this principle is dealing with. OCP states that classes should be closed for modification but open for extension. But as we learned, 100 percent closure is not attainable. Closure must be strategic. We design our systems such that they are closed to the most common kinds of changes that we have experienced.

CCP amplifies this by grouping together classes that are open to certain types of changes into the same components. Thus, when a change in requirements comes along, that change has a good chance of being restricted to a minimal number of components.

Summary of Component Cohesion

In the past, our view of cohesion was much simpler. We used to think that cohesion was simply the attribute of a module to perform one, and only one, function. However, the three principles of component cohesion describe a much more complex kind of cohesion. In choosing the classes to group together into a component, we must consider the opposing forces involved in reusability and developability.

Balancing these forces with the needs of the application is nontrivial. Moreover, the balance is almost always dynamic. That is, the partitioning that is appropriate today might not be appropriate next year. Thus, the composition of the component will likely jitter and evolve with time as the focus of the project changes from developability to reusability.

Principles of Component Coupling: Stability

The next three principles deal with the relationships between components. Here again, we will run into the tension between developability and logical design. The forces that impinge on the architecture of a component structure are technical, political, and volatile.

image

The Acyclic Dependencies Principle (ADP)


Allow no cycles in the component dependency graph.


Have you ever worked all day, gotten some stuff working, and then gone home, only to arrive the next morning to find that your stuff no longer works? Why doesn’t it work? Because somebody stayed later than you and changed something you depend on! I call this “the morning-after syndrome.”

The “morning-after syndrome” occurs in development environments in which many developers are modifying the same source files. In relatively small projects with only a few developers, it isn’t too big a problem. But as the size of the project and the development team grows, the mornings after can get pretty nightmarish. It is not uncommon for weeks to go by without being able to build a stable version of the project. Instead, everyone keeps on changing and changing their code, trying to make it work with the last changes that someone else made.

Over the past several decades, two solutions to this problem have evolved: the weekly build and ADP. Both solutions have come from the telecommunications industry.

The weekly build

The weekly build is common in medium-sized projects. It works like this: For the first 4 days of the week, all the developers ignore one another. They all work on private copies of the code and don’t worry about integrating with one another. Then, on Friday, they integrate all their changes and build the system. This has the wonderful advantage of allowing the developers to live in an isolated world for four days out of five. The disadvantage, of course, is the large integration penalty that is paid on Friday.

Unfortunately, as the project grows, it becomes less feasible to finish integrating on Friday. The integration burden grows until it starts to overflow into Saturday. A few such Saturdays are enough to convince the developers that integration should begin on Thursday. And so the start of integration slowly creeps toward the middle of the week.

As the duty cycle of development versus integration decreases, the efficiency of the team decreases, too. Eventually, this becomes so frustrating that the developers or the project managers declare that the schedule should be changed to a biweekly build. This suffices for a time, but the integration time continues to grow with project size.

This eventually leads to a crisis. To maintain efficiency, the build schedule has to be continually lengthened. Yet lengthening the build schedule increases project risks. Integration and testing become more and more difficult, and the team loses the benefit of rapid feedback.

Eliminating dependency cycles

The solution to this problem is to partition the development environment into releasable components. The components become units of work that can be the responsibility of a developer or a team of developers. When developers get a component working, they release it for use by the other developers. They give it a release number, and move it into a directory for other teams to use, and continue to modify their component in their own private areas. Everyone else uses the released version.

As new releases of a component are made, other teams can decide whether to immediately adopt the new release. If they decide not to, they simply continue using the old release. Once they decide that they are ready, they begin to use the new release.

Thus, none of the teams are at the mercy of the others. Changes made to one component do not need to have an immediate effect on other teams. Each team can decide for itself when to adapt its component to new releases of the components they use. Moreover, integration happens in small increments. There is no single point at which all developers must come together and integrate everything they are doing.

This is a very simple and rational process and is widely used. However, to make it work, you must manage the dependency structure of the components. There can be no cycles. If there are cycles in the dependency structure, the morning-after syndrome cannot be avoided.

Consider the component diagram in Figure 28-1. Here, we see a rather typical structure of components assembled into an application. The function of this application is unimportant for the purpose of this example. What is important is the dependency structure of the components. Note that this structure is a directed graph. The components are the nodes, and the dependency relationships are the directed edges.

Figure 28-1. Component structures are a directed acyclic graph.

image

Now note one more thing. Regardless of which component you begin at, it is impossible to follow the dependency relationships and wind up back at that component. This structure has no cycles. It is a directed acyclic graph (DAG).

Now, note what happens when the team responsible for MyDialogs makes a new release of its component. It is easy to find out who is affected by this release; you simply follow the dependency arrows backward. Thus, MyTasks and MyApplication are both going to be affected. The developers currently working on those components will have to decide when they should integrate with the new release of MyDialogs.

When MyDialogs is released, it has no effect on many of the other components in the system. They don’t know about MyDialogs and don’t care when it changes. This is nice. It means that the impact of releasing MyDialogs is relatively small.

When the developers working on the MyDialogs component would like to run a test of that component, all they need do is build their version of MyDialogs with the version of the Windows component they are currently using. None of the other components in the system need be involved. This is nice; it means that the developers working on MyDialogs have relatively little work to do to set up a test and have relatively few variables to consider.

When it is time to release the whole system, it is done from the bottom up. First, the Windows component is compiled, tested, and released, followed by MessageWindow and MyDialogs, then Task, and then TaskWindow and Database. MyTasks is next and, finally, MyApplication. This process is very clear and easy to deal with. We know how to build the system because we understand the dependencies between its parts.

The effect of a cycle in the component dependency graph

Let us say that a new requirement forces us to change one of the classes in MyDialogs such that it makes use of a class in MyApplication. This creates a dependency cycle, as shown in the component diagram of Figure 28-2.

Figure 28-2. A component diagram with a cycle

image

This cycle creates some immediate problems. For example, the developers working on the MyTasks component know that in order to release, they must be compatible with Tasks, MyDialogs, Database, and Windows. However, with the cycle in place, they must now also be compatible with MyApplication, TaskWindow, and MessageWindow. That is, MyTasks now depends on every other component in the system. This makes MyTasks very difficult to release. MyDialogs suffers the same fate. In fact, the cycle forces MyApplication, MyTasks, and MyDialogs to always be released at the same time. They have, in effect, become one large component. All the developers who are working in any of those components will experience the morning-after syndrome once again. They will be stepping all over one another, since they must all be using exactly the same release of one another’s components.

But this is only part of the trouble. Consider what happens when we want to test the Mydialogs component. We find that we must reference every other component in the system, including the Database component. This means that we have to do a complete build just to test MyDialogs. This is intolerable.

Have you ever wondered why you have to reference so many different libraries and so much of everybody else’s stuff just to run a simple unit test of one of your classes? It is probably because there are cycles in the dependency graph. Such cycles make it very difficult to isolate modules. Unit testing and releasing become very difficult and error prone. Further, compile times grow geometrically with the number of modules. Moreover, when there are cycles in the dependency graph, it can be very difficult to work out what order to build the components in. Indeed, there may be no correct order, which can lead to some nasty problems.

Breaking the cycle

It is always possible to break a cycle of components and reinstate the dependency graph as a DAG. There are two primary mechanisms.

  1. Apply the Dependency-Inversion Principle (DIP). In the case of Figure 28-2, we could create an abstract base class that has the interface that MyDialogs needs. We could then put that abstract base into MyDialogs and inherit it into MyApplication. This inverts the dependency between MyDialogs and MyApplication, thus breaking the cycle. See Figure 28-3.

    Figure 28-3. Breaking the cycle with dependency inversion

    image

    Note that once again, that we named the interface after the client rather than the server. This is yet another application of the rule that interfaces belong to clients.

  2. Create a new component that both MyDialogs and MyApplication depend on. Move the class(es) that they both depend on into that new component. See Figure 28-4.

    Figure 28-4. Breaking the cycle with a new component

    image

The second solution implies that the component structure is volatile in the presence of changing requirements. Indeed, as the application grows, the component-dependency structure jitters and grows. Thus, the dependency structure must always be monitored for cycles. When cycles occur, they must be broken somehow. Sometimes, this will mean creating a new component, making the dependency structure grow.

Top-down versus bottom-up design

The issues we have discussed so far lead to an inescapable conclusion. The component structure cannot be designed from the top down in the absence of code. Rather, that structure evolves as the system grows and changes.

Some of you may find this to be counterintuitive. We have come to expect that large-grained decompositions, such as components, are also high-level functional decompositions. When we see a large-grained grouping, such as a component dependency structure, we feel that the components ought to somehow represent the functions of the system.

Although it is true that components offer services and functions to one another, there is more to it than that.

The component-dependency structure is a map of the buildability of the application. This is why component structures can’t be completely designed at the start of the project. This is also why they are not strictly based on functional decomposition. As more and more classes accumulate in the early stages of implementation and design, there is a growing need to manage the dependencies so that the project can be developed without the morning-after syndrome. Moreover, we want to keep changes as localized as possible, so we start paying attention to SRP and CCP and collocate classes that are likely to change together.

As the application continues to grow, we become concerned about creating reusable elements. Thus, CRP begins to dictate the composition of the components. Finally, as cycles appear, ADP is applied, and the component-dependency graph jitters and grows for reasons that have more to do with dependency structure than with function.

If we tried to design the component-dependency structure before we had designed any classes, we would likely fail rather badly. We would not know much about common closure, we would be unaware of any reusable elements, and we would almost certainly create components that produced dependency cycles. Thus, the component-dependency structure grows and evolves with the logical design of the system.

It does not take long, however, for the component structure to settle down into something that is stable enough to support multiple-team development. Once this happens, the teams can focus on their own components. Communication between teams can be restricted to the component boundaries. This enables many teams to work concurrently on the same project with a minimum of overhead.

Keep in mind, however, that the structure of the components will continue to jitter and change as development proceeds. This prevents perfect isolation between the component teams. Those teams will have to work together as the shapes of the components push and pull against each other.

The Stable-Dependencies Principle (SDP)


Depend in the direction of stability.


Designs cannot be completely static. Some volatility is necessary if the design is to be maintained. We accomplish this by conforming to CCP. Using this principle, we create components that are sensitive to certain kinds of changes. These components are designed to be volatile; we expect them to change.

Any component that we expect to be volatile should not be depended on by a component that is difficult to change! Otherwise, the volatile component will also be difficult to change.

It is the perversity of software that a module that you have designed to be easy to change can be made difficult to change by someone else simply hanging a dependency upon it. Not a line of source code in your module need change, and yet your module will suddenly be difficult to change. By conforming to SDP, we ensure that modules that are intended to be easy to change are not depended on by modules that are more difficult to change than they are.

Stability

What is meant by stability? Stand a penny on its side. Is it stable in that position? You’d likely say that it was not. However, unless disturbed, it will remain in that position for a very long time. Thus, stability has nothing directly to do with frequency of change. The penny is not changing, but it is difficult to think of it as stable.

Webster says that something is stable if it is “not easily moved.”1 Stability is related to the amount of work required to make a change. The penny is not stable, because it requires very little work to topple it. On the other hand, a table is very stable, because it takes a considerable amount of effort to turn it over.

How does this relate to software? Many factors make a software component difficult to change: its size, complexity, clarity, and so on. But we are going to ignore all those factors and focus on something different. One sure way to make a software component difficult to change is to make lots of other software components depend on it. A component with lots of incoming dependencies is very stable, because it requires a great deal of work to reconcile any changes with all the dependent components.

Figure 28-5 shows X, a stable component. This component has three components depending on it and therefore has three good reasons not to change. We say that it is responsible to those three components. On the other hand, X depends on nothing, so it has no external influence to make it change. We say that it is independent.

Figure 28-5. X: A stable component

image

Figure 28-6, on the other hand, shows a very instable component. Y has no other components depending on it; we say that it is irresponsible. Y also has three components that it depends on, so changes may come from three external sources. We say that Y is dependent.

Figure 28-6. Y: An instable component

image

Stability metrics

How can we measure the stability of a component? One way is to count the number of dependencies that enter and leave that component. These counts will allow us to calculate the positional stability of the component:

Ca (afferent couplings): The number of classes outside this component that depend on classes within this component

Ce (efferent couplings): The number of classes inside this component that depend on classes outside this component

image

This metric has the range [0,1]. I = 0 indicates a maximally stable component. I = 1 indicates a maximally instable component.

The Ca and Ce metrics are calculated by counting the number of classes outside the component in question that have dependencies on the classes inside the component in question. Consider the example in Figure 28-7:

Figure 28-7. Tabulating Ca, Ce, and I

image

The dashed arrows between the components represent component’s dependencies. The relationships between the classes of those components show how those dependencies are implemented. There are inheritance and association relationships.

Now, let’s say that we want to calculate the stability of the component Pc. We find that three classes outside Pc depend on classes in Pc. Thus, Ca = 3. Moreover, there is one class outside Pc that classes in Pc depend on. Thus, Ce = 1, and I = 1/4.

In C#, these dependencies are typically represented by using statements. Indeed, the I metric is easiest to calculate when you have organized your source code such that there is one class in each source file. In C#, the I metric can be calculated by counting using statements and fully qualified names.

When the I metric is 1, it means that no other component depends on this component (Ca = 0), and this component does depend on other components (Ce > 0). This is as instable as a component can get; it is irresponsible and dependent. Its lack of dependents gives it no reason not to change, and the components that it depends on may give it ample reason to change.

On the other hand, when the I metric is 0, the component is depended on by other components (Ca > 0) but does not itself depend on any other components (Ce = 0). It is responsible and independent. Such a component is as stable as it can get. Its dependents make it difficult to change, and it has no dependencies that might force it to change.

According to SDP, the I metric of a component should be larger than the I metrics of the components that it depends on. That is, I metrics should decrease in the direction of dependency.

Variable component stablility

If all the components in a system were maximally stable, the system would be unchangeable. This is not a desirable situation. Indeed, we want to design our component structure so that some components are instable and some are stable. Figure 28-8 shows an ideal configuration for a system with three components.

Figure 28-8. Ideal component configuration

image

The changeable components are on top and depend on the stable component at the bottom. Putting the instable components at the top of the diagram is a useful convention, since any arrow that points up is violating SDP.

Figure 28-9 shows how SDP can be violated. Flexible is a component that we intend to be easy to change. We want Flexible to be instable, with an I metric close to 0. However, some developer, working in the component named Stable, has hung a dependency on Flexible. This violates SDP, since the I metric for Stable is much lower than the I metric for Flexible. As a result, Flexible will no longer be easy to change. A change to Flexible will force us to deal with Stable and all its dependents.

Figure 28-9. Violation of SDP

image

To fix this, we somehow have to break the dependence of Stable on Flexible. Why does this dependency exist? Let’s assume that within Flexible is a class C that another class U within Stable needs to use. See Figure 28-10.

Figure 28-10. The cause of the bad dependency

image

We can fix this by using DIP. We create an interface called IU and put it in a component named UInterface. We make sure that this interface declares all the methods that U needs to use. We then make C inherit from this interface. See Figure 28-11. This breaks the dependency of Stable on Flexible and forces both components to be dependent on UInterface. UInterface is very stable (I = 0), and Flexible retains its necessary instability (I = 1). All the dependencies now flow in the direction of decreasing I.

Figure 28-11. Fixing the stability violation, using DIP

image

High-level design placement

Some software in the system should not change very often. This software represents the high-level architecture and design decisions. We don’t want these architectural decisions to be volatile. Thus, the software that encapsulates the high-level design of the system should be put into stable components (I = 0). The instable components (I = 1) should contain only the software that is likely to change.

However, if the high-level design is put into stable components, the source code that represents that design will be difficult to change, which could make the design inflexible. How can a component that is maximally stable (I = 0) be flexible enough to withstand change? The answer is to be found in OCP. This principle tells us that it is possible and desirable to create classes that are flexible enough to be extended without requiring modification. What kinds of classes conform to this principle? The answer is Abstract classes.

The Stable-Abstractions Principle (SAP)


A component should be as abstract as it is stable.


This principle sets up a relationship between stability and abstractness. It says that a stable component should also be abstract so that its stability does not prevent it from being extended. On the other hand, it says that an instable component should be concrete, since its instability allows the concrete code within it to be easily changed.

Thus, if a component is to be stable, it should also consist of abstract classes so that it can be extended. Stable components that are extensible are flexible and do not overly constrain the design.

Combined, SAP and SDP amount to DIP for components. This is true because the SDP says that dependencies should run in the direction of stability, and SAP says that stability implies abstraction. Thus, dependencies run in the direction of abstraction.

However, DIP deals with classes. With classes, there are no shades of gray. A class either is abstract or is not. The combination of SDP and SAP deals with components and allows that a component can be partially abstract and partially stable.

Measuring abstraction

The A metric is a measure of the abstractness of a component. Its value is simply the ratio of abstract classes in a component to the total number of classes in the component, where

Nc is the number of classes in the component.

Na is the number of abstract classes in the component. Remember, an abstract class is a class with at least one abstract method and cannot be instantiated:

image

The A metric ranges from 0 to 1. Zero implies that the component has no abstract classes at all. A value of 1 implies that the component contains nothing but abstract classes.

The main sequence

We are now in a position to define the relationship between stability (I) and abstractness (A). We can create a graph with A on the vertical axis and I on the horizontal axis. If we plot the two “good” kinds of components on this graph, we will find the components that are maximally stable and abstract at the upper left at (0,1). The components that are maximally instable and concrete are at the lower right at (1,0). See Figure 28-12.

Figure 28-12. The A/I graph

image

Not all components can fall into one of these two positions. Components have degrees of abstraction and stability. For example, it is very common for one abstract class to derive from another abstract class. The derivative is an abstraction that has a dependency. Thus, though it is maximally abstract, it will not be maximally stable. Its dependency will decrease its stability.

Since we cannot enforce that all components sit at either (0,1) or (1,0), we must assume that a locus of points on the A/I graph defines reasonable positions for components. We can infer what that locus is by finding the areas where components should not be, that is, zones of exclusion. See Figure 28-13.

Figure 28-13. Zones of exclusion

image

Consider a component in the area near (0,0). This component is highly stable and concrete. Such a component is not desirable, because it is rigid. It cannot be extended, because it is not abstract. And it is very difficult to change, because of its stability. Thus, we do not normally expect to see well-designed components sitting near (0,0). The area around (0,0) is a zone of exclusion, the zone of pain.

It should be noted that in some cases, components do indeed fall within the zone of pain. An example would be a component representing a database schema. Database schemas are notoriously volatile, extremely concrete, and are highly depended upon. This is one of the reasons that the interface between OO applications and databases is so difficult and that schema updates are generally painful.

Another example of a component that sits near (0,0) is a component that holds a concrete utility library. Although such a component has an I metric of 1, it may in fact be nonvolatile. Consider a “string” component, for example. Even though all the classes within it are concrete, it is nonvolatile. Such components are harmless in the (0,0) zone, since they are not likely to be changed. Indeed, we can consider a third axis of the graph being that of volatility. If so, the graph in Figure 28-13 shows the plane at volatility = 1.

Consider a component near (1,1). This location is undesirable, because it is maximally abstract and yet has no dependents. Such components are useless. Thus, this is called the zone of uselessness.

It seems clear that we’d like our volatile components to be as far from both zones of exclusion as possible. The locus of points that is maximally distant from each zone is the line that connects (1,0) and (0,1). This line is known as the main sequence.2

A component that sits on the main sequence is not “too abstract” for its stability; nor is it “too instable” for its abstractness. It is neither useless nor particularly painful. It is depended on to the extent that it is abstract, and it depends upon others to the extent that it is concrete.

Clearly, the most desirable positions for a component to hold are at one of the two endpoints of the main sequence. However, in my experience, less than half the components in a project can have such ideal characteristics. Those other components have the best characteristics if they are on or close to the main sequence.

Distance from the main sequence

This leads us to our last metric. If it is desirable for components to be on or close to the main sequence, we can create a metric that measures how far away a component is from this ideal.

D (Distance). image This ranges from [0,~0.707].

D′ (Normalized distance). D′ = |A+I-1|. This metric is much more convenient than D, since it ranges from [0,1]. Zero indicates that the component is directly on the main sequence. One indicates that the component is as far away as possible from the main sequence.

Given this metric, a design can be analyzed for its overall conformance to the main sequence. The D metric for each component can be calculated. Any component that has a D value that is not near 0 can be reexamined and restructured. In fact, this kind of analysis has been a great aid to me in helping to define components that are more maintainable and less sensitive to change.

Statistical analysis of a design is also possible. One can calculate the mean and variance of all the D metrics for the components within a design. One would expect a conformant design to have a mean and variance close to 0. The variance can be used to establish “control limits” that can identify components that are “exceptional” in comparison to all the others. See Figure 28-14.

Figure 28-14. Scatter plot of component D scores

image

In this scatter plot—not based on real data—we see that the bulk of the components lie along the main sequence but that some of them are more than one standard deviation (Z = 1) away from the mean. These aberrant components are worth looking at. For some reason, they are either very abstract with few dependents or very concrete with many dependents.

Another way to use the metrics is to plot the D′ metric of each component over time. Figure 28-15 shows a mock-up of such a plot. You can see that some strange dependencies have been creeping into the Payroll component over the last few releases. The plot shows a control threshold at D′ = 0.1. The R2.1 point has exceeded this control limit, so it would be worth our while to find out why this component is so far from the main sequence.

Figure 28-15. Time plot of a single component’s D′ scores

image

Conclusion

The dependency management metrics described in this chapter measure the conformance of a design to a pattern of dependency and abstraction that I think is a “good” pattern. Experience has shown that certain dependencies are good and others are bad. This pattern reflects that experience. However, a metric is not a god; it is merely a measurement against an arbitrary standard. It is certainly possible that the standard chosen in this chapter is appropriate only for certain applications and not for others. It may also be that far better metrics can be used to measure the quality of a design.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.83.87.94