< Day Day Up > |
3.4 Refactoring to Reduce CouplingYou may start with a cleanly defined design and you may layer your design, as we've discussed, so that each layer does one autonomous job. You may have coupling only at the appropriate places. But if you don't try to maintain that design, it won't last. Your code will naturally move toward tighter coupling unless you fight that tendency. In the last part of this chapter, we review some of the types of coupling, and how to avoid them. The benefits of looser coupling include:
Keep in mind that some coupling is natural. You've got to have some degree of coupling to do anything at all. Coupling gets out of hand when things that don't belong together are bound together. Your goal should be to avoid accidental coupling—you want any coupling in your application to be intentional and useful. Also, keep in mind that decoupling often comes at a price. You can add JMS queues and XML messages between every class, but you'll work harder and your application will be dog slow. Decoupling becomes much more important between major subsystems and layers of your architecture. 3.4.1 MicrocouplingMost of the code written today has many relationships, and most of those relationships are tightly coupled. Anything from a method call to the use of a common variable increases your coupling. Like I said earlier, that's not inherently bad. You just want to keep it intentional, and keep your coupling confined to an area of the architecture. Your software design strongly suggests where your coupling should be. Excessive coupling across packages, and across layers—in general, excessive coupling across a focused idea—breaks down your ability to do one thing and do it well. When you find it, refactor it. 3.4.1.1 Direct accessThe easiest type of coupling to find is direct access. When you directly call a method on another class or access its member functions, you're coupled to it. You can break coupling in a number of ways. When you're trying to loosen the coupling from two classes, often the easiest way to is to insert some kind of intermediary. The Java programming language includes interfaces for this purpose. Bear in mind that interfaces are useless for decoupling unless they are paired with a factory. This line of code: MyInterface myinterface = new MyObject( ); is no less tightly coupled than this one: MyObject myobject = new MyObject( ); Whereas this one accomplishes the task and is completely decoupled: MyInterface myinterface = MyFactory.getObject( ); Think about cars. They work because they have combustion engines that drive axles, which spin wheels. In order to drive a car, you don't manipulate the engine and axles directly; you turn a steering wheel, press gas and brake pedals, and maneuver a shift. The steering wheel, pedals, and shift make up the interface to a car. There is a big difference between a '72 Beetle and a '04 Ferrari under the hood, but anybody can drive either because they share an interface. An interface lets you couple to a capability rather than an implementation. Let's say that you're building classes that you'd like to fire when an event occurs. You could have your code explicitly call the fire method on all of the classes that you want to notify. This approach is limited to behavior that you can anticipate at compile time. A slightly better approach is to build a class that supports the method fire. Then, everything that needs to be triggered can inherit from that class. That's the solution many novice Java developers use. It's limiting, because you may want to trigger other types of classes too. Instead, use an interface called Firable: interface Firable { public void fire( ); } Notice that you don't see an implementation. Now, whenever you want a Firable class, you simply implement the interface: public class AlarmClock implements Firable { public void fire( ) { System.out.println("Ring!"); } } Now, other classes can use your "fire" method without coupling directly to yours: public void wakeUp(Firable clock) { clock.fire( ); } The idea is to couple to an idea rather than an implementation. You don't want to build an interface that repeats every method of a class. Instead, break out the concepts that you want to expose in the interface. If you find yourself addicted to JUnit as I have, you'll use this trick with some frequency. The nice thing about this approach is that you don't have to have any special behavior to test the alarm clock. You can also quickly mock a Firable class to help test code that fires your interface. Interfaces serve as intermediaries, and you can decouple with other kinds of intermediaries as well. A façade is an intermediary that is nothing more than a thin wrapper. At first glance, you might think that you're trading coupling from one area of the application to the other, so you gain nothing at all. That premise is not entirely true. You'll see a few direct benefits:
You've probably seen other kinds of intermediaries as well. Rather than initialize a class with a new one, followed immediately by many sets, you can insert a constructor as an intermediary to enforce a policy for construction and consolidate several method calls. If you need to consistently call five methods to do a job, such as to establish a JDBC connection, you can wrap that code into a single method, or a class, like a connection manager. 3.4.1.2 InheritanceInheritance is one of the least understood mechanisms in modern programming. It is tempting to use the casual "inheritance is for is-a relationships," but this is just semantic handwaving. Everything "is-a" something else. Conceptually, there are two kinds of inheritance: implementation and interface. When a class inherits from another class and by doing so inherits actual implementation details (field values or code blocks), that is implementation inheritance. When a class implements an interface, thus promising to provide the services described there but without inheriting any specific values or code, that is interface inheritance. In languages like C++, where multiple implementation inheritance is allowed, the problem can be quite severe. Classes that inherit from multiple direct parents can become logical Frankenstein's monsters, half-living beasts that don't quite look normal and never behave. Newer languages like Java solve part of the problem by eliminating multiple implementation inheritance. A Java class can have only one direct parent class (which in turn can have one direct parent, and so on). The chain is easier to follow and the results more predictable. However, classes can implement as many interfaces as they desire. Since interfaces do not impart specific implementation details to the implementer, just a public contract for services provided, the results are again easier to predict. Since any implementation code sitting behind an interface is living in the class itself, there is never the question of hidden conflicts and accidental overrides creating random runtime behavior. In order to decide which kinds of ideas require which kind of inheritance, it requires a little common sense and a little Zen meditation. When two or more classes represent specific categories of a single idea (Employees and Customers are both a kind of Person), then implementation inheritance makes sense. Person is a good candidate for a superclass. All logical children of that idea share the data and methods abstracted back to the Person object. Interfaces, on the other hand, are useful for identifying services that cross-cut the logical model of the application. Imagine you are writing an application for a veterinary clinic. You might have two classes, Employee and Customer, which both inherit from Person. You might also have three other classes, Cat, Dog, and Bird, all inheriting from Animal. If they should be persistent, you can implement the PersistentObject interface as needed. The key is that each kind of person must be a Person; they need not necessarily be persistent. Each kind of animal must be an Animal, but they only may be persistent. 3.4.1.3 Transitive couplingKeep in mind that coupling is transitive. If A is coupled to B, and B is coupled to C, then A is coupled to C. This type of coupling often seems innocuous, but it can get out of control in a hurry. It's especially painful when you're dealing with nested properties. Whether you have something like this: store.getAddress( ).getCountry( ).getState( ).getCity( ) or something like this: address.country.state.city you're building a whole lot of assumptions into a very small place. Dave Thomas, founder of the Pragmatic Programmer practice, calls this programming style the "Java train wreck." The worst form of the train wreck reaches into many different packages. The problem is that you're coupling all four classes together. Think of the things that might some day change. You might need to support multiple addresses, or international provinces instead of states. Decouple this kind of code. You might decide to add some convenience methods for your customers or you might need to build a flatter structure, or even determine why you need access to the city in the first place. If it's to compute a tax, you might have a getTax method that isolates this coupling to one place. If it's because stores in certain cities have special attributes, you may add the attributes or methods to Store to loosen the overall coupling. 3.4.1.4 The role of transparencySometimes, you want to apply a little extra energy and completely focus certain pieces of code. For example, recall that we wanted to add security to our Account class without adding special security methods. We would do so with another layer. You would say that the Account class is transparent with respect to security. Business rules often need special treatment, because they tend to be complex and tend to change with great frequency. Increasingly, leading edge Java developers look to find ways to isolate the business domain model from other concerns. Right now, the Java community is struggling to find the right way to package service layers, in order to keep business domain models fully transparent. Component architectures like EJB say that you should build your application as components and snap them into containers that provide the services. This architecture has tended to be too invasive and cumbersome. Instead, others say that services should be packaged as aspects, using a new development model called Aspect-Oriented Programming (see Chapter 11). As a compromise, many people are working to develop lighter containers that allow plain Java objects rather than special components. Pico and Spring (covered in Chapter 8) are two lightweight containers that are growing in popularity. 3.4.1.5 Testing and couplingAs you've already seen, your first defense against tight coupling is good, solid unit testing of bite-sized building blocks. As you code, you'll likely build an implementation, use that implementation in your code, and then reuse it again within your unit tests, as in Figure 3-5. Since you've built at least two clients into your development model and intend to test bite-sized pieces, you're much more likely to keep your coupling down to a level that's easy to manage. Further, your test cases will use each new class outside of its original context. With test-first development, you'll quickly understand where your coupling and reuse problems lie. Figure 3-5. Testing offers the chance to have multiple clients for your new classes3.4.2 MacrocouplingCoupling at a higher level, or macrocoupling, is usually a much more serious problem than microcoupling because you want to keep each software layer as autonomous as possible. For years, distributed technologies forced significant coupling, making many kinds of refactoring nearly impossible. Communication protocols forced clients and servers to manage intricate handshakes even to make a connection. Later, remote procedure call technologies forced clients to bind directly to a named procedure with a fixed set of parameters and fixed orders and types. CORBA took things a step further, and forced clients to bind to a whole specific object. Today, you don't usually have to couple as tightly. A variety of technologies help build and connect independent systems. You can fight macrocoupling on several different levels. 3.4.2.1 Communication modelYour communication model can have a dramatic impact on the degree of coupling between your systems. Early in your design process, make some painless decisions that reduce the coupling between your systems:
Each of these techniques can reduce coupling, but remember that sometimes coupling is good. If you've got strict control of both ends of an interface, and if you don't expect the interface to change, then a tighter coupling can possibly buy you better performance. For the most part, however, it's usually worth it to pay a small performance penalty to reduce coupling from the beginning. 3.4.2.2 FaçadesFaçade layers don't really reduce your coupling. Instead, they let you couple to something that's a little less permanent. In addition, façades have some other important benefits:
Figure 3-6. Client 1 must make four round-trip communications to the server; Client 2 reduces the total number of communications to one3.4.2.3 Shared dataApplications that share interfaces usually need to share data as well. Whether you're using a buffer or a parameter list, the problem is the same. If you fix the number, type, order, or size of parameters, you're asking for trouble, because changes in the payload invariably force both sides of the interface to change. These strategies can help you to reduce coupling between subsystems that share data:
Flexible data interchange formats are both a blessing and a curse. Your endpoints are more flexible in the face of changes to the payload, but it is more difficult to know exactly what is being shared. The more loosely typed your data interchange format, the more self-describing it must be. This is vital. If you pass name-value pairs, make sure that the consumer of the data can enumerate over both the values and the names. XML is a perfect format, since it is inherently self-describing. 3.4.2.4 DatabasesThe data access layer is one of the most problematic for Java developers to isolate. It doesn't need to be. Many good frameworks and solutions let you build an independent, transparent business model that knows nothing about persistence. Many persistence frameworks (such as Hibernate, JDO, and OJB) handle this well. You must also ask whether you need a full relational database management system (RDBMS). Relational databases are large, expensive (in both resources and dollars) and complex. Sometimes, flat files are all that is needed. Make sure that you need what it is you are trying to wrap. Regardless, you need not bite off a full persistence framework to solve a good chunk of this problem. You can build a lightweight DAO layer (like the one that we started for this chapter's example) to manage all data access for your application. There are a variety of IDEs and standalone tools that generate DAO layers automatically. 3.4.2.5 ConfigurationMany times, you might want to avoid a particular standardized service to use a lighter, faster proprietary service. If you did so, you would have better performance and an easier interface, but you could be boxing your users into a corner. The makers of Kodo JDO faced that problem, and decided to make the service configurable. Increasingly, frameworks use configuration to decouple systems. Better configuration options invariably reduce coupling. This list is far from exhaustive. If you want to excel at finding coupling problems, you've got to sharpen your observation skills. There's simply no substitute for reading code and watching the usage patterns, especially around the perimeter of a layer. |
< Day Day Up > |