Understanding Dependencies

Jakob Jenkov
Last update: 2014-05-26

You can't read a good book on OOP without it mentioning dependencies, loose coupling etc. and with good reason. Understanding dependencies is important when doing object oriented design of API's and applications. However, in my opinion the subject could be investigated further than many books do. That is the purpose of this text. If you are an experienced OO developer you may already know much or all of what is written here. I believe though, that many developers will still be able to pick up something from this text.

What is a Dependency?

Whenever a class A uses another class or interface B, then A depends on B. A cannot carry out it's work without B, and A cannot be reused without also reusing B. In such a situation the class A is called the "dependant" and the class or interface B is called the "dependency". A dependant depends on its dependencies.

Two classes that uses each other are called "coupled". The coupling between classes can be loose or tight, or somewhere in between. The tightness of a coupling is not binary. It is not either "loose" or "tight". The degrees of tightness are continuous, not discrete. You can also characterize dependencies as "strong" or "weak". A tight coupling leads to strong dependencies, and a loose coupling leads to weak dependencies, or even no dependencies in some situations.

Dependencies, or couplings, are directional. That A depends on B doesn't mean that B also depends on A.

Why are Dependencies Bad?

Dependencies are bad because they decrease reuse. Decreased reuse is bad for many reasons. Often reuse has positive impact on development speed, code quality, code readability etc.

How dependencies can hurt reuse is best illustrated by an example:

Imagine you have a class CalendarReader that is able to read a calendar event list from an XML file. The implementation of CalendarReader is sketched below:

public class CalendarReader {
    public List readCalendarEvents(File calendarEventFile){
        //open InputStream from File and read calendar events.
    }
}

The method readCalendarEvents takes a File object as parameter. Thus this method depends on the File class. This dependency on the File class means that the CalendarReader is capable only of reading calendar events from local files in the file system. It cannot read calendar event files from a network connection, a database or a from a resource on the classpath. You can say that the CalendarReader is tightly coupled to the File class and thereby the local file system.

A less tightly coupled implementation would be to exchange the File parameter with an InputStream parameter, as sketched below:

public class CalendarReader {
    public List readCalendarEvents(InputStream calendarEventFile){
        //read calendar events from InputStream
    }
}

As you may know, an InputStream can be obtained from either a File object, a network Socket, a URLConnection class, a Class object (Class.getResourceAsStream(String name)), a column in a database via JDBC etc. Now the CalendarReader is not coupled to the local file system anymore. It can read calendar event files from many different sources.

With the InputStream version of the readCalendarEvents() method the CalendarReader has become more reusable. The tight coupling to the local file system has been removed. Instead it has been replaced with a dependency on the InputStream class. The InputStream dependency is more flexible than the File class dependency, but that doesn't mean that the CalendarReader is 100% reusable. It still cannot easily read data from a NIO Channel, for instance.

 

Dependency Types

A dependency isn't just a dependency. There are several types of dependencies. Each type leads to more or less flexibility in the code. The dependency types are:

  • Class Dependencies
  • Interface Dependencies
  • Method / Field Dependencies

Class dependencies are dependencies on classes. For instance, the method in the below code box takes a String as parameter. Therefore it depends on the String class.

public byte[] readFileContents(String fileName){
    //open the file and return the contents as a byte array.
}

Interface dependencies are dependencies on interfaces. For instance, the method in the below code box takes a CharSequence as parameter. CharSequence is a standard Java interface (in the java.lang package). Both CharBuffer, String, StringBuffer, and StringBuilder implements the CharSequence interface, so instances of any of these classes can be used as parameter to the method.

public byte[] readFileContents(CharSequence fileName){
    //open the file and return the contents as a byte array.
}

Method or field dependencies are dependencies on concrete methods or fields of an object. It doesn't matter what the class of the object is, or what interfaces it implements, as long as it has a method or field of the required type. The following method illustrates a method dependency. The method depends on a method called "getFileName" in the class of the object given as parameter (fileNameContainer). Note, the dependency isn't visible from the method declaration!

public byte[] readFileContents(Object fileNameContainer){

    Method method   = fileNameContainer
                          .getClass()
                          .getMethod("getFileName", null);

    String fileName = method.invoke(fileNameContainer, null);

    //open the file and return the contents as a byte array.
}

Method or field dependencies are common in API's that use reflection to obtain it's goals. For instance, Butterfly Persistence uses reflection to detect getters and setters of a class. Without getters and setters Butterfly Persistence cannot read and write objects of that class to and from database. In that way Butterfly Persistence depends on getters and setters. Hibernate (a similar ORM API) can use either getters and setters, or access the fields directly, also via reflection. That way Hibernate has either method or field dependencies.

Method (or "function") dependencies can also be seen in languages that support function pointers or method pointers to be passed as parameters to other methods. For instance, C# Delegates.

 

Additional Dependency Characteristics

Dependencies have other important characteristics than just the type. Dependencies can be compile-time, runtime, visible, hidden, direct, indirect, contextual etc. These additional dependency characteristics will be covered in the following sections.

Interface Implementation Dependencies

If a class A depends on an interface I, then A does not depend on the concrete implementation of I. But, A depends on some implementation of I. A cannot carry out its work without some implementation of I. Therefore, whenever a class depends on an interface, that class also depends on an implementation.

The more methods an interface has, the less chance there is that developers will provide their own implementation for that interface, unless they are required to. Therefore, the more methods an interface has the larger the probability is that developers will just stick to the default implementation of that interface. In other words, the larger and more complex an interface becomes, the tighter it is coupled to its default implementation!

Because of interface implementation dependencies, you should not add functionality to an interface blindly. If the functionality could be encapsulated in its own component, behind its own interface, you should do so.

Below is an example of what this means. The code example shows a tree node for a hierarchical tree structure.

public interface ITreeNode {
    public void            addChild(ITreeNode node);
    public List<ITreeNode> getChildren();
    public ITreeNode       getParent();
}

Imagine that you want to be able to count descendents of a given node. At first you might be tempted to add a countDescendents() method to the ITreeNode interface. However, if you do so anyone who wants to implement the ITreeNode interface will also have to implement the countDescendent() method.

Instead you could implement a DescendentCounter class that can traverse an ITreeNode instance an count all descendents of that instance. This DescendentCounter can be reused with different implementations of the ITreeNode interface. You have just saved your users the trouble of implementing the countDescendents() method, even if they need to implement the ITreeNode interface!

Compile-time and Runtime Dependencies

A dependency that can be resolved at compile time is a compile-time dependency. A dependency that cannot be resolved until runtime is a runtime dependency. Compile-time dependencies tend to be easier for developers to see than runtime dependencies, but sometimes runtime dependencies can be more flexible. For instance, Butterfly Persistence detects getters and setters of a class at runtime, and maps them automatically to the database table of that class. This is a very easy way to map classes to database tables. However, to do so Butterfly Persistence depends on properly named getters and setters.

Visible and Hidden Dependencies

A visible dependency is a dependency that developers can see from a class's interface. If a dependency cannot be seen from the class's interface, it is a hidden dependency.

In the earlier examples, the String and CharSequence dependencies of the readFileContents() methods are visible dependencies. They are visible from the method declaration, which is a part of the class's interface. The method dependencies of the readFileContents() method that take an Object as parameter, are invisible. You cannot see from the interface if the readFileContents() method calls the fileNameContainer.toString() to obtain the file name, or as it actually does, calls the getFileName() method.

Another example of a hidden dependency is the dependency on a static singleton, or static methods from within a method. You cannot see from the interface if a class depends on static methods or static singletons.

As you can imagine hidden dependencies can be bad. They are hard to detect for developers using the classes with the hidden dependencies. They can only see them by inspecting the code.

This is not the same as saying that you should never use hidden dependencies. Hidden dependencies are often the result of providing sensible defaults. For instance, in this example it may not be a problem:

public class MyComponent{

  protected MyDependency dependency = null;

   public MyComponent(){
       this.dependency = new MyDefaultImpl();
       }

  public MyComponent(MyDependency dependency){
    this.dependency = dependency;
  }
}

MyComponent has a hidden dependency on MyDefaultImpl as you can see in the first constructor. But if MyDefaultImpl does not have any dangerous side effects, then this hidden dependency is not dangerous.

Direct and Indirect Dependencies

A dependency can be either direct or indirect dependency. If a class A uses a class B then A has a direct dependency on B. If A depends on B, and B depends on C, then A has an indirect dependency on C. If you cannot use A without B, and cannot use B without C, then you cannot use A without C either.

Indirect dependencies are also called chained dependencies, or "transitive dependencies" (in "Better, Faster, Lighter Java" by Bruce A. Tate and Justin Gehtland)

Unnecessarily Extensive Dependencies

Sometimes components depend on more information than they need to carry out their job. For instance, imagine a login component for a web application. The login component needs only a user name and a password, and will return the user object, if any, that matches these. The interface could look like this:

public class LoginManager{
    
  public User login(HttpServletRequest request){
    String user     = request.getParameter("user");
    String password = request.getParameter("password");

    //read user and return it.
  }
}

Calling the component would look like this:

LoginManager loginManager = new LoginManager();
User         user         = loginManager.login(request);

It looks simple, right? And even if the login method needs more parameters, you don't need to change the calling code.

But the login method now has what I call an "unnecessarily extensive dependency" on the HttpServletRequest interface. It depends on more than it needs to carry out its work. The LoginManager only needs a user name and a password to lookup a user, but takes a HttpServletRequest as parameter for the login method. An HttpServletRequest contains a lot more information than the LoginManager needs.

The dependency on the HttpServletRequest interface causes two problems:


  1. The LoginManager cannot be reused (called) without an HttpServletRequest instance. This can make unit testing of the LoginManager harder. You will need a mock HttpServletRequest instance, which could be a lot of work.

  2. The LoginManager requires the names of the user name and password parameters to be called "user" and "password". This is also an unnecessary dependency.

A much better interface for the LoginManager's login method would be:

public User login(String user, String password){
    //read user and return it.
}

But look what happens to the calling code now:

LoginManager loginManager = new LoginManager();
User user = loginManager.login(
    request.getParameter("user"),
    request.getParameter("password"));

It gets more complex. For a component that takes 5 request parameters to do its job, it would look even worse. This is the main reason that developers create unnecessarily extensive dependencies. To simplify the calling code.

Local and Context Dependencies

When developing applications it is normal to break the application into minor components. Some of these components are general purpose components, which could be useful in other applications too. Other components are application specific and are not of any use outside of the application.

For a general purpose component any classes belonging to the component (or API) are "local". The rest of the application is the "context". If a general purpose component depends on application specific classes, this is called a "context dependency". Context dependencies are bad because it makes the general purpose component unusable outside of the application too.

It is tempting to think that only a bad OO designer would create context dependencies, but this is not true. Context dependencies often occur when developers try to simplify the design of their application. A good example of this are request processing applications, like message queue connected applications or web applications.

Imagine an application that receives request in XML form, processes the requests and sends back a result in XML form. During processing the XML request is processed by several individual components. Each of these components need different information, some of that information being produced by earlier components. It is very tempting to gather the XML file and all related processing inside some kind of request object, which is passed to all components in the processing sequence. Processing components can then read information from that request object, and attach further information too it for use by later processing components. By taking this request object as parameter, each of the request processing components depend on this request object. The request object is application specific, thus causing each request processing component to have a context dependency.

 

Standard vs. Custom Class/Interface Dependencies

In many situations it is better for a component to depend on a class or interface from the standard Java (or C# etc.) packages. These classes and interfaces are always available to anyone, making it easier to satisfy these component dependencies. In addition the these classes are less likely to change and cause your application to fail compilation.

In some situations though, depending on JDK classes is not the best thing to do. For example, lets say a method needs 4 strings for its configuration. Then you method takes 4 strings as parameters. An example could be the driver name, database url, user name and password needed for a database connection. If all of these 4 strings are always used together, it may be clearer for the users of that method if you group the 4 strings into a class, and pass instances of the class around, instead of the 4 strings.

 

Summary

By now you have seen several different types and characteristics of dependencies. In general interface dependencies are preferable over class dependencies. In some situations though, you may find that a class dependency may be preferable to an interface dependency. Method and field dependencies can be very useful, but remember that they are also typically hidden dependencies, and hidden dependencies makes it harder for users of your component to detect it, and thereby satisfy it.

Interface implementation dependencies are more common than you would think. I have seen them in many applications and API's. Try to limit them as much as possible, by keeping interfaces small. At least the interfaces you may expect the user of the component to implement themselves. Move additional functionality (like counting etc.) to external components that take an instance of the interface in question as parameter.

Personally I prefer compile-time dependencies over runtime dependencies, but in some cases runtime dependencies are more elegant. For instance Mr. Persister uses runtime dependencies on getters and setters which frees your pojos from implemententing a persistence interface. Runtime dependencies can thus sometimes be less invasive than compile-time dependencies.

Hidden dependencies can be dangerous, but as runtime dependencies are sometimes also hidden dependencies, you may not always have a choice.

Remember that even if a component does not have a direct dependency on another component it may still have an indirect dependency on it. Although typically less limiting indirect dependencies are still dependencies.

Try to avoid unnecessarily extensive dependencies. Keep in mind though, that unnecessarily extensive dependencies often occur when you group multiple parameters into a class. This is a common refactoring usually carried out to make the code look simpler, but as you have now seen, this can also cause unnecessarily extensive dependencies.

A component that is to be reused across many different contexts, should not have any context dependencies. Meaning it should not depend on any other components in the context in which it is intially developed and integrated.

This text has only described dependencies. It does not tell you what to do about them. Other texts on this training site will delve into this topic.

Jakob Jenkov

Featured Videos

Java Generics

Java ForkJoinPool

P2P Networks Introduction



















Close TOC
All Tutorial Trails
All Trails
Table of contents (TOC) for this tutorial trail
Trail TOC
Table of contents (TOC) for this tutorial
Page TOC
Previous tutorial in this tutorial trail
Previous
Next tutorial in this tutorial trail
Next