Skip to main content

Command Palette

Search for a command to run...

One Lesson From Clean Code That Transformed My Coding Style

How Robert Martin's Stepdown Rule Helps Me Write Cleaner Code

Published
7 min read
One Lesson From Clean Code That Transformed My Coding Style

Hi everyone! Welcome back to Java with John. A little while ago, I finished reading the classic "Clean Code" by Robert C. Martin and was inspired by all the lessons I learned. For this blog post, I thought I’d share with you my favourite rule from the book that changed the way I style my code. While I still don’t follow this lesson perfectly, I often try my best to emulate it and implement it closely, which has significantly improved the readability of my code.

The Stepdown Rule

In Clean Code, Robert discusses a concept called the stepdown rule, which we’ll be discussing today. The stepdown rule says that each function should read as a top-down narrative: starting with the bigger picture, then working down to the specific details. In practice, this means breaking down a method that contains multiple steps using descriptive helper functions that describe what each step does. This makes the code read like:

public void myMethod() {
    stepOne();
    var items = stepTwo();
    return stepThree(items);
}

This keeps myMethod small and focused on its higher-level logic while abstracting the finer details of each step into helper methods. This should help myMethod read more like a list of easy-to-understand steps, and when you want to delve deeper into how each step works, you can look at each of the helper methods, which stay focused on the logic of that step. You can think of this as similar to how we create new service interfaces to abstract business logic and make other classes more coherent, just on the method level.

Practicing The Stepdown Rule

To show this rule more in detail, I prepared a simple program that we can refactor to showcase how this style cleans up our code:

package io.john.amiscaray.service;

import io.john.amiscaray.domain.Item;
import lombok.AllArgsConstructor;

import java.io.PrintStream;
import java.util.List;

@AllArgsConstructor
public class ItemPrinter {

    private static final Integer TABLE_ITEM_SIZE = 20;
    private PrintStream printer;

    public void printItems(List<Item> items) throws IllegalAccessException {
        var itemFields = Item.class.getDeclaredFields();
        var separatorLength = itemFields.length * 21 + 1;
        for (var field : itemFields) {
            printTableCell(field.getName());
        }

        printNextTableLine(separatorLength);
        for (var item : items) {
            for (var field : itemFields) {
                field.setAccessible(true);
                var fieldValue = field.get(item);
                if (fieldValue instanceof Float moneyValue) {
                    printTableCell("$" + moneyValue);
                } else {
                    printTableCell(fieldValue.toString());
                }
            }
            printNextTableLine(separatorLength);
        }
    }

    private void printTableCell(String item) {
        var padding = TABLE_ITEM_SIZE - item.length();
        var paddingLeft = padding / 2;
        var paddingRight = padding - paddingLeft;
        printer.printf("|" + " ".repeat(paddingLeft) + "%s" + " ".repeat(paddingRight), item);
    }

    public void printNextTableLine(int length) {
        printer.println("|");
        printer.println("_".repeat(Math.max(0, length)));
    }

}
package io.john.amiscaray.domain;

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class Item {

    private String name;
    private String description;
    private Float cost;

}
package io.john.amiscaray;

import io.john.amiscaray.domain.Item;
import io.john.amiscaray.service.ItemPrinter;

import java.util.List;

public class Main {

    public static void main(String[] args) throws IllegalAccessException {
        var itemPrinter = new ItemPrinter(System.out);
        itemPrinter.printItems(List.of(
                new Item("Chicken Breast", "10lbs", 13.99f),
                new Item("Salad", "Caesar Salad", 10.99f)
        ));
    }

}

The class we will be refactoring is the ItemPrinter class. This contains a print method that logs a list of Item objects as a table. To do this, first, it prints the field names of the Item class as the table header (fetching them using reflection), then it prints the field values of each object in the table rows. As you can see, this method violates the stepdown rule by showing specific details of each of its steps within the method body. While this may not be a huge deal with a simpler method like this, for a more complex method, violating this rule can make it very difficult to understand and maintain. Leveraging this rule, we can start to make the method cleaner. First, let's start by separating the steps of the printItems method into helpers:

package io.john.amiscaray.service;

import io.john.amiscaray.domain.Item;
import lombok.AllArgsConstructor;

import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.List;

@AllArgsConstructor
public class ItemPrinter {

    private static final Integer TABLE_ITEM_SIZE = 20;
    private PrintStream printer;

    public void printItems(List<Item> items) throws IllegalAccessException {
        var itemFields = getItemFields();
        var separatorLength = calculateLineSeparatorLength(itemFields);

        printTableHeader(itemFields);
        printNextTableLine(separatorLength);
        printTableItems(items, itemFields, separatorLength);
    }

    private static Field[] getItemFields() {
        return Item.class.getDeclaredFields();
    }

    private static int calculateLineSeparatorLength(Field[] itemFields) {
        return itemFields.length * 21 + 1;
    }

    private void printTableHeader(Field[] itemFields) {
        for (var field : itemFields) {
            printTableCell(field.getName());
        }
    }

    private void printNextTableLine(int length) {
        printer.println("|");
        printer.println("_".repeat(Math.max(0, length)));
    }

    private void printTableItems(List<Item> items, Field[] itemFields, int separatorLength) throws IllegalAccessException {
        for (var item : items) {
            for (var field : itemFields) {
                field.setAccessible(true);
                var fieldValue = field.get(item);
                if (fieldValue instanceof Float moneyValue) {
                    printTableCell("$" + moneyValue);
                } else {
                    printTableCell(fieldValue.toString());
                }
            }
            printNextTableLine(separatorLength);
        }
    }

    private void printTableCell(String item) {
        var padding = TABLE_ITEM_SIZE - item.length();
        var paddingLeft = padding / 2;
        var paddingRight = padding - paddingLeft;
        printer.printf("|" + " ".repeat(paddingLeft) + "%s" + " ".repeat(paddingRight), item);
    }

}

Now, after that refactor, the ItemPrinter#printItems method starts to follow the list of steps format I mentioned above. The inner helper methods help us describe the steps the printItems method takes, making it easier to understand at a glance how it works, while making sure each method focuses on one thing (another rule that Robert advocates for). By making sure each method does one thing, it makes it easier to understand how each of them works, making the whole class easier to understand.

While that one refactor helped a ton, I still have my eye on the printTableItems method for us to refactor. The logic within the loops of checking the field type and printing the appropriate value seems like something we can break down further using this rule:

package io.john.amiscaray.service;

import io.john.amiscaray.domain.Item;
import lombok.AllArgsConstructor;

import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.List;

@AllArgsConstructor
public class ItemPrinter {

    private static final Integer TABLE_ITEM_SIZE = 20;
    private PrintStream printer;

    public void printItems(List<Item> items) throws IllegalAccessException {
        var itemFields = getItemFields();
        var separatorLength = calculateLineSeparatorLength(itemFields);

        printTableHeader(itemFields);
        printNextTableLine(separatorLength);
        printTableItems(items, itemFields, separatorLength);
    }

    private static Field[] getItemFields() {
        return Item.class.getDeclaredFields();
    }

    private static int calculateLineSeparatorLength(Field[] itemFields) {
        return itemFields.length * 21 + 1;
    }

    private void printTableHeader(Field[] itemFields) {
        for (var field : itemFields) {
            printTableCell(field.getName());
        }
    }

    private void printNextTableLine(int length) {
        printer.println("|");
        printer.println("_".repeat(Math.max(0, length)));
    }

    private void printTableItems(List<Item> items, Field[] itemFields, int separatorLength) throws IllegalAccessException {
        for (var item : items) {
            printItemRow(itemFields, item);
            printNextTableLine(separatorLength);
        }
    }

    private void printItemRow(Field[] itemFields, Item item) throws IllegalAccessException {
        for (var field : itemFields) {
            printItemFieldValue(item, field);
        }
    }

    private void printItemFieldValue(Item item, Field field) throws IllegalAccessException {
        field.setAccessible(true);
        var fieldValue = field.get(item);
        if (fieldValue instanceof Float moneyValue) {
            printTableCell("$" + moneyValue);
        } else {
            printTableCell(fieldValue.toString());
        }
    }

    private void printTableCell(String item) {
        var padding = TABLE_ITEM_SIZE - item.length();
        var paddingLeft = padding / 2;
        var paddingRight = padding - paddingLeft;
        printer.printf("|" + " ".repeat(paddingLeft) + "%s" + " ".repeat(paddingRight), item);
    }

}

The printTableItems method is much simpler to understand now that it reads as: "for each item in the items, print the row and a new table line".

One thing that felt unnatural at first is how we are using so many small methods that are not really reusable (a bit of a mindset shift of methods being reusable chunks of logic). However, being able to use these small methods almost as documentation of larger methods makes this mindset shift worth it in my opinion.

Conclusion

With that, I hope you got a good understanding of the stepdown rule and how you can use it to improve your code style. After reading this, I challenge you to start implementing this rule yourself and see how much of a difference it can make in making your code as pretty as it can be. Happy coding!

1 views
One Lesson From Clean Code That Transformed My Coding Style