11. Avoiding Dependencies and Highly Coupled Classes

As presented in Chapter 1, “Introduction to Object-Oriented Concepts,” the traditional criteria of classical object-oriented programming are encapsulation, inheritance, and polymorphism. Theoretically, to consider a programming language as an object-oriented language, it must follow these three principles. In addition, as also covered in Chapter 1, I like to include composition.

Thus, when I teach object-oriented programming, my list of fundamental concepts looks like this:

  • Encapsulation

  • Inheritance

  • Polymorphism

  • Composition

Tip

Perhaps I should add interfaces to this list, but I have always considered interfaces to be a specific type of inheritance.

Adding composition to this list is even more important in today’s development environment because of the debate over how to use inheritance appropriately. Concerns about using inheritance are not a recent phenomenon. In the past several years, this debate has heated up. Many developers I talk to advocate for using composition rather than inheritance (often called composition over inheritance). In fact, some avoid using inheritance at all, or at least limit the use of inheritance to a single hierarchical level.

The reason for focusing on how to use inheritance revolves around the issue of coupling. The arguments for using inheritance are, most certainly, reusability, extensibility, and polymorphism; however, inheritance can cause problems by creating dependencies between classes—in effect, coupling the classes. These dependencies create potential problems for maintenance and testing. Chapter 7, “Mastering Inheritance and Composition,” discussed how inheritance might actually weaken encapsulation, which seems counterintuitive because they are both fundamental concepts. Nevertheless, this is actually part of the fun, and requires that we really think about how we should use inheritance.

Caution

Be aware that I am not advocating avoiding inheritance. The discussion here is actually about avoiding dependencies and highly coupled classes. When to use inheritance is an important part of this discussion.

This debate leads to the following question: if not inheritance, then what? The short answer is to use composition. This should not be surprising because throughout the book I contend that there are really only two ways to reuse classes: using inheritance and using composition. You can either create a child from a parent class via inheritance or contain one class within another class using composition.

If, as some people advocate, inheritance is to be avoided, why do we spend time learning it? The answer is simple: A lot of code utilizes inheritance. As most developers soon come to understand, the vast majority of the code encountered appears in maintenance mode. Thus, it is imperative to understand how to fix, enhance, and maintain code written using inheritance. You may even write some new code using inheritance. In short, a programmer needs to cover all the possible bases and learn the entire developers’ toolkit. However, this also means that we have to keep adding tools to that kit as well as rethink how we use them.

Again, please understand that I am not making any value judgments here. I am not claiming that inheritance is problematic and to avoid it. What I am saying is that it is important to fully grasp how inheritance is used, carefully study alternative ways of design, and then decide for yourself. Thus, the intent of the examples in this chapter is not necessarily to describe the optimal way to design your classes; they are educational exercises meant to get you thinking about the issues associated with deciding between inheritance and composition. Remember that it is important for all technologies to evolve, keep the good, and refine the not-so good.

Moreover, composition poses its own coupling issues. In Chapter 7 I discussed the various types of composition: associations and aggregations. Aggregations are objects that are embedded in other objects (created with the new keyword) while associations are objects that are passed into other objects via a parameter list. Because aggregations are embedded in objects, they are highly coupled, which we want to avoid.

Therefore, while inheritance may have obtained a reputation as encouraging highly coupled classes, composition (using aggregations) also can create highly coupled classes. Let’s revisit the stereo component example used in Chapter 9, “Building Objects and Object-Oriented Design,” to bring all of these concepts together in a specific example.

Creating a stereo with aggregations can be likened to creating a boombox, which is a product that has all the components embedded inside a single unit. In many situations, this can be very convenient. It can be picked up, moved easily, and requires no special assembly. However, this design can also lead to many problems. If one component, say the MP3 player, breaks, you must take in the entire unit for repair. Even worse, many problems may arise to render the entire boombox unusable, such as an electrical issue.

Creating a stereo with associations can mitigate many of the problems encountered with aggregations. Think of a component stereo system as a bunch of associations connected by patch cords (or wireless). In this design, there is a central object called a receiver connected to several other objects such as speakers, CD players, even turntables and cassette players. In fact, think of this as a vendor-neutral solution because we can simply obtain a component off the shelf, which is a major advantage.

In this situation, if the CD player breaks, you simply disconnect it, providing the opportunity to either fix the CD player (while still enjoying the use of the other components) or swapping it out with a new CD player that works. This is the advantage of using associations and keeping the coupling between classes to a minimum.

Tip

As pointed out in Chapter 9, although highly coupled classes are generally frowned upon, there might be times when you are willing to accept the risk of a highly coupled design. The boombox is one such example. Despite the fact that it has a highly coupled design, it is sometimes the preferred choice.

Now that we have reviewed the coupling issues of both inheritance and composition, let’s explore examples of some highly coupled designs using both inheritance and composition. As I often do in the classroom, we will iterate through these examples until we use a technique called dependency injection to mitigate the coupling issues.

Composition versus Inheritance and Dependency Injection

To begin, we can focus on how to take an inheritance model (gleaned from examples often used in this book) and redesign it, not with inheritance but with composition. The second example shows how we can redesign with composition—albeit using aggregation, which is not necessarily an optimal solution. The third example shows how to avoid aggregations and design with associations instead—the concept of dependency injection.

1) Inheritance

Whether or not you buy into the argument of composition over inheritance, let’s begin by presenting a straightforward example of inheritance and explore how it might otherwise be implemented using composition, revisiting the mammal example used throughout the book.
In this case, we introduce a bat—a mammal that can fly, as seen in Figure 11.1.

Usage of inheritance to create the Mammal heirarchy.
Figure 11.1 Using inheritance to create mammals.

In this example specifically, inheritance appears to be the obvious choice. Creating a Dog class that inherits from Mammal is a slam dunk—isn’t it? Look at the following code, which utilizes inheritance in this manner:

class Mammal {
    public void eat () {System.out.println("I am Eating");};
}
class Bat extends Mammal {
    public void fly () {System.out.println("I am Flying");};
}
class Dog extends Mammal {
    public void walk () {System.out.println("I am Walking");};
}
public class TestMammal {

    public static void main(String args[]) {

        System.out.println("Composition over Inheritance");;

        System.out.println("
Dog");
        Dog fido = new Dog();
        fido.eat();
        fido.walk();
        System.out.println("
Bat");
        Bat brown = new Bat();
        brown.eat();
        brown.fly();
    }
}

In this design, a Mammal has a single behavior, eat(), assuming that all mammals must eat. However, we start to see the problem with inheritance immediately when we add two Mammal subclasses, Bat and Dog. While a dog can walk, not all mammals walk. In addition, while a bat can indeed fly, not all mammals fly. So the question is, where do these methods go? Just like in our earlier penguin example, because not all birds fly, deciding where to place methods in an inheritance hierarchy can be tricky.

Separating the Mammal class into FlyingMammals and WalkingMammals is not a very elegant solution because this is only the tip of the proverbial iceberg. Some mammals can swim, some mammals even lay eggs. Moreover, there are likely countless other behaviors that individual mammal species possess, and it might be impractical to create a separate class for all of these behaviors. Thus, rather than approaching this design as an is-a relationship, perhaps we should explore it using a has-a relationship.

2) Composition

In this strategy, rather than embedding the behaviors in the classes themselves, we create individual classes for each behavior. Therefore, rather than placing behaviors in an inheritance hierarchy, we can create classes for each behavior and then build individual mammals by including just the behaviors that they require (via aggregation).

Thus, we create a class called Walkable and a class called Flyable, as seen in Figure 11.2.

Using Composition to build the Mammal heirarchy.
Figure 11.2 Using composition to create mammals.

For example, look at the following code. We still have the Mammal class with its eat() method, and we still have the Dog and Bat classes. The major design difference here is that the Dog and Bat classes obtain their behaviors via aggregation using composition.

Caution

Be aware that the term aggregation is used in the preceding paragraph. This example illustrates how composition can be used in lieu of inheritance; however, in this example, we are using aggregation, which still contains significant coupling. Thus, consider this an intermediate, educational step moving toward the next example using interfaces.

class Mammal {
    public void eat () {System.out.println("I am Eating");};
}
class Walkable {
    public void walk () {System.out.println("I am Walking");};
}
class Flyable {
    public void fly () {System.out.println("I am Flying");};
}
class Dog {
    Mammal dog = new Mammal();
    Walkable walker = new Walkable();
}
class Bat {
    Mammal bat = new Mammal();
    Flyable flyer = new Flyable();
}
public class TestMammal {

    public static void main(String args[]) {

        System.out.println("Composition over Inheritance");;
        System.out.println("
Dog");;
        Dog fido = new Dog();
        fido.dog.eat();
        fido.walker.walk();

        System.out.println("
Bat");;
        Bat brown = new Bat();
        brown.bat.eat();
        brown.flyer.fly();

    }

}

Note

The intent of this example is to illustrate how to use composition in lieu of inheritance; that does not mean that you cannot use inheritance at all in your designs. If you determine that absolutely all mammals eat, then, for example, perhaps you would decide to place the eat() method in the Mammal class and have Dog and Bat inherit from Mammal. As always, this is a design decision.

Perhaps the heart of this discussion lies in the concept we covered earlier, that inheritance breaks encapsulation. This is easy to understand because a change in the Mammal class would require a recompilation (and perhaps even a redeployment) of all the Mammal subclasses. This means that the classes are highly coupled, and this is counter to our stated goal of uncoupling classes as much as possible.

In our composition example, if we wanted to add a Whale class, none of the previously written classes would need a rewrite. You would add a class called Swimmable and a class called Whale. Then the Swimmable class could be reused for, say, a Dolphin class.

class Swimmable {
    public void fly () {System.out.println("I am Swimming");};
}
class Whale {
    Mammal whale = new Mammal();
    Walkable swimmer = new Swimmable ();
}

The main application can add this functionality with no changes to the classes that previously existed.

System.out.println("
Whale");
Whale shamu = new Whale();
shamu.whale.eat();
shamu.swimmer.swim();

One rule of thumb is to use inheritance in only truly polymorphic situations. Thus, Circles and Rectangles inheriting from Shape may well be a legitimate use of inheritance. On the other hand, behaviors such as walking and flying might not be good candidates for inheritance because overriding them could be problematic. For example, if you overrode the fly()method in Dog, the only obvious option would be a no-op (do nothing). Again, as we have seen with the earlier Penguin example, you don’t want a Dog to run over a cliff, execute the available fly()method and then, to Fido’s great chagrin, find that the fly()method doesn’t do anything.

While this example does indeed implement this solution using composition, there is a serious flaw to the design. The objects are highly coupled, since the use of the new keyword is obvious.

class Whale {
    Mammal whale = new Mammal();
    Walkable swimmer = new Swimmable ();
}

To complete our exercise of decoupling the classes, we introduce the concept of dependency injection. In short, rather than creating objects inside other objects, we will inject the objects from the outside via parameter lists. The discussion focuses solely on the concept of injecting dependencies.

Dependency Injection

The example in the previous section uses composition (with aggregation) to provide the Dog with a behavior called Walkable. The Dog class literally created a new Walkable object within the Dog class itself, as the following code fragment illustrates:

class Dog {
    Walkable walker = new Walkable();
}

Although this does in fact work, the classes remain highly coupled. To completely decouple the classes in the previous example, let’s implement the concept of dependency injection mentioned previously. Dependency injection and inversion of control are often covered together. One definition of inversion of control (IOC) is to make it someone else’s responsibility to make an instance of the dependency and pass it to you. This is exactly what we will implement in this example.

Because not all mammals walk, fly, or swim, to begin the decoupling process, we create interfaces to represent the behaviors for our various mammals. For this example, I will focus on the walking behavior by creating an interface called IWalkable as seen in Figure 11.3.

Addition of interface to the mammal heirarchy.
Figure 11.3 Using interfaces to create mammals.

The code for the IWalkable interface is as follows:

interface IWalkable {
    public void walk();
}

The only method in this interface is walk(), which is left to the concrete class to provide the implementation.

class Dog extends Mammal implements IWalkable{
    Walkable walker;
    public void setWalker (Walkable w) {
        this.walker=w;
    }
    public void walk () {System.out.println("I am Walking");};
}

Note that the Dog class extends the Mammal class and implements the IWalkable interface. Also note that the Dog class provides a reference and a constructor that provides the mechanism to inject the dependency.

Walkable walker;
public void setWalker (Walkable w) {
    this.walker=w;
}

In a nutshell, this is what dependency injection is. The Walkable behavior is not created inside the Dog class using the new keyword; it is injected into the Dog class via the parameter list.

Here is the complete example:

class Mammal {
    public void eat () {System.out.println("I am Eating");};
}
interface IWalkable {
    public void walk();
}
class Dog extends Mammal implements IWalkable{
    Walkable walker;
    public void setWalker (Walkable w) {
        this.walker=w;
    }
    public void walk () {System.out.println("I am Walking");};
}
public class TestMammal {
    public static void main(String args[]) {
        System.out.println("Composition over Inheritance");
        System.out.println("
Dog");
        Walkable walker = new Walkable();
        Dog fido = new Dog();
        fido.setWalker(walker);
        fido.eat();
        fido.walker.walk();
    }
}

While this example uses injection by constructor, it is not the only way to handle dependency injection.

Injection by Constructor

One way to inject the Walkable behavior is to create a constructor within the Dog class that, when invoked, will accept an argument from the main application as follows:

class Dog {
    Walkable walker;
    public Dog (Walkable w) {
    this.walker=w;
    }
}

In this approach, the application instantiates a Walkable object and inserts it into the Dog via the constructor.

Walkable walker = new Walkable();

Dog fido = new Dog(walker);
Injection by Setter

Although a constructor will initialize attributes when an object is instantiated, there is often a need to reset values during the lifetime of an object. This is where accessor methods come into play—in the form of setters. The Walkable behavior can be inserted into the Dog class by using a setter, here called setWalker():

class Dog {
    Walkable walker;
    public void setWalker (Walkable w) {
        this.walker=w;
    }
}

As with the constructor technique, the application instantiates a Walkable object and inserts it into the Dog via the setter:

Walkable walker = new Walkable();
Dog fido = new Dog();
fido.setWalker(walker);

Conclusion

Dependency injection decouples your class’s construction from the construction of its dependencies. It is like buying something off the shelf (from a vendor) rather than building it on your own each time.

This plays to the heart of the discussion of Inheritance and composition. It is very important to note that this is simply a discussion. The purpose of this chapter is not necessarily to describe the “optimal” way to design your classes but to get you thinking about the issues associated with deciding between Inheritance and composition. In the next chapter, we explore The SOLID principles of object-oriented design, concepts highly regarded and accepted by the software development community.

References

Martin, Robert, et al. Agile Software Development, Principles, Patterns, and Practices. 2002. Boston: Pearson Education, Inc.

Martin, Robert, et al. Clean Code. 2009. Boston: Pearson Education, Inc.

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

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