How to Avoid Static Helper Classes

Static helper classes are classes which contain only static methods, often having names that end in Helper or Utils. The existence of such a class usually indicates a sub-optimal design. In this post I’ll talk about why these classes are a poor design choice in an object oriented language such as Java and how to refactor to a better design.

Why are static helper classes bad?

Static helper classes are bad because they make programs harder to understand (and thus harder to onboard new developers to), lead to bugs because it’s unclear what data they’re meant to operate on, and they make changes harder due to increased coupling.

1. It is harder to understand the code and onboard new developers

Let’s say your code has a Money object, and you need its value in US cents. A helper based solution would look like MoneyHelper.toUsCents(moneyValue), however it’s simpler to write moneyValue.toUsCents(), which will be autocompleted by your IDE. The location of the code is obvious when it is attached to the money object itself, the developer does not need to know about a separate helper class. There is also a real danger of code duplication, a new engineer unaware of the MoneyHelper class may write a new UsCentsUtils class that duplicates the logic. Suddenly your program becomes much harder to maintain.

2. Bugs can arise due to little control over input data

Suppose your program stores usernames as a combination of a display name and a unique identifier, something like “letmebefrank::123”. Many unrelated classes need to work with these usernames and there’s a UserNameHelper class with a getDisplayName(String userName) method that returns a String. The issue is that the Helper encourages dealing with building block data types like String, which are much too general for a domain object like a username. When looking at a userName variable a developer has no idea if UserNameHelper.getDisplayName() has been called yet or not, which can easily lead to bugs and unnecessarily complicated logic. Contrast this with a dedicated UserName object that allows the display name to be unambiguously accessed by a call to userName.getDisplayName().

3. Code can become tightly coupled

Code coupling can easily arise from static helper classes because their methods are visible everywhere and often operate on general data types, like int or String. Going back to the user name example, perhaps the application also deals with products have an ID in this format: “productDisplayName::productId”. An enterprising engineer decided to eliminate the duplication between UserNameHelper.getDisplayName() and ProductNameHelper.getDisplayName() into a more general NameHelper.getDisplayName() method. This works fine until a new service comes along that has a requirement that product display names and productIds can contain ::, so the product naming scheme has to change. Suddenly every call site of NameHelper.getDisplayName() needs to be inspected to determine if it is operating on String representing a product or user, which would be time consuming and error-prone.

How to avoid static helper classes?

When you want to share code between unrelated classes

Static helpers are often created when otherwise unrelated classes need to share common logic. In the user name example many classes need to find the display name of a user. Looking deeper, the problem is not with the classes that need the logic, it is with the data itself. Instead of containing a String that represents the user object, the classes should be refactored to contain a UserName object that has a method for getting the display name. Whenever there seems to be a “need” to create a static helper class, instead see if refactoring the data that the helper will act on solves the problem.

Before
// Some class

   String displayName = UserNameHelper.getDisplayName(userName);

// Meanwhile, in another class

   String displayName = UserNameHelper.getDisplayName(userName);
After
// Some class

   String displayName = userName.getDisplayName();

// Meanwhile, in another class

   String displayName = userName.getDisplayName();
When your method otherwise isn’t testable

Classes can be difficult to test if some of their work involves handing data off to other internal member variables or sending a remote call. While this can be tested using mocks or with integration tests, often developers would like to write a unit test for a specific piece of logic as well. Thus a new static helper is born, whose public methods can be easily tested (or, worse, a method is made public though it really ought to be private). For example, say we have a list of Product objects that we need to group by product category. It is tempting to write a ProductListHelper.groupByCategory() method, but that comes with all the downsides of static helpers. Instead write a new ProductList class to encapsulate the collection and provides a groupByCategory() method (see Encapsulating Collections). This new class can be easily unit tested independently. ProductList can now be passed between classes and methods as a parameter type, encapsulating its data and making all its methods clear via autocompletion. This follows a general theme: create a new class that encapsulates logic with the data it operates on.

Before
   List<Product> products = productLoader.getProducts();
   Map<ProductCategory, List<Product>> groupedProducts = ProductListHelper.groupByCategory(products);
After
   ProductList products = productLoader.getProducts();
   Map<ProductCategory, ProductList> groupedProducts = products.groupByCategory();

When to use static helper classes?

Static helpers are useful for operations on building block data types such as double or List when those operations do not contain business logic (e.g. Math.sqrt()), or can have business logic injected into them (e.g. Lists.transform). Keep in mind that if a method really is generally applicable and domain independent enough to be a good candidate for a static helper, it likely already exists either built in to Java or in a library like Guava. Should Double implement 50 different math functions? Probably not, that responsibility can live elsewhere. Should Product implement getProductName()? Absolutely!

Takeaway

The key theme of this article is encapsulate method logic in a class with the data it operates on. Following this design principle leads to more readable, more maintainable software. Next time you find yourself “needing” a static helper (or wanting to get rid of an existing helper), try applying these patterns. Encapsulating logic with the data it operates on instead of exposing it via a static helper can be the difference between sustained feature velocity and an unmaintainable mess that all fear to touch.




Leave a Reply

Your email address will not be published. Required fields are marked *