DEV Community

Mark Rubin
Mark Rubin

Posted on

Method Decomposition And Class Decomposition

This is part of a brief series on Good Habits For Java Programmers.

In the beginning...

When you're first learning Java, most of the time you'll be writing short programs whose code is placed entirely in a public static void main method of a class that does nothing but host that main method. For example, maybe you're learning how to iterate with a for loop, and so read or write a little program to sum up all the evens between 0 and 10.

public static void main(String args[]) {
    int sumOfEvens = 0;
    for (int even = 0; even <= 10; even += 2) {
        sumOfEvens += even;
    }
    System.out.println("The sum of evens from 0 to 10 is " + sumOfEvens);
}
Enter fullscreen mode Exit fullscreen mode

It makes sense to keep all of that code in main from a teaching and learning standpoint: it's the simplest place to put the code, and by introducing no additional complexity, the instructor and you get to focus on the other concepts being taught -- say, how a for loop works or how to use a local variable (sumOfEvens) or how the algorithm works (adding 2 each time takes us to the next even). I have no problem with this teaching methodology. In fact, I think it's a good idea to leave you to code in main while you focus on other things.

This was a small program, but since this is how students start, and each incremental addition in size feels so, well, incremental, often students stay with this model, eventually writing programs with far more code in their public static void main method than is advisable. As soon as students learn what methods are and how to call them from one another, they should start decomposing their logic from main into submethods: main will call one or more helper methods which themselves may call one or more helper methods, and so on, until each method is simple and does a single thing. The goal is to have well named methods where each method has a single responsibility that aligns with the name of the method.

Converting a single method that has lots of code embedded in it to do several things into multiple methods is called "decomposition" because it involves breaking apart (decomposing) the single big method into several smaller methods.

Decomposing even this small example

Some students appreciate the point, but aren't sure how to apply it, especially for a small example like this. What would decomposing this look like? Here's one go at it:

    public static void main(String args[]) {
        int sumOfEvens = sumEvensUntil(10);
        outputSumOfEvens(sumOfEvens);
    }

    private static int sumEvensUntil(int until) {
        int sumOfEvens = 0;
        for (int even = 0; even <= 10; even += 2) {
            sumOfEvens += even;
        }
        return sumOfEvens;
    }

    private static void outputSumOfEvens(int sum) {
        System.out.println("The sum of evens from 0 to 10 is " + sum);
    }

Enter fullscreen mode Exit fullscreen mode

Imagine that instead of hardcoding that 10 for our stopping point, we prompt the user for our stopping point. Then we might have:

    public static void main(String args[]) {
        int stoppingPoint = requestStoppingPoint();
        int sumOfEvens = sumEvensUntil(stoppingPoint);
        outputSumOfEvens(sumOfEvens, stoppingPoint);
    }

    private static int requestStoppingPoint() {
        System.out.println("Type in a non-negative integer for the stopping point.");
        Scanner scanner = new Scanner(System.in);
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        return scanner.nextInt();
    }

    private static int sumEvensUntil(int until) {
        int sumOfEvens = 0;
        for (int even = 0; even <= 10; even += 2) {
            sumOfEvens += even;
        }
        return sumOfEvens;
    }

    private static void outputSumOfEvens(int sum, int stoppingPoint) {
        System.out.println("The sum of evens from 0 to " + stoppingPoint + " is " + sum);
    }
Enter fullscreen mode Exit fullscreen mode

Maybe we request a starting point, too:

    public static void main(String args[]) {
        int startingPoint = requestStartingPoint();
        int stoppingPoint = requestStoppingPoint();
        int sumOfEvens = sumEvensUntil(startingPoint, stoppingPoint);
        outputSumOfEvens(sumOfEvens, startingPoint, stoppingPoint);
    }

    private static int requestStartingPoint() {
        System.out.println("Type in a non-negative integer for the starting point.");
        Scanner scanner = new Scanner(System.in);
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        return scanner.nextInt();
    }

    private static int requestStoppingPoint() {
        System.out.println("Type in a non-negative integer for the stopping point.");
        Scanner scanner = new Scanner(System.in);
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        return scanner.nextInt();
    }

    private static int sumEvensUntil(int start, int until) {
        int sumOfEvens = 0;
        for (int even = start; even <= 10; even += 2) {
            sumOfEvens += even;
        }
        return sumOfEvens;
    }

    private static void outputSumOfEvens(int sum, int startingPoint, int stoppingPoint) {
        System.out.println("The sum of evens from " + startingPoint + " to " + stoppingPoint + " is " + sum);
    }
Enter fullscreen mode Exit fullscreen mode

Let's appreciate the difference here from what a typical beginner's program would look like to do the same.

    public static void main(String args[]) {
        System.out.println("Type in a non-negative integer for the starting point.");
        Scanner scanner = new Scanner(System.in);
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        int startingPoint = scanner.nextInt();

        System.out.println("Type in a non-negative integer for the stopping point.");
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        int stoppingPoint = scanner.nextInt();

        int sumOfEvens = 0;
        for (int even = startingPoint; even <= stoppingPoint; even += 2) {
            sumOfEvens += even;
        }

        System.out.println("The sum of evens from " + startingPoint + " to " + stoppingPoint + " is " + sum);
    }
Enter fullscreen mode Exit fullscreen mode

I hope you can see that the second, non-decomposed version, is harder to read and to follow, even for such a short program. Why is that? In large part it's because the entire logic of the program is presented in a single, long, linear sequence of code. You have to mentally figure out what each set of lines does, assign a sort of mental label to what each does, then consider how each interacts with the next mentally labeled bit of code, and so on, down the line. Beginning Java programmers acknowledge this by putting in those newlines to help separate out those mentally labeled bits of code. There's a reason why the newlines are placed where they are: their placement is an attempt to introduce structure and order onto what otherwise seems unstructured and unordered. In fact, you'll often see new programmers add comments to help them and their readers keep track of what's going on:

    public static void main(String args[]) {
        // Get the starting point.
        System.out.println("Type in a non-negative integer for the starting point.");
        Scanner scanner = new Scanner(System.in);
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        int startingPoint = scanner.nextInt();

        // Get the ending point.
        System.out.println("Type in a non-negative integer for the stopping point.");
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        int stoppingPoint = scanner.nextInt();

        // Sum the evens
        int sumOfEvens = 0;
        for (int even = startingPoint; even <= stoppingPoint; even += 2) {
            sumOfEvens += even;
        }

        // Print out the result
        System.out.println("The sum of evens from " + startingPoint + " to " + stoppingPoint + " is " + sum);
    }
Enter fullscreen mode Exit fullscreen mode

So our new programmers usually are correctly decomposing the problem and are looking for ways to indicate the problem decomposition. The right way to indicate the problem decomposition is to reflect it with method decomposition.

Lots of nice things come from that method decomposition, but I think the most obvious one is that you no longer need those comments. If you use well named methods, the names of the methods tell me what they do, and the sequence of calls serve to narrate what's going on all on their own.

Class decomposition

Does the main belong in this class, EvenSummer? As I say in Where Should Main Live, I don't think so. I would move that main out into its own class. If you do that, that's a form of class decomposition: breaking down the responsibilities of a single class into multiple classes, each with a dedicated purpose.

What about the prompting? Should that be in the same class as the class that knows how to sum? What about the printing? It would be better if not and leave EvenSummer with only the responsibility and modeling of how to sum evens. Make it pure and have it be responsible for one thing only, managing the logic for summing evens.

So where would the input prompting and parsing and output go? Perhaps in a Driver class that would ask for input, and call into EvenSummer. How about a final program that looks like this (note that I'm including multiple files' contents here):

// In Main.java
public class Main {
    public static void main(String[] args) {
        Driver.sumUserSuppliedEvens();
    }
}

// In Driver.java
public class Driver {
    public static void sumUserSuppliedEvens() {
        int startingPoint = requestStartingPoint();
        int stoppingPoint = requestStoppingPoint();
        int sumOfEvens = EvensSummer.sumEvensUntil(startingPoint, stoppingPoint);
        outputSumOfEvens(sumOfEvens, startingPoint, stoppingPoint);
    }

    private static int requestStartingPoint() {
        System.out.println("Type in a non-negative integer for the starting point.");
        Scanner scanner = new Scanner(System.in);
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        return scanner.nextInt();
    }

    private static int requestStoppingPoint() {
        System.out.println("Type in a non-negative integer for the stopping point.");
        Scanner scanner = new Scanner(System.in);
        // Ignore user input errors, and assume they'll type in a non-negative integer.
        return scanner.nextInt();
    }

    private static void outputSumOfEvens(int sum, int startingPoint, int stoppingPoint) {
        System.out.println("The sum of evens from " + startingPoint + " to " + stoppingPoint + " is " + sum);
    }
}

// In EvensSummer.java
public class EvensSummer {
    public static int sumEvensUntil(int start, int until) {
        int sumOfEvens = 0;
        for (int even = start; even <= 10; even += 2) {
            sumOfEvens += even;
        }
        return sumOfEvens;
    }
}
Enter fullscreen mode Exit fullscreen mode

We've now decomposed our logic into classes, each with its own responsibility and within those classes, we've decomposed our logic into methods, each with its own responsibility.

Why do this?

  • Readability It's a lot easier to read the decomposed code than the code that is not decomposed. It's easier for the author and for the code reviewers to understand what the program does as a whole, and what each part does and how they fit together. And we don't need comments: the code is self-documenting!
  • Maintainability When you break up your methods and classes like this, it's much easier to make updates to your code later. In part, that's because of the improved readability -- you can see where the seams are in the code, and so where the appropriate places are to layer in new functionality. Hopefully, the stepwise way in which I was able to add prompting the user for two different inputs helps drive that home. And as I added more code, it didn't make it harder to read my program. Compare my method decomposed version to the version that did not decompose the methods.
  • Debuggability A lot of debugging involves visually inspecting your code to see what could have gone wrong. Breaking the code into distinct, dedicated methods in distinct, dedicated classes makes it easier for you to train your focus on just what that specific method is supposed to do, and whether it does it correctly. Your mental model gets simpler because each method is simpler. There are also benefits having to do with the impossibility of one bit of code's influencing another: with the non-decomposed, long line of code implementation, every bit of that implementation can share the same variables and possibly accidentally overwrite or misuse them. When you decompose your logic, you break your logic into smaller scoped code blocks that don't influence each other.
  • Reusability Single purpose methods are easier to reuse. You can call the same method from different points in your code. That's not so obvious here, in this example, because we're not reusing any code. A lot of your early programs don't have much code reuse. And when you do need to reuse code, you'll probably naturally refactor your code so you have a method you can reuse. So this reason may fall a bit flat for you, even though it's true.
  • Testability Your textbooks will likely tell you that it's easier to test individual, small, single purpose methods and small, dedicated classes. That's very true, and important. But you're also probably not writing testing code right now. So you'll have to trust us that this is true, and we're promoting good habits for later, when you do write tests.

How do I know when to decompose?

It's often a judgement call when to decompose, and I would bet most students think that decomposing their methods and classes is adding a bunch of unnecessary overhead. Their programs are small, and they wrote them, so they can track what's going on in them.

But your programs will grow with time, and developing and practicing the good habits of decomposing them now, when it's actually easier because they are small, will serve you well.

Plus, you sometimes do iterate on your same program. Homework assignments occasionally ask you to enhance an earlier assignment. So you may be, even in the beginning of your programming journey working on smaller programs, in the business of maintaining your programs. And you certainly have to try to fix bugs. Even for these small problems, the benefits of decomposition come out, despite the fact that you can sometimes overcome the deficits of not decomposing by exerting some extra brain power.

Here's a great tip. Beginning programmers do have an instinct for this: as I said above, it's why they insert newlines into their code, and it's why they add comments to blocks of code. I see it when they ask me for help, and when we're looking at their code together, they tell me "This part here gets the starting number from the user; then this part gets the ending number; then this part sums the evens; and then this part prints out the answer." If you ever find yourself doing any of these things -- inserting blank lines, adding comments, narrating to yourself or someone else what each part does -- that's a very strong signal you should decompose your logic!

Other notes

Notice that in my decomposed example, I create two instances of Scanner. I could choose to make one instance and pass it in to each of the methods: e.g.

    public static void main(String args[]) {
        Scanner scanner = new Scanner(System.in);
        int sumOfEvens = sumEvensUntil(requestStartingPoint(scanner), requestStoppingPoint(scanner));
        outputSumOfEvens(sumOfEvens);
    }
// Updates to requestStartingPoint and requestStoppingPoint ommitted
Enter fullscreen mode Exit fullscreen mode

I also don't call close on my Scanners (and so close never gets called on those InputStreams, System.in). That's not great, but let's limit the contents of what we're dealing with. Closing resources is usually a later topic in a new Java programmer's journey.

Top comments (0)