Agile Principles, Patterns, and Practices in C#

According to Booch, "all well structured object-oriented architectures have clearly-defined layers, with each layer providing some coherent set of services through a well-defined and controlled interface."[1] A naive interpretation of this statement might lead a designer to produce a structure similar to Figure 11-1. In this diagram, the high-level Policy layer uses a lower-level Mechanism layer, which in turn uses a detailed-level Utility layer. Although this may look appropriate, it has the insidious characteristic that the Policy layer is sensitive to changes all the way down in the Utility layer. Dependency is transitive. The Policy layer depends on something that depends on the Utility layer; thus, the Policy layer transitively depends on the Utility layer. This is very unfortunate.

[1] [Booch96], p. 54

Figure 11-1. Naive layering scheme

Figure 11-2 shows a more appropriate model. Each upper-level layer declares an abstract interface for the services it needs. The lower-level layers are then realized from these abstract interfaces. Each higher-level class uses the next-lowest layer through the abstract interface. Thus, the upper layers do not depend on the lower layers. Instead, the lower layers depend on abstract service interfaces declared in the upper layers. Not only is the transitive dependency of PolicyLayer on UtilityLayer broken; so too is the direct dependency of the PolicyLayer on MechanismLayer.

Figure 11-2. Inverted layers

Ownership Inversion

Note that the inversion here is one of not only dependencies but also interface ownership. We often think of utility libraries as owning their own interfaces. But when DIP is applied, we find that the clients tend to own the abstract interfaces and that their servers derive from them.

This is sometimes known as the Hollywood principle: "Don't call us; we'll call you."[2] The lower-level modules provide the implementation for interfaces that are declared within, and called by, the upper-level modules.

[2] [Sweet85]

Using this inversion of ownership, PolicyLayer is unaffected by any changes to MechanismLayer or UtilityLayer. Moreover, PolicyLayer can be reused in any context that defines lower-level modules that conform to the PolicyService-Interface. Thus, by inverting the dependencies, we have created a structure that is simultaneously more flexible, durable, and mobile.

In this context, ownership simply means that the owned interfaces are distributed with the owning clients and not with the servers that implement them. The interface is in the same package or library with the client. This forces the server library or package to depend on the client library or package.

Of course, there are times when we don't want the server to depend on the client. This is especially true when there are many clients but only one server. In that case, the clients must agree on the service interface and publish it in a separate package.

Dependence on Abstractions

A somewhat more naive, yet still very powerful, interpretation of DIP is the simple heuristic: "Depend on abstractions." Simply stated, this heuristic recommends that you should not depend on a concrete class and that rather, all relationships in a program should terminate on an abstract class or an interface.

  • No variable should hold a reference to a concrete class.

  • No class should derive from a concrete class.

  • No method should override an implemented method of any of its base classes.

Certainly, this heuristic is usually violated at least once in every program. Somebody has to create the instances of the concrete classes, and whatever module does that will depend on them.[3] Moreover, there seems no reason to follow this heuristic for classes that are concrete but nonvolatile. If a concrete class is not going to change very much, and no other similar derivatives are going to be created, it does very little harm to depend on it.

[3] Actually, there are ways around this if you can use strings to create classes. C# allows this. So do several other languages. In such languages, the names of the concrete classes can be passed into the program as configuration data.

For example, in most systems, the class that describes a string is concrete. In C#, for example, it is the concrete class string. This class is not volatile. That is, it does not change very often. Therefore, it does no harm to depend directly on it.

However, most concrete classes that we write as part of an application program are volatile. It is those concrete classes that we do not want to depend directly on. Their volatility can be isolated by keeping them behind an abstract interface.

This is not a complete solution. There are times when the interface of a volatile class must change, and this change must be propagated to the abstract interface that represents the class. Such changes break through the isolation of the abstract interface.

This is the reason that the heuristic is a bit naive. If, on the other hand, we take the longer view that the client modules or layers declare the service interfaces that they need, the interface will change only when the client needs the change. Changes to the classes that implement the abstract interface will not affect the client.

Категории