Lesson Tuesday

Our applications have grown substantially in size! Their overall functionality is simple and straightforward, but hardly matches the functionality of huge applications like Wordpress for our Blog, or Basecamp for our To Do List, or Yelp for our Restaurant API.

Wordpress, for instance, powers a substantial percentage of websites worldwide. Its blogging platform supports categories, posts, pages, plugins, themes, extensions, and countless other capabilities. Basecamp is also one of the most widely used Project Management software applications available. Yelp offers a plethora of filters, endpoints, and serves a massive amount of data to API clients worldwide.

Yet, all of these massive applications started where we are now: Simple data models, CRUD functionality, basic data persistence, and a little routing. They've just grown since then to incorporate many more data models, and code to handle everything from authentication, to communicating plugins, Google, Facebook, and more. A large project like this can literally contain thousands of independent moving parts. Many of these classes likely look very similar to others, with only subtle differences in properties or methods.

Our apps, like theirs, will grow in complexity. As developers, team members and project managers, we will face the challenge of keeping these complex and multifaceted applications organized. After all, project teams shift all the time: New members are added, others leave the company or move to a different team. We need to account for fluctuations. Therefore, there are many approaches to keep our code clean, DRY and nicely-refactored.

We can:

  • Agree upon standard naming conventions.
  • Track our code with version control.
  • Follow a strict BDD process where testing always comes first.
  • Mark redundant code for deletion to prevent a cluttered codebase.
  • Establish refactoring/review protocol to deal with code rot.
  • Use comments to explain confusing sections of code.
  • Emphasize loose coupling (making code as modular as possible so it can be upgraded easily, see separation of backend/frontend, etc).

Another technique used in many languages is to employ Software Design Patterns, as you already learned about in our lessons on the DAO pattern, that make it very clear what kind of functionality a data model must implement. One of these approaches we have already addressed casually in class are Interfaces. We already know these are a way of ensuring any class we create agrees to provide a minimum standard of code necessary to run correctly.

In this lesson, we'll dig deeper into interfaces and explore them more in depth. In simpler terms, an interface is a group of methods multiple different classes may inherit.

Demonstration

For instance, say we had an interface called Noisy:

public interface Noisy {
    void angryNoise();
    void happyNoise();  
}

The Noisy interface declared two methods: One for making a happy noise, and one for making an angry noise. Notice it doesn't say how that method should look, or what functionality it contains, it just says "Hey, this functionality should exist.

Now, we know many things are capable of being noisy. So, many classes can implement this interface. For instance, an Elephant could be pretty noisy:

public class Elephant implements Noisy {

    @Override
    public void angryNoise() {
         System.out.println("rumble");
    }

    @Override
    public void happyNoise() {
         System.out.println("trumpet");
    }
}

And a Dog can be pretty loud, too:

public class Dog implements Noisy {

    @Override
    public void angryNoise() {
         System.out.println("growl");
    }

    @Override
    public void happyNoise() {
         System.out.println("bark");
    }
}

However, despite both Dog and Elephant classes implementing the same interface with the same methods, Dogs and Elephants make different noises; so the interface's methods are personalized to each animal's class. This doesn't make the code shorter, per se, but it clarifies that each of these classes MUST provide this functionality.

When we say, a class "implements an interface", we are saying "this class needs to provide the methods of the interface that it implements."

Interfaces allow us to separate what a class should be able to do in terms of functionality (make angry noises, for instance) from how it does it (either rumbling or growling, depending on the animal). Notice that the interface itself does not contain any logic in any of the methods it outlines. It only lists the method signature (what was that again? Look it up!) of each, which means that it declares how many arguments the method should take and of which type they should be.

Interfaces are commonly likened to "contracts" that the developer "signs". Because whenever a class implements an interface it is obligated to include every method outlined in the interface. If it does not, the class won't even compile. Therefore, implementing an interface is like signing a contract saying you promise this class will contain every method listed in the interface.

This helps us keep our code organized, by introducing a check-and-balance system. If I know I am writing a class that should be able to a.) make an angry noise and b.) make a happy noise, I can tell my class that I am implementing the Noisy interface. If I later forget to write those methods, or write them with a different number of type of arguments, I will receive a compiler error.

Rules for Interfaces

Beyond the requirement to include every method, there are several other rules to consider when creating and using interfaces:

  • Interface methods can only be public (and are by default).
  • Member variables in the interface can only be public, static, and final (and are by default).
  • A class must implement all methods of an interface, unless the class is declared as abstract (we won't get into abstract classes at this point. Just remember that there is one exception to this rule.). That means, using the example above, any other People or Animal classes that implement the interface Noisy must have methods for both happyNoise() and angryNoise() defined in their class.
  • When defining the methods implemented by the interface into a class, you should use the @Override annotation.
    • Note: Often, code is able to compile without this annotation. However, it's considered best practice to include it so other developers can easily see which methods are inherited and which are unique to the class. Additionally, telling the compiler this method is overridden from another source will prompt it to alert you if it doesn't actually see a method of that same signature. This provides yet another layer of error-catching and bug-prevention that makes Java so notoriously stable.
  • Interfaces are fixed. When you change an interface, you have to change every class implementing that interface.
  • Interfaces cannot be instantiated; that is, they cannot create objects. Therefore, they do not contain constructors either.
  • An interface can extend multiple other interfaces.

For more information, read the Oracle documentation on Creating Interfaces.

Benefits of Implementing Interfaces

But why would we want to do this? When we inherit abstract classes we're inheriting fully-functional methods complete with the logic required to execute them. The interfaces just contain empty methods! Well, there's several reasons:

Catching Mistakes

Interfaces can catch errors. Because you are required to override and include every method defined by an interface, this ensures the compiler will catch you if you accidentally forget one.

This may seem silly; but imagine a Java application with hundreds and hundreds of classes. If we know ahead of time that many of them will require the same 7 methods (even if the logic for these methods will differ between classes), we can define an interface outlining these methods. Then, we can inherit it wherever we know these 7 methods will be required.

If we accidentally forget any of them; the compiler will let us know immediately.

The value of implementing interfaces becomes even more apparent when we use Object inheritance, which we will learn about next.