There you go once again, while performing a code review or after having justified a quick coding in the name of urgency and priority: it clearly stand in front of you yet another helper class. But everything works fine and the show must go on, release after release, so that helper class soon becomes a monster class, providing tons of static methods, freely growing in its utils package, often a no man land of technical debts where the object oriented design didn’t dare to step in. The provided facility is centralized and hence DRY – would shout some developer, may be its coder. It’s fast because everything is static – may claim somebody else within the team, may be the one who added another static method in its list. It’s easy to use, we kept it simple – you could hear in the room, yet another misunderstanding of KISS.
We could argue that often helper and utils classes come to hand really easily, especially when we can’t modify the right target class of the new functionality (located in an external library, for instance) or we actually can’t find that target (unclear domain model, PoC, lack of requirements), or we simply don’t want to find it (laziness, the main global source of helper classes). The big problem though is that’s clearly not an object oriented solution and by the time (and by lack of team communication, resources rotation, quick fixes and workarounds) it could lead to endless containers of static methods and maintenance headaches (you wanted to be DRY, but you end up having ten methods providing almost the same functionality, if not the same; you wanted to be fast, but now you can’t easily add a cache mechanism in that static monster or you get in troubles with concurrency; you wanted to keep things simple, but your IDE now provides a long list of heterogeneous methods which doesn’t simplify your task). But don’t worry, we’ll try to solve it.
Let’s refactor that helper class
Firstly, we need a definition of our target problem: a stateless class (with that special Helper or Utils suffix) which provides only static methods, never instantiated as an object in the project, without a clear responsibility.
Secondly, we need an almost* deterministic approach to solve the problem. That almost stands for exceptions and project peculiarity: the final decision really depends on specific scenarios which would just vanish any claim of universal solution. We’ll need eventually to analyse the given class and try to:
- Find the target class to which a certain static method should belong or
- Find the target business domain which the class actually provides and hence transform it in a related component, renaming it and removing static methods (replacing them by behaviours), or
- Add a new class providing one or more behaviours (a previously existing static method) via an object oriented approach.
Any of the above solutions would provide us a better model. Let’s then get to the point through the following steps (assuming project refactoring accordingly for each step):
- To facilitate our task, let’s remove any unused method of our helper class within the project (your IDE will definitely help you).
- Let’s then set the class definition to final. Did you get any compilation error within the project? If yes, why that helper or utils class was extended? You may already have a target though: the child class. If the child class is yet another helper class (really?), merge it with its parent.
- If not already there, let’s add a private constructor to the class. Did you get any compilation error within the project? Then somewhere the class was actually instantiated, hence it wasn’t a pure helper class or it wasn’t used correctly. Look at those callers, you may spot a target class (or domain) to which a method or a whole set of methods may belong.
- Let’s group the class methods by a certain affinity, similar signatures, breaking down the helper class in smaller helper classes (from miscellaneous to correlated methods, that affinity may be our target domain indeed). Often at this point we would move from a large utils class to lighter helper classes (tip: don’t be afraid of creating classes with just one method at this point), narrowing our scopes (from ProjectUtils to CarHelper, EngineHelper, WheelHelper, etc.). (Hey, isn’t your code already getting cleaner?).
- If any of these new classes has only one method, let’s check its usage. If we got only one caller, lucky you, that’s our target class! You could move the method to that class as behaviour or private method (keeping its static marker or getting advantage of internal state). The helper class disappeared.
- In each helper class we got so far (it could actually be your starting point though) identify a common state among these correlated methods. Tip: look for a common parameter most of those methods have (i.e. all methods take as input a Car object), that’s an alert, these methods should probably belong to the Car class (or an extension? A wrapper?) as behaviours. Otherwise, that common parameter may become a class field, a state, which could be passed to a constructor and used by all the (non static any more then) methods. That state would suggest you the prefix of the class, methods affinity could suggest a class of behaviours (CarValidator, CarReader, CarConverter and so on). The helper class disappeared.
- If the family of methods uses different parameters, depending on optional input or representations of the same input, then consider transforming the Helper via a fluent interface using the Builder pattern: from a collection of static methods like Helper.calculate(x), calculate(x, y), calculate(x, z), calculate(y, z) we could easily get to something like newBuilder().with(x).with(y).calculate(). The helper class would then offer behaviours, reduce its list of business methods and provide more flexibility for future extensions. Callers would then use it as internal field for reuse or instantiate it where needed. The helper class (as we knew it) disappeared.
- If the helper class provides methods which are actually actions for different inputs (but, at this point, for the same domain), consider applying the Command pattern: the caller will actually create the required command (which will handle the necessary input and offer a behaviour) and an invoker will execute it within a certain context. You may get a command implementation for each static method and your code would move from an Helper.calculate(x, y), calculate(z) to something like invoker.calculate(new Action(x, y)). Bye bye helper class.
- If the helper class provides methods for the same input but different logics, consider applying the Strategy pattern: each static method may easily become a strategy implementation, vanishing the need of its original helper class (replaced by a context component then).
- If the given set of static methods concerns a certain class hierarchy or a defined collection of components, then consider applying the Visitor pattern: you may get several visitor implementations providing different visit methods which would probably replace partially or entirely the previously existing static methods.
- If none of the above cases met your criteria, then apply the three most important indicators: your experience, your competences in the given project and common sense.
The process is pretty clear, looking for the right domain and a reasonable target class or considering refactoring the given helper class via a standard approach applying an object oriented design (with an increased code complexity in some cases though, worth it?). Going through the list of aforementioned cases, more then one bell would probably help you while trying to understand how to accomplish this often not too easy refactoring; specific constraints may limit certain solutions; complexity of static methods and involved flows may require several phases of refactoring, refining it till an acceptable result. Or you may choose to stick with that helper class (hopefully applying at least the first 5 steps above), in the name of code readability and simplicity, to a certain extent. Helper classes are not universally evil, but too often you don’t actually need them.