Mastering OOP: A Practical Guide To Inheritance, Interfaces, And Abstract Classes

So far as I can tell, it is uncommon to come across educational content in the field of software development which provides an appropriate mixture of theoretical and practical information. If I was to guess why, I assume it is because individuals who focus on theory tend to get into teaching, and individuals who focus on practical information tend to get paid to solve specific problems, using specific languages and tools.

Mastering OOP: A Practical Guide To Inheritance, Interfaces, And Abstract Classes source code. I highly doubt that Android would have become the dominant mobile platform if one needed to implement even a small portion of the 8000+ lines necessary to interact with the system, just to generate a simple window with some text. Inheritance is what allows us to not have to rebuild the Android framework, or whatever platform you happen to be working with, from scratch.

Inheritance Can Be Used For Abstraction

Insofar as it can be used to share implementation across classes, inheritance is relatively simple to understand. However, there is another important way in which inheritance can be used, which is conceptually related to the interfaces and abstract classes which we will be discussing soon.

If you please, suppose for the next little while that an abstraction, used in the most general sense, is a less detailed representation of a thing. Instead of qualifying that with a lengthy philosophical definition, I will try to point out how abstractions work in daily life, and shortly thereafter discuss them expressly in terms of software development.

Suppose you are traveling to Australia, and you are aware that the region you are visiting is host to a particularly high density of inland taipan snakes (they are apparently quite poisonous). You decide to consult Wikipedia to learn more about them by looking at images and other information. By doing so, you are now acutely aware of a particular kind of snake which you have never seen before.

Abstractions, ideas, models, or whatever else you want to call them, are less detailed representations of a thing. It is important that they are less detailed than the real thing because a real snake can bite you; images on Wikipedia pages typically do not. Abstractions are also important because both computers and human brains have a limited capacity to store, communicate, and process information. Having enough detail to use this information in a practical way, without taking up too much space in memory, is what makes it possible for computers and human brains alike to solve problems.

To tie this back into inheritance, all of the three main topics I am discussing here can be used as abstractions, or mechanisms of abstraction. Suppose that in our “Hello World” app’s layout file, we decide to add an ImageView, Button, and ImageButton:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/btnDisplay" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageButton android:id="@+id/imbDisplay" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android:id="@+id/imvDisplay" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>

Also suppose that our Activity has implemented View.OnClickListener to handle clicks:

public class MainActivity extends Activity implements View.OnClickListener { private Button b; private ImageButton ib; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... b = findViewById(R.id.imvDisplay).setOnClickListener(this); ib = findViewById(R.id.btnDisplay).setOnClickListener(this); iv = findViewById(R.id.imbDisplay).setOnClickListener(this); } @Override public void onClick(View view) { final int id = view.getId(); //handle click based on id... }
}

The key principle here is that Button, ImageButton, and ImageViewinherit from the Viewclass. The result is that this function onClickcan receive click events from disparate (though hierarchically related) UI elements by referencing them as their less detailed parent class. This is far more convenient than having to write a distinct method for handling every kind of widget on the Android platform (not to mention custom widgets).

Interfaces And Abstraction

You may have found the previous code example to be a bit uninspiring, even if you understood why I chose it. Being able to share implementation across a hierarchy of classes is incredibly useful, and I would argue that to be the primary utility of inheritance. As for allowing us to treat a set of classes that have a common parent class as equal in type (i.e. as the parent class), that feature of inheritance has limited use.

By limited, I am speaking of the requirement of child classes to be within the same class hierarchy in order to be referenced via, or known as the parent class. In other words, inheritance is a very restrictive mechanism for abstraction. In fact, if I suppose that abstraction is a spectrum that moves between different levels of detail (or information), I might say that inheritance is the least abstract mechanism for abstraction in Java.

Before I proceed to discussing interfaces, I would like to mention that as of Java 8, two features called Default Methods and Static Methods have been added to interfaces. I will discuss them eventually, but for the moment I would like us to pretend that they do not exist. This is in order for me to make it easier to explain the primary purpose of using an interface, which was initially, and arguably still is, the most abstract mechanism for abstraction in Java.

Less Detail Means More Freedom

In the section on inheritance, I gave a definition of the word implementation, which was meant to contrast with another term we will now go into. To be clear, I do not care about the words themselves, or whether you agree with their usage; only that you understand what they conceptually point to.

Whereas inheritance is primarily a tool to share implementation across a set of classes, we might say that interfaces are primarily a mechanism to share behavior across a set of classes. Behavior used in this sense is truly just a non-technical word for abstract methods. An abstract method is a method which does not, in fact cannot, contain a method body:

public interface OnClickListener { void onClick(View v);
}

The natural reaction for me, and a number of individuals whom I have tutored, after first looking at an interface, was to wonder what the utility of sharing only a return type, method name, and parameter list might be. On the surface, it looks like a great way to create extra work for yourself, or whoever else might be writing the class which implementsthe interface. The answer, is that interfaces are perfect for situations where you want a set of classes to behave in the same manner (i.e. they possess the same public abstract methods), but you expect them to implement that behavior in different ways.

To take a simple but relevant example, the Android platform possesses two classes which are primarily in the business of creating and managing part of the user interface: Activity and Fragment. It follows that these classes will very often have the requirement of listening to events which pop up when a widget is clicked (or otherwise interacted with by a user). For argument’s sake, let us take a moment to appreciate why inheritance will almost never solve such a problem:

public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... }
}

Not only would making our Activities and Fragments inherit from OnClickManagermake it impossible to handle events in a different manner, but the kicker is that we could not even do that if we wanted to. Both Activity and Fragment already extend a parent class, and Java does not allow multiple parent classes. So our problem is that we want a set of classes to behave the same way, but we must have flexibility on how the class implements that behavior. This brings us back to the earlier example of the View.OnClickListener:

public interface OnClickListener { void onClick(View v);
}

This is the actual source code (which is nested in the Viewclass), and these few lines allow us to ensure consistent behavior across different widgets (Views) and UI controllers (Activities, Fragments, etc.).

Abstraction Promotes Loose-Coupling

I have hopefully answered the general question about why interfaces exist in Java; among many other languages. From one perspective, they are just a means of sharing code between classes, but they are deliberately less detailed in order to allow for different implementations. But just as inheritance can be used both as a mechanism for sharing code and abstraction (albeit with restrictions on class hierarchy), it follows that interfaces provide a more flexible mechanism for abstraction.

In an earlier section of this article, I introduced the topic of loose/tight-coupling by analogy of the difference between using nails and screws to build some kind of structure. To recap, the basic idea is that you will want to use screws in situations where changing the existing structure (which can be a result of fixing mistakes, design changes, and so forth) is likely to happen. Nails are fine to use when you just need to fasten parts of the structure together and are not particularly worried about taking them apart in the near future.

Nails and screws are meant to be analogous to concrete and abstract references (the term dependencies also applies) between classes. Just so there is no confusion, the following sample will demonstrate what I mean:

class Client { private Validator validator; private INetworkAdapter networkAdapter; void sendNetworkRequest(String input){ if (validator.validateInput(input)) { try { networkAdapter.sendRequest(input); } catch (IOException e){ //handle exception } } }
} class Validator { //...validation logic boolean validateInput(String input){ boolean isValid = true; //...change isValid to false based on validation logic return isValid; }
} interface INetworkAdapter { //... void sendRequest(String input) throws IOException;
}

Here, we have a class called Client which possesses two kinds of references. Notice that, assuming Clientdoes not have anything to do with creating its references (it really should not), it is decoupled from the implementation details of any particular network adapter.

There are a few important implications of this loose coupling. For starters, I can build Clientin absolute isolation of any implementation of INetworkAdapter. Imagine for a moment that you are working in a team of two developers; one to build the front end, one to build the back end. As long as both developers are kept aware of the interfaces which couple their respective classes together, they can carry on with the work virtually independently of one another.

Secondly, what if I were to tell you that both developers could verify that their respective implementations functioned properly, also independently of each other’s progress? This is very easy with interfaces; just build a Test Double which implementsthe appropriate interface:

class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); }
}

In principle, what can be observed is that working with abstract references opens the door to increased modularity, testability, and some very powerful design patterns such as the Facade Pattern, Observer Pattern, and more. They can also allow developers to find a happy balance of designing different parts of a system based on behavior (Program To An Interface), without getting bogged down in implementation details.

A Final Point On Abstractions

Abstractions do not exist in the same way as a concrete thing. This is reflected in the Java Programming language by the fact that abstract classes and interfaces may not be instantiated.

For example, this would definitely not compile:

public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { //ERROR x2: Foo f = new Foo(); Bar b = new Bar() } private abstract class Foo{} private interface Bar{} }

In fact, the idea of expecting an unimplemented interface or abstract class to function at runtime makes as much sense as expecting a UPS uniform to float around delivering packages. Something concrete must be behind the abstraction for it to be of utility; even if the calling class does not need to know what is actually behind abstract references.

Abstract Classes: Putting It All Together

If you have made it this far, then I am happy to tell you that I have no more philosophical tangents or jargon phrases to translate. Simply put, abstract classes are a mechanism for sharing implementation and behavior across a set of classes. Now, I will admit straight away that I do not find myself using abstract classes all that often. Even so, my hope is that by the end of this section you will know exactly when they are called for.

Workout Log Case Study

Roughly a year into building Android apps in Java, I was rebuilding my first Android app from scratch. The first version was the kind of horrendous mass of code that you would expect from a self-taught developer with little guidance. By the time I wanted to add new functionality, it became clear that the tightly coupled structure I had built exclusively with nails, was so impossible to maintain that I must rebuild it entirely.

The app was a workout log that was designed to allow easy recording of your workouts, and the ability to output the data of a past workout as a text or image file. Without getting into too much detail, I structured the data models of the app such that there was a Workoutobject, which comprised of a collection of Exerciseobjects (among other fields which are irrelevant to this discussion).

As I was implementing the feature for outputting workout data to some kind of visual medium, I realized that I had to deal with a problem: Different kinds of exercises would require different kinds of text outputs.

To give you a rough idea, I wanted to change the outputs depending on the type of exercise like so:

  • Barbell: 10 REPS @ 100 LBS
  • Dumbbell: 10 REPS @ 50 LBS x2
  • Bodyweight: 10 REPS @ Bodyweight
  • Bodyweight +: 10 REPS @ Bodyweight + 45 LBS
  • Timed: 60 SEC @ 100 LBS

Before I proceed, note that there were other types (working out can become complicated) and that the code I will be showing has been trimmed down and changed to fit nicely into an article.

In keeping with my definition from before, the goal of writing an abstract class, is to implement everything (even state such as variables and constants) which is shared across all child classes in the abstract class. Then, for anything which changes across said child classes, create an abstract method:

abstract class Exercise { private final String type; protected final String name; protected final int[] repetitionsOrTime; protected final double[] weight; protected static final String POUNDS = "LBS"; protected static final String SECONDS = "SEC "; protected static final String REPETITIONS = "REPS "; public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) { this.type = type; this.name = name; this.repetitionsOrTime = repetitionsOrTime; this.weight = weight; } public String getFormattedOutput(){ StringBuilder sb = new StringBuilder(); sb.append(name); sb.append("\n"); getSetData(sb); sb.append("\n"); return sb.toString(); } /** * Append data appropriately based on Exercise type * @param sb - StringBuilder to Append data to */ protected abstract void getSetData(StringBuilder sb); //...Getters
} 

I may be stating the obvious, but if you have any questions about what should or should not be implemented in the abstract class, the key is to look at any part of the implementation which has been repeated in all child classes.

Now that we have established what is common amongst all exercises, we can begin to create child classes with specializations for each kind of String output:

Barbell Exercise:
class BarbellExercise extends Exercise { public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append("\n"); } }
}
Dumbbell Exercise:
class DumbbellExercise extends Exercise { private static final String TIMES_TWO = "x2"; public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append(TIMES_TWO); sb.append("\n"); } }
}
Bodyweight Exercise:
class BodyweightExercise extends Exercise { private static final String BODYWEIGHT = "Bodyweight"; public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(BODYWEIGHT); sb.append("\n"); } }
}

I am certain that some astute readers will find things which could have been abstracted out in a more efficient manner, but the purpose of this example (which has been simplified from the original source) is to demonstrate the general approach. Of course, no programming article would be complete without something which can be executed. There are several online Java compilers which you may use to run this code if you want to test it out (unless you already have an IDE):

public class Main { public static void main(String[] args) { //Note: I actually used another nested class called a "Set" instead of an Array //to represent each Set of an Exercise. int[] reps = {10, 10, 8}; double[] weight = {70.0, 70.0, 70.0}; Exercise e1 = new BarbellExercise( "Barbell", "Barbell Bench Press", reps, weight ); Exercise e2 = new DumbbellExercise( "Dumbbell", "Dumbbell Bench Press", reps, weight ); Exercise e3 = new BodyweightExercise( "Bodyweight", "Push Up", reps, weight ); System.out.println( e1.getFormattedOutput() + e2.getFormattedOutput() + e3.getFormattedOutput() ); }
}

Executing this toy application yields the following output:
Barbell Bench Press

10 REPS @ 70.0LBS
10 REPS @ 70.0LBS
8 REPS @ 70.0LBS Dumbbell Bench Press
10 REPS @ 70.0LBSx2
10 REPS @ 70.0LBSx2
8 REPS @ 70.0LBSx2 Push Up
10 REPS @ Bodyweight
10 REPS @ Bodyweight
8 REPS @ Bodyweight

Further Considerations

Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementation, as opposed to behavior. These features are known as Default Methods and Static Methods.

I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.

I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance. A common pattern in Java is to write what is known as a Utility class, which is a simple classcontaining the requisite implementation in a static method:

public class TimeConverterUtil { /** * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate * format such as 12, 30 -> 12:30 pm */ public static String convertTime (int hour, int minute){ String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute); DateFormat f1 = new SimpleDateFormat("HH:mm"); Date d = null; try { d = f1.parse(unformattedTime); } catch (ParseException e) { e.printStackTrace(); } DateFormat f2 = new SimpleDateFormat("h:mm a"); return f2.format(d).toLowerCase(); }
}

Using this static method in an external class (or another static method) looks like this:


public class Main { public static void main(String[] args){ //... String time = TimeConverterUtil.convertTime(12, 30); //... }
}

Cheat Sheet

We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.

I Want A Set Of Child Classes To Share Implementation

Classic inheritance, which requires a child class to inherit from a parent class, is a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent class, is to see whether it is repeated in a number of different classes line for line. The acronym DRY (Don’t Repeat Yourself) is a good mnemonic device to watch out for this situation.

While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent class, which provides a limited degree of abstraction.

I Want A Set Of Classes To Share Behavior

Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior), but you do not expect the implementation of that behavior to be repeated across inheritors.

By definition, Java interfaces may not contain any implementation (except for Default and Static Methods), but any class which implements an interface, must supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy.

I Want A Set Of Child Classes To Share Behavior And Implementation

Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract class, and anything which requires flexibility may be specified as an abstract method.

Smashing Editorial (dm, il)

Posted by Web Monkey