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

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

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

Ryan Kay

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.

This is, of course, a broad generalization, but if we accept it briefly for arguments sake, it follows that many people (by no means all people) who take on the role of teacher, tend to be either poor at, or utterly incapable of explaining the practical knowledge relevant to a particular concept.

In this article, I will do my best to discuss three core mechanisms which you will find in most Object Oriented Programming (OOP) languages: Inheritance, interfaces (a.k.a. protocols), and abstract classes. Rather than giving you technical and complex verbal explanations of what each mechanism is, I will do my best to focus on what they do, and when to use them.

However, before addressing them individually, I would like to briefly discuss what it means to give a theoretically sound, yet practically useless explanation. My hope is that you can use this information to help you sift through different educational resources and to avoid blaming yourself when things do not make sense.

Different Degrees Of Knowing

Knowing Names

Knowing the name of something is arguably the most shallow form of knowing. In fact, a name is only generally useful to the extent that it either is commonly used by many people to refer to the same thing and/or it helps to describe the thing. Unfortunately, as anyone who has spent time in this field has discovered, many people use different names for the same thing (e.g. interfaces and protocols), the same names for different things (e.g. modules and components), or names which are esoteric to the point of being absurd (e.g. Either Monad). Ultimately, names are just pointers (or references) to mental models, and they can be of varying degrees of usefulness.

To make this field even more difficult to study, I would hazard a guess that for most individuals, writing code is (or at least was) a very unique experience. Even more complicated is understanding how that code is ultimately compiled into machine language, and represented in physical reality as a series of electrical impulses changing over time. Even if one can recall the names of the processes, concepts, and mechanisms that are employed in a program, there is no guarantee that the mental models which one creates for such things are consistent with the models another individual; let alone whether they are objectively accurate.

It is for these reasons, alongside the fact that I do not have a naturally good memory for jargon, that I consider names to be the least important aspect of knowing something. That is not to say that names are useless, but I have in the past learned and employed many design patterns in my projects, only to learn of the commonly used name months, or even years later.

Knowing Verbal Definitions And Analogies

Verbal definitions are the natural starting point for describing a new concept. However, as with names, they can be of varying degrees of usefulness and relevance; much of that depending on what the end goals of the learner are. The most common problem I see in verbal definitions is assumed knowledge typically in the form of jargon.

Suppose for example, that I was to explain that a thread is very much like a process, except that threads occupy the same address space of a given process. To someone who is already familiar with processes and address spaces, I have essentially stated that threads can be associated with their understanding of a process (i.e. they possess many of the same characteristics), but they can be differentiated based on a distinct characteristic.

To someone who does not possess that knowledge, I have at best not made any sense, and at worst caused the learner to feel inadequate in some way for not knowing the things I have assumed they should know. In fairness, this is acceptable if your learners really ought to possess such knowledge (such as teaching graduate students or experienced developers), but I consider it to be a monumental failure to do so in any introductory level material.

Often it is very difficult to provide a good verbal definition of a concept when it is unlike anything else the learner has seen before. In this case, it is very important for the teacher to select an analogy which is likely to be familiar to the average person, and also relevant insofar as it conveys many of the same qualities of the concept.

For example, it is critically important for a software developer to understand what it means when software entities (different parts of a program) are tightly-coupled or loosely-coupled. When building a garden shed, a junior carpenter may think that it is faster and easier to put it together using nails instead of screws. This is true up until the point at which a mistake is made, or a change in the design of the garden shed necessitates rebuilding part of the shed.

At this point, the decision to use nails to tightly-couple the parts of the garden shed together, has made the construction process as a whole more difficult, likely slower, and extracting nails with a hammer runs the risk of damaging the structure. Conversely, screws can take a bit of extra time to assemble, but they are easy to remove and pose little risk of damaging nearby parts of the shed. This is what I mean by loosely-coupled. Naturally, there are cases where you really just need a nail, but that decision ought to be guided by critical thinking and experience.

As I will discuss in detail later on, there are different mechanisms for connecting parts of a program together which provide varying degrees of coupling; just like nails and screws. While my analogy may have helped you understand what this critically important term means, I have not given you any idea of how to apply it outside of the context of building a garden shed. This leads me to the most important kind of knowing, and the key to deeply understanding vague and difficult concepts in any field of inquiry; although we will stick to writing code in this article.

Knowing In Code

In my opinion, strictly regarding software development, the most import form of knowing a concept comes from being able to use it in the working application code. This form of knowing can be attained simply by writing lots of code and solving many different problems; jargon names and verbal definitions need not be included.

In my own experience, I recall solving the problem of communicating with a remote database, and a local database through a single interface (you will know what that means soon if you do not already); rather than the client (whatever class which talks to the interface) needing to call the remote and local (or even a test database) explicitly. In fact, the client had no idea what was behind the interface, so I did not need to change it regardless of if it was running in a production app or a test environment. About a year after I solved this problem, I came across the term “Facade Pattern”, and not long after the term “Repository Pattern”, which are both names that people use for the solution described prior.

All of this preamble is to hopefully illuminate some of the flaws which are most often made in explaining topics such as inheritance, interfaces, and abstract classes. Of the three, inheritance is likely the simplest one to both use and understand. In my experience both as a student of programming, and a teacher, the other two are almost invariably an issue to learners unless very special attention is paid to avoiding the mistakes discussed earlier. From this point on, I will do my best to make these topics as simple as they should be, but no simpler.

A Note On The Examples

Being most fluent in Android mobile application development myself, I will use examples taken from that platform so that I may teach you about building GUI applications at the same time as introducing language features of Java. However, I will not be going into so much detail that the examples ought to be unintelligible by someone with a cursory understanding of Java EE, Swing or JavaFX. My ultimate goal in discussing these topics is to help you to understand what they mean in the context of solving a problem in just about any sort of application.

I would also like to warn you, dear reader, that at times it may seem like I am being needlessly philosophical and pedantic about specific words and their definitions. The reason for this is that there truly is a deep philosophical underpinning required to understand the difference between something which is concrete (real), and something which is abstract (less detailed than a real thing). This understanding applies to many things outside of the field of computing, but it is of particularly high importance for any software developer to grasp the nature of abstractions. In any case, if my words fail you, the examples in code will hopefully not.

Inheritance And Implementation

When it comes to building applications with a graphical user interface (GUI), inheritance is arguably the most important mechanism for making it possible to quickly build an application.

Although there is a lesser understood benefit to using inheritance to be discussed later, the primary benefit is to share implementation between classes. This word “implementation”, at least for the purposes of this article, has a distinct meaning. To give a general definition of the word in English, I might say that to implement something, is to make it real.

To give a technical definition specific to software development, I might say that to implement a piece of software, is to write concrete lines of code which satisfy the requirements of said piece of software. For example, suppose I am writing a sum method: private double sum(double first, double second){

private double sum(double first, double second){
        //TODO: implement
}

The snippet above, even though I have made it as far as writing a return type (double) and a method declaration which specifies the arguments (first, second) and the name which can be used to call said method (sum), it has not been implemented. In order to implement it, we must complete the method body like so:

private double sum(double first, double second){
        return first + second;
}

Naturally, the first example would not compile, but we will see momentarily that interfaces are a way in which we can write these sorts of unimplemented functions without errors.

Inheritance In Java

Presumably, if you are reading this article, you have used the extends Java keyword at least once. The mechanics of this keyword are simple and most often described using examples to do with different kinds of animals or geometric shapes; Dogand Catextend Animal, and so forth. I will assume that I do not need to explain rudimentary type theory to you, so let us get right into the primary benefit of inheritance in Java, via the extendskeyword.

Building a console-based “Hello World” application in Java is very simple. Assuming you possess a Java Compiler (javac) and runtime environment (jre), you can write a class which contains a main function like so:

public class JavaApp{
     public static void main(String []args){
        System.out.println("Hello World");
     }
}

Building a GUI application in Java on almost any of its main platforms (Android, Enterprise/Web, Desktop), with a bit of help from an IDE to generate the skeleton/boilerplate code of a new app, is also relatively easy thanks to the extendskeyword.

Suppose that we have an XML Layout called activity_main.xml (we typically build user interfaces declaratively in Android, via Layout files) containing a TextView(like a text label) called tvDisplay:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <TextView
        android:id="@+id/tvDisplay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        />


</FrameLayout>

Also, suppose that we would like tvDisplayto say “Hello World!” To do so, we simply need to write a class which uses the extendskeyword to inherit from the Activityclass:

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        ((TextView)findViewById(R.id.tvDisplay)).setText("Hello World");
    }

The effect of inheriting the implementation of the Activityclass can be best appreciated by taking a quick look at its 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)

Testing Made Easier Via Framework Minimalism And Software Architecture

Testing Made Easier Via Framework Minimalism And Software Architecture

Testing Made Easier Via Framework Minimalism And Software Architecture

Ryan Kay

Like many other Android developers, my initial foray into testing on the platform lead me to be immediately confronted with a demoralizing degree of jargon. Further, what few examples I came across at the time (circa 2015) did not present practical use cases which may have inclined me to think that the cost to benefit ratio of learning a tool like Espresso in order to verify that a TextView.setText(…) was working properly, was a reasonable investment.

To make matters even worse, I did not have a working understanding of software architecture in theory or practice, which meant that even if I bothered to learn these frameworks, I would have been writing tests for monolithic applications comprised of a few god classes, written in spaghetti code. The punchline is that building, testing, and maintaining such applications is an exercise in self-sabotage quite regardless of your framework expertise; yet this realization only becomes clear after one has built a modular, loosely-coupled, and highly-cohesive application.

From here we arrive at one of the main points of discussion in this article, which I will summarize in plain language here: Among the primary benefits of applying the golden principles of software architecture (do not worry, I will discuss them with simple examples and language), is that your code can become easier to test. There are other benefits to applying such principles, but the relationship between software architecture and testing is the focus of this article.

However, for the sake of those who wish to understand why and how we test our code, we will first explore the concept of testing by analogy; without requiring you to memorize any jargon. Before getting deeper into the primary topic, we will also look at the question of why so many testing frameworks exist, for in examining this we may begin to see their benefits, limitations, and perhaps even an alternative solution.

Testing: Why And How

This section will not be new information for any seasoned tester, but perhaps you may enjoy this analogy nonetheless. Of course I am a software engineer, not a rocket engineer, but for a moment I will borrow an analogy which relates to designing and building objects both in physical space, and in the memory space of a computer. It turns out that while the medium changes, the process is in principle quite the same.

Suppose for a moment that we are rocket engineers, and our job is to build the first stage* rocket booster of a space shuttle. Suppose as well, that we have come up with a serviceable design for the first stage to begin building and testing in various conditions.

“First stage” refers to boosters which are fired when the rocket is first launched

Before we get to the process, I would like to point out why I prefer this analogy: You should not have any difficulty answering the question of why we are bothering to test our design before putting it in situations where human lives are at stake. While I will not try to convince you that testing your applications before launch could save lives (although it is possible depending on the nature of the application), it could save ratings, reviews, and your job. In the broadest sense, testing is the way in which we make sure that single parts, several components, and whole systems work before we employ them in situations where it is critically important for them to not fail.

Returning to the how aspect of this analogy, I will introduce the process by which engineers go about testing a particular design: redundancy. Redundancy is simple in principle: Build copies of the component to be tested to the same design specification as what you wish to use at launch time. Test these copies in an isolated environment which strictly controls for preconditions and variables. While this does not guarantee that the rocket booster will work properly when integrated in the whole shuttle, one can be certain that if it does not work in a controlled environment, it will be very unlikely to work at all.

Suppose that of the hundreds, or perhaps thousands of variables which the copies of the rocket design have been tested against, it comes down to ambient temperatures in which the rocket booster will be test fired. Upon testing at 35° Celsius, we see that everything functions without error. Again, the rocket is tested at roughly room temperature without failure. The final test will be at the lowest recorded temperature for the launch site, at -5° Celcius. During this final test, the rocket fires, but after a short period, the rocket flares up and shortly thereafter explodes violently; but fortunately in a controlled and safe environment.

At this point, we know that changes in temperature appear to be at least involved in the failed test, which leads us to consider what parts of the rocket booster may be adversely effected by cold temperatures. Over time, it is discovered that one key component, a rubber O-ring which serves to staunch the flow of fuel from one compartment to another, becomes rigid and ineffectual when exposed to temperatures approaching or below freezing.

It is possible that you have noticed that his analogy is loosely based on the tragic events of the Challenger space shuttle disaster. For those unfamiliar, the sad truth (insofar as investigations concluded) is that there were plenty of failed tests and warnings from the engineers, and yet administrative and political concerns spurred the launch to proceed regardless. In any case, whether or not you have memorized the term redundancy, my hope is that you have grasped the fundamental process for testing parts of any kind of system.

Concerning Software

Whereas the prior analogy explained the fundamental process for testing rockets (while taking plenty of liberty with the finer details), I will now summarize in a manner which is likely more relevant to you and I. While it is possible to test software by only launching it to devices once it is in any sort of deployable state, I suppose instead that we can apply the principle of redundancy to the individual parts of the application first.

This means that we create copies of the smaller parts of the whole application (commonly referred to as Units of software), set up an isolated test environment, and see how they behave based on whatever variables, arguments, events, and responses which may occur at runtime. Testing is truly as simple as that in theory, but the key to even getting to this process lies in building applications which are feasibly testable. This comes down to two concerns which we will look at in the next two sections. The first concern has to do with the test environment, and the second concern has to do with the way in which we structure applications.

Why Do We Need Frameworks?

In order to test a piece of software (henceforth referred to as a Unit, although this definition is deliberately an over-simplification), it is necessary to have some kind of testing environment which allows you to interact with your software at runtime. For those building applications to be executed purely on a JVM (Java Virtual Machine) environment, all that is required to write tests is a JRE (Java Runtime Environment). Take for example this very simple Calculator class:

class Calculator {
    private int add(int a, int b){
        return a + b;
    }

    private int subtract(int a, int b){
        return a - b;
    }
}

In absence of any frameworks, as long as we have a test class which contains a mainfunction to actually execute our code, we can test it. As you may recall, the main function denotes the starting point of execution for a simple Java program. As for what we are testing for, we simply feed some test data into the Calculator’s functions and verify that it is performing basic arithmetic properly:

public class Main {

    public static void main(String[] args){
    //create a copy of the Unit to be tested
        Calculator calc = new Calculator();
    //create test conditions to verify behaviour
        int addTest = calc.add(2, 2);
        int subtractTest = calc.subtract(2, 2);

    //verify behaviour by assertion
        if (addTest == 4) System.out.println("addTest has passed.");
        else System.out.println("addTest has failed.");

        if (subtractTest == 0) System.out.println("subtractTest has passed.");
        else System.out.println("subtractTest has failed.");
    }
}

Testing an Android application is of course, a completely different procedure. Although there is a main function buried deep within the source of the ZygoteInit.java file (the finer details of which are not important here), which is invoked prior to an Android application being launched on the JVM, even a junior Android developer ought to know that the system itself is responsible for calling this function; not the developer. Instead, the entry points for Android applications happen to be the Application class, and any Activity classes which the system can be pointed to via the AndroidManifest.xml file.

All of this is just a lead up to the fact that testing Units in an Android application presents a greater level of complexity, strictly because our testing environment must now account for the Android platform.

Taming The Problem Of Tight Coupling

Tight coupling is a term which describes a function, class, or application module which is dependent on particular platforms, frameworks, languages, and libraries. It is a relative term, meaning that our Calculator.java example is tightly coupled to the Java programming language and standard library, but that is the extent of its coupling. Along the same lines, the problem of testing classes which are tightly coupled to the Android platform, is that you must find a way to work with or around the platform.

For classes tightly coupled to the Android platform, you have two options. The first, is to simply deploy your classes to an Android device (physical or virtual). While I do suggest that you test deploy your application code before shipping it to production, this is a highly inefficient approach during the early and middle stages of the development process with respect to time.

A Unit, however technical a definition you prefer, is generally thought of as a single function in a class (although some expand the definition to include subsequent helper functions which are called internally by the initial single function call). Either way, Units are meant to be small; building, compiling, and deploying an entire application to test a single Unit is to miss the point of testing in isolation entirely.

Another solution to the problem of tight coupling, is to use testing frameworks to interact with, or mock (simulate) platform dependencies. Frameworks such as Espresso and Robolectric give developers far more effective means for testing Units than the previous approach; the former being useful for tests run on a device (known as “instrumented tests” because apparently calling them device tests was not ambiguous enough) and the latter being capable of mocking the Android framework locally on a JVM.

Before I proceed to railing against such frameworks instead of the alternative I will discuss shortly, I want to be clear that I do not mean to imply that you should never use these options. The process which a developer uses to build and test their applications should be born of a combination of personal preference and an eye for efficiency.

For those who are not fond of building modular and loosely coupled applications, you will have no choice but to become familiar with these frameworks if you wish to have an adequate level of test coverage. Many wonderful applications have been built this way, and I am not infrequently accused of making my applications too modular and abstract. Whether you take my approach or decide to lean heavily on frameworks, I salute you for putting in the time and effort to test your applications.

Keep Your Frameworks At Arms Length

For the final preamble to the core lesson of this article, it is worth discussing why you might want to have an attitude of minimalism when it comes to using frameworks (and this applies to more than just testing frameworks). The subtitle above is a paraphrase from the magnanimous teacher of software best practices: Robert “Uncle Bob” C. Martin. Of the many gems he has given me since I first studied his works, this one took several years of direct experience to grasp.

Insofar As I understand what this statement is about, the cost of using frameworks is in the time investment required to learn and maintain them. Some of them change quite frequently and some of them do not change frequently enough. Functions become deprecated, frameworks cease to be maintained, and every 6-24 months a new framework arrives to supplant the last. Therefore, if you can find a solution which can be implemented as a platform or language feature (which tend to last much longer), it will tend to be more resistant to changes of the various types mentioned above.

On a more technical note, frameworks such as Espresso and to a lesser degree Robolectric, can never run as efficiently as simple JUnit tests, or even the framework free test from earlier on. While JUnit is indeed a framework, it is tightly coupled to the JVM, which tends to change at a much slower rate than the Android platform proper. Fewer frameworks almost invariably means code which is more efficient in terms of the time it takes to execute and write one or more tests.

From this, you can probably gather that we will now be discussing an approach which will leverage some techniques which allows us to keep the Android platform at arms length; all the while allowing us plenty of code coverage, test efficiency, and the opportunity to still use a framework here or there when the need arises.

The Art Of Architecture

To use a silly analogy, one might think of frameworks and platforms as being like overbearing colleagues who will take over your development process unless you set appropriate boundaries with them. The golden principles of software architecture can give you the general concepts and specific techniques necessary to both create and enforce these boundaries. As we will see in a moment, if you have ever wondered what the benefits of applying software architecture principles in your code truly are, some directly, and many indirectly make your code easier to test.

Separation Of Concerns

Separation Of Concerns is by my estimation the most universally applicable and useful concept in software architecture as a whole (without meaning to say that others should be neglected). Separation of concerns (SOC) can be applied, or completely ignored, across every perspective of software development I am aware of. To briefly summarize the concept, we will look at SOC when applied to classes, but be aware that SOC can be applied to functions through extensive usage of helper functions, and it can be extrapolated to entire modules of an application (“modules” used in the context of Android/Gradle).

If you have spent much time at all researching software architectural patterns for GUI applications, you will likely have come across at least one of: Model-View-Controller (MVC), Model-View-Presenter (MVP), or Model-View-ViewModel (MVVM). Having built applications in every style, I will say upfront that I do not consider any of them to be the single best option for all projects (or even features within a single project). Ironically, the pattern which the Android team presented some years ago as their recommended approach, MVVM, appears to be the least testable in absence of Android specific testing frameworks (assuming you wish to use the Android platform’s ViewModel classes, which I am admittedly a fan of).

In any case, the specifics of these patterns are less important than their generalities. All of these patterns are just different flavours of SOC which emphasize a fundamental separation of three kinds of code which I refer to as: Data, User Interface, Logic.

So, how exactly does separating Data, User Interface, and Logic help you to test your applications? The answer is that by pulling logic out of classes which must deal with platform/framework dependencies into classes which possess little or no platform/framework dependencies, testing becomes easy and framework minimal. To be clear, I am generally talking about classes which must render the user interface, store data in a SQL table, or connect to a remote server. To demonstrate how this works, let us look at a simplified three layer architecture of a hypothetical Android application.

The first class will manage our user interface. To keep things simple, I have used an Activity for this purpose, but I typically opt for Fragments instead as user interface classes. In either case, both classes present similar tight coupling to the Android platform:

public class CalculatorUserInterface extends Activity implements CalculatorContract.IUserInterface {

    private TextView display;
    private CalculatorContract.IControlLogic controlLogic;
    private final String INVALID_MESSAGE = "Invalid Expression.";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        controlLogic = new DependencyProvider().provideControlLogic(this);

        display = findViewById(R.id.textViewDisplay);
        Button evaluate = findViewById(R.id.buttonEvaluate);
        evaluate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                controlLogic.handleInput('=');
            }
        });
        //..bindings for the rest of the calculator buttons
    }

    @Override
    public void updateDisplay(String displayText) {
        display.setText(displayText);
    }

    @Override
    public String getDisplay() {
        return display.getText().toString();
    }

    @Override
    public void showError() {
        Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show();
    }
}

As you can see, the Activity has two jobs: First, since it is the entry point of a given feature of an Android application, it acts as a sort of container for the other components of the feature. In simple terms, a container can be thought of as a sort of root class which the other components are ultimately tethered to via references (or private member fields in this case). It also inflates, binds references, and adds listeners to the XML layout (the user interface).

Testing Control Logic

Rather than having the Activity possess a reference to a concrete class in the back end, we have it talk to an interface of type CalculatorContract.IControlLogic. We will discuss why this is an interface in the next section. For now, just understand that whatever is on the other side of that interface is supposed to be something like a Presenter or Controller. Since this class will be controlling interactions between the front-end Activity and the back-end Calculator, I have chosen to call it CalculatorControlLogic:

public class CalculatorControlLogic implements CalculatorContract.IControlLogic {

    private CalculatorContract.IUserInterface ui;
    private CalculatorContract.IComputationLogic comp;

    public CalculatorControlLogic(CalculatorContract.IUserInterface ui, CalculatorContract.IComputationLogic comp) {
        this.ui = ui;
        this.comp = comp;
    }

    @Override
    public void handleInput(char inputChar) {
        switch (inputChar){
            case '=':
                evaluateExpression();
                break;
            //...handle other input events
        }
    }
    private void evaluateExpression() {
        Optional result = comp.computeResult(ui.getDisplay());

        if (result.isPresent()) ui.updateDisplay(result.get());
        else ui.showError();
    }
}

There are many subtle things about the way in which this class is designed that make it easier to test. Firstly, all of its references are either from the Java standard library, or interfaces which are defined within the application. This means that testing this class without any frameworks is an absolute breeze, and it could be done locally on a JVM. Another small but useful tip is that all of the different interactions of this class can be called via a single generic handleInput(...) function. This provides a single entry point to test every behaviour of this class.

Also note that in the evaluateExpression() function, I am returning a class of type Optional<String> from the back end. Normally I would use what functional programmers call an Either Monad, or as I prefer to call it, a Result Wrapper. Whatever stupid name you use, it is an object which is capable of representing multiple different states through a single function call. Optional is a simpler construct which can represent either a null, or some value of the supplied generic type. In any case, since the back end might be given an invalid expression, we want to give the ControlLogicclass some means of determining the result of the backend operation; accounting for both success and failure. In this case, null will represent a failure.

Below is an example test class which has been written using JUnit, and a class which in testing jargon is called a Fake:

public class CalculatorControlLogicTest {

    @Test
    public void validExpressionTest() {

        CalculatorContract.IComputationLogic comp = new FakeComputationLogic();
        CalculatorContract.IUserInterface ui = new FakeUserInterface();
        CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp);

        controller.handleInput('=');

        assertTrue(((FakeUserInterface) ui).displayUpdateCalled);
        assertTrue(((FakeUserInterface) ui).displayValueFinal.equals("10.0"));
        assertTrue(((FakeComputationLogic) comp).computeResultCalled);

    }

    @Test
    public void invalidExpressionTest() {

        CalculatorContract.IComputationLogic comp = new FakeComputationLogic();
        ((FakeComputationLogic) comp).returnEmpty = true;
        CalculatorContract.IUserInterface ui = new FakeUserInterface();
        ((FakeUserInterface) ui).displayValueInitial = "+7+7";
        CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp);

        controller.handleInput('=');

        assertTrue(((FakeUserInterface) ui).showErrorCalled);
        assertTrue(((FakeComputationLogic) comp).computeResultCalled);

    }

    private class FakeUserInterface implements CalculatorContract.IUserInterface{
        boolean displayUpdateCalled = false;
        boolean showErrorCalled = false;
        String displayValueInitial = "5+5";
        String displayValueFinal = "";

        @Override
        public void updateDisplay(String displayText) {
            displayUpdateCalled = true;
            displayValueFinal = displayText;
        }

        @Override
        public String getDisplay() {
            return displayValueInitial;
        }

        @Override
        public void showError() {
            showErrorCalled = true;
        }
    }

    private class FakeComputationLogic implements CalculatorContract.IComputationLogic{
        boolean computeResultCalled = false;
        boolean returnEmpty = false;

        @Override
        public Optional computeResult(String expression) {
            computeResultCalled = true;
            if (returnEmpty) return Optional.empty();
            else return Optional.of("10.0");
        }
    }
}

As you can see, not only can this test suite be executed very rapidly, but it did not take very much time at all to write. In any case, we will now look at some more subtle things which made writing this test class very easy.

The Power Of Abstraction And Dependency Inversion

There are two other important concepts which have been applied to CalculatorControlLogic which have made it trivially easy to test. Firstly, if you have ever wondered what the benefits of using Interfaces and Abstract Classes (collectively referred to as abstractions) in Java are, the code above is a direct demonstration. Since the class to be tested references abstractions instead of concrete classes, we were able to create Fake test doubles for the user interface and back end from within our test class. As long as these test doubles implement the appropriate interfaces, CalculatorControlLogiccould not care less that they are not the real thing.

Secondly, CalculatorControlLogichas been given its dependencies via the constructor (yes, that is a form of Dependency Injection), instead of creating its own dependencies. Therefore, it does not need to be re-written when used in a production or testing environment, which is a bonus for efficiency.

Dependency Injection is a form of Inversion Of Control, which is a tricky concept to define in plain language. Whether you use Dependency Injection or a Service Locator Pattern, they both achieve what Martin Fowler (my favourite teacher on such topics) describes as “the principle of separating configuration from use.” This results in classes which are easier to test, and easier to build in isolation from one another.

Testing Computation Logic

Finally, we come to the ComputationLogic class, which is supposed to approximate an IO device such as an adapter to a remote server, or a local database. Since we need neither of those for a simple calculator, it will just be responsible for encapsulating the logic required to validate and evaluate the expressions we give it:

public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic {

    private final char ADD = '+';
    private final char SUBTRACT = '-';
    private final char MULTIPLY = '*';
    private final char DIVIDE = '/';

    @Override
    public Optional computeResult(String expression) {
        if (hasOperator(expression)) return attemptEvaluation(expression);
        else return Optional.empty();

    }

    private Optional attemptEvaluation(String expression) {
        String delimiter = getOperator(expression);
        Binomial b = buildBinomial(expression, delimiter);
        return evaluateBinomial(b);
    }

    private Optional evaluateBinomial(Binomial b) {
        String result;
        switch (b.getOperatorChar()) {
            case ADD:
                result = Double.toString(b.firstTerm + b.secondTerm);
                break;
            case SUBTRACT:
                result = Double.toString(b.firstTerm - b.secondTerm);
                break;
            case MULTIPLY:
                result = Double.toString(b.firstTerm * b.secondTerm);
                break;
            case DIVIDE:
                result = Double.toString(b.firstTerm / b.secondTerm);
                break;
            default:
                return Optional.empty();
        }
        return Optional.of(result);
    }

    private Binomial buildBinomial(String expression, String delimiter) {
        String[] operands = expression.split(delimiter);
        return new Binomial(
                delimiter,
                Double.parseDouble(operands[0]),
                Double.parseDouble(operands[1])
        );
    }

    private String getOperator(String expression) {
        for (char c : expression.toCharArray()) {
            if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE)
                return "\\" + c;
        }

        //default
        return "+";
    }

    private boolean hasOperator(String expression) {
        for (char c : expression.toCharArray()) {
            if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true;
        }
        return false;
    }

    private class Binomial {
        String operator;
        double firstTerm;
        double secondTerm;

        Binomial(String operator, double firstTerm, double secondTerm) {
            this.operator = operator;
            this.firstTerm = firstTerm;
            this.secondTerm = secondTerm;
        }

        char getOperatorChar(){
            return operator.charAt(operator.length() - 1);
        }
    }

    }

There is not too much to say about this class since typically there would be some tight coupling to a particular back-end library which would present similar problems as a class tightly coupled to Android. In a moment we will discuss what to do about such classes, but this one is so easy to test that we may as well have a try:

public class CalculatorComputationLogicTest {

    private CalculatorComputationLogic comp = new CalculatorComputationLogic();

    @Test
    public void additionTest() {
        String EXPRESSION = "5+5";
        String ANSWER = "10.0";

        Optional result = comp.computeResult(EXPRESSION);

        assertTrue(result.isPresent());
        assertEquals(result.get(), ANSWER);
    }

    @Test
    public void subtractTest() {
        String EXPRESSION = "5-5";
        String ANSWER = "0.0";

        Optional result = comp.computeResult(EXPRESSION);

        assertTrue(result.isPresent());
        assertEquals(result.get(), ANSWER);
    }

    @Test
    public void multiplyTest() {
        String EXPRESSION = "5*5";
        String ANSWER = "25.0";

        Optional result = comp.computeResult(EXPRESSION);

        assertTrue(result.isPresent());
        assertEquals(result.get(), ANSWER);
    }

    @Test
    public void divideTest() {
        String EXPRESSION = "5/5";
        String ANSWER = "1.0";

        Optional result = comp.computeResult(EXPRESSION);

        assertTrue(result.isPresent());
        assertEquals(result.get(), ANSWER);
    }

    @Test
    public void invalidTest() {
        String EXPRESSION = "Potato";

        Optional result = comp.computeResult(EXPRESSION);

        assertTrue(!result.isPresent());
    }
}

The easiest classes to test, are those which are simply given some value or object, and are expected to return a result without the necessity of calling some external dependencies. In any case, there comes a point where no matter how much software architecture wizardry you apply, you will still need to worry about classes which cannot be decoupled from platforms and frameworks. Fortunately, there is still a way we can employ software architecture to: At worst make these classes easier to test, and at best, so trivially simple that testing can be done at a glance.

Humble Objects And Passive Views

The above two names refer to a pattern in which an object that must talk to low-level dependencies, is simplified so much that it arguably does not need to be tested. I was first introduced to this pattern via Martin Fowler’s blog on variations of Model-View-Presenter. Later on, through Robert C. Martin’s works, I was introduced to the idea of treating certain classes as Humble Objects, which implies that this pattern does not need to be limited to user interface classes (although I do not mean to say that Fowler ever implied such a limitation).

Whatever you choose to call this pattern, it is delightfully simple to understand, and in some sense I believe it is actually just the result of rigorously applying SOC to your classes. While this pattern applies also to back end classes, we will use our user interface class to demonstrate this principle in action. The separation is very simple: Classes which interact with platform and framework dependencies, do not think for themselves (hence the monikers Humble and Passive). When an event occurs, the only thing they do is forward the details of this event to whatever logic class happens to be listening:

//from CalculatorActivity's onCreate() function:
evaluate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                controlLogic.handleInput('=');
            }
        });

The logic class, which should be trivially easy to test, is then responsible for controlling the user interface in a very fine-grained manner. Rather than calling a single generic updateUserInterface(...) function on the user interface class and leaving it to do the work of a bulk update, the user interface (or other such class) will possess small and specific functions which should be easy to name and implement:

//Interface functions of CalculatorActivity:
        @Override
    public void updateDisplay(String displayText) {
        display.setText(displayText);
    }

    @Override
    public String getDisplay() {
        return display.getText().toString();
    }

    @Override
    public void showError() {
        Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show();
    }
//…

In principal, these two examples ought to give you enough to understand how to go about implementing this pattern. The object which possesses the logic is loosely coupled, and the object which is tightly coupled to pesky dependencies becomes almost devoid of logic.

Now, at the start of this subsection, I made the statement that these classes become arguably unnecessary to test, and it is important we look at both sides of this argument. In an absolute sense, it is impossible to achieve 100% test coverage by employing this pattern, unless you still write tests for such humble/passive classes. It is also worth noting that my decision to use a Calculator as an example App, means that I cannot escape having a gigantic mass of findViewById(...) calls present in the Activity. Giant masses of repetitive code are a common cause of typing errors, and in the absence of some Android UI testing frameworks, my only recourse for testing would be via deploying the feature to a device and manually testing each interaction. Ouch.

It is at this point that I will humbly say that I do not know if 100% code coverage is absolutely necessary. I do not know many developers who strive for absolute test coverage in production code, and I have never done so myself. One day I might, but I will reserve my opinions on this matter until I have the reference experiences to back them up. In any case, I would argue that applying this pattern will still ultimately make it simpler and easier to test tightly coupled classes; if for no reason other than they become simpler to write.

Another objection to this approach, was raised by a fellow programmer when I described this approach in another context. The objection was that the logic class (whether it be a Controller, Presenter, or even a ViewModel depending on how you use it), becomes a God class.

While I do not agree with that sentiment, I do agree that the end result of applying this pattern is that your Logic classes become larger than if you left more decisions up to your user interface class.

This has never been an issue for me as I treat each feature of my applications as self-contained components, as opposed to having one giant controller for managing multiple user interface screens. In any case, I think this argument holds reasonably true if you fail to apply SOC to your front end or back end components. Therefore, my advice is to apply SOC to your front end and back end components quite rigorously.

Further Considerations

After all of this discussion on applying the principles of software architecture to reduce the necessity of using a wide-array of testing frameworks, improve the testability of classes in general, and a pattern which allows classes to be tested indirectly (at least to some degree), I am not actually here to tell you to stop using your preferred frameworks.

For those curious, I often use a library to generate mock classes for my Unit tests (for Java I prefer Mockito, but these days I mostly write Kotlin and prefer Mockk in that language), and JUnit is a framework which I use quite invariably. Since all of these options are coupled to languages as opposed to the Android platform, I can use them quite interchangeably across mobile and web application development. From time to time (if project requirements demand it), I will even use tools like Robolectric, MockWebServer, and in my five years of studying Android, I did begrudgingly use Espresso once.

My hope is that in reading this article, anyone who has experienced a similar degree of aversion to testing due to paralysis by jargon analysis, will come to see that getting started with testing really can be simple and framework minimal.

Further Reading on SmashingMag:

Smashing Editorial (dm, il)