Object Oriented Programming: Objects as utilities

❝Utilities are more than just a bunch of static methods with a common target.❞
Contents

In the article on Utilities we discussed how utilities, and more specifically, utility methods help to reuse common usage patterns. In this article we explore some of the more elaborate constructions for utilities.

Utility methods are lacking portability, persistence, flexibility

Utility methods as described in an earlier article are very much like functions. In the paradigm of object oriented programming, they are somewhat limited, though:

  1. No portability of logic
    With static methods, the only dynamic aspects of its execution are the input parameters. This limits the usefulness as you would need to provide all arguments at the moment of execution. There is no way to prepare code for execution at a later moment in time.

  2. No flexibility of logic that is executed
    With static methods, you call the method and you immediately invoke the exact method. There is no variation in what gets executed. The only factors you can influence are the input parameters. That means that, using static utility methods, there is no dynamic, i.e. at run-time, choice of behavior.

  3. Not configurable to context
    Static methods are limited to local state of their current execution in what they can store. Every dynamic aspect of the utility method is determined through its defined parameters. That means that if utility methods are applied in a certain context, then all parameters of the context must be provided at every call to the utility method. There is no configurability, no memory, for a utility method.

Items 1 and 2 are counterparts of the same characteristic: making code portable means that with copying/moving the logic, you influence the “receiver”. Depending on your perspective, relevance is in defining the behavior that is provided for use (item 1, provider’s perspective) or design the logic to allow for an influence through logic provided by the caller (item 2, consumer’s perspective).

Item 3 is about having no persistent state in which we can store configuration. As a consequence, every method call to a utility method is independent and isolated. However, sometimes we expect a utility to behave a certain way and the ability to configure a utility before actual use is valuable.

This preconfiguration sets the operating parameters to match the current context before the utility is actually used. It allows us the reduce the number of parameters on the methods to those parameters that vary with each call. The context - whose parameters are fixed even for repeated use - can be left out when calling a method.

Any one of these characteristics may be needed. There is no reason why all 3 characteristics need to be present.

Objects as utilities

Objects by definition combine state + logic, providing a portable unit. In case more complicated utilities are required, an object is the logical next step. On a single utility object, we might call multiple methods (either different methods or the same method multiple times).

In article Object Oriented Programming: Expectations we distinguish between internal state and expectations. The expectations are what enables us to preconfigure/prepare an object for later use. This allows us to - at runtime - set the final parameters to the logic. Since we create the object at one location, then pass it on to another location for actual usage, we create a disconnect between creation and use, which makes the utility portable. It also means that the user of a utility need not know its configuration parameters. The disconnect is not only a temporal disconnect between moment of creation and moment of use (or we could think of it as separate locations in code), but it extends to separation of concerns. For the latter, we require both properties to be available: portability and configurability.

Utility methods provide accessibility, utility objects provide portability.

For the purpose of objects as utilities, we limit the object to containing expectations only. Once we introduce internal state to an object, the object will need to preserve consistency.

Utility objects

a.k.a. helper objects

Utility objects are similar to utility methods. Utility objects have the following properties:

  1. No internal state
    Note that by allowing internal state, an object will implicitly have its own concerns about managing state. If this is the case, we are not talking about a “mere” utility anymore, but a full-fledged object. Implementation logic for utility objects is reduced to using expectations, as these provide information on how the method should operate.
  2. Portability
    Utility objects are instances, so they can be handled as such: use and pass on.
  3. Configurable
    At construction time, we can specify “operational parameters” that will be enforced throughout the use of the utility object.

The advantages of utility methods are preserved. The same advantages still hold, however they are slightly weakened due to the need to instantiate such an object and provide expectations before use.

Orthogonality of accessibility and portability

We have looked at accessibility and portability. (Static) Utility methods make usage patterns easily accessible. Objects make utilities portable. These characteristics are orthogonal. A utility method is by definition not portable. It has a fixed location and logic and is only as “dynamic” as the parameters it defines. But the usage pattern is reusable as we can call it at any time, exactly due to this static nature. There is no need for any preparation in advance. Objects on the other hand are portable but must be constructed in advance. Portability is useful only if you need to match the utility logic to the current context.

Utility methods and objects provide solutions on different dimensions of the problem. They need to be combined accordingly in order to come to a fully capable solution that works for all relevant scenarios that are composed of requirements in both the accessibility and portability dimension. Utility methods and objects are truly complementary. Objects may, for example, call on static utility methods for the actual (usage) logic of the methods.

Examples of objects as utilities

There are a many examples of objects as utilities. Many of these objects follow predefined interfaces, because the concept of portability allows for the logic defined by the caller to be executed by the callee. Many utilities you will encounter are very simple objects. They implement a single interface with little or no internal state. These objects are merely vessels for the logic that we as callers must provide.

Some examples of utilities, by no means exhaustive:

Notice that these utility objects are often defined for very specific problems and as such are closely related to the type to which the utility is applied and possibly also the context in which the utility is used. The utility is there to provide the logic.

Take the Comparator, for example. It contains the logic with which we can compare objects of a certain type. If necessary, the comparator may include some expectations to define exactly how it should operate. These operational parameters are within bounds of course, as requirements set by the Comparator interface take precedence over any expectations.

Many of the Gang of Four-design patterns are utilities applied to fix very specific problems. These utilities only rely on public API to perform their function. Note also that many patterns of the Gang of Four rely merely on interfaces, i.e. defining a common guaranteed API, and plain object composition for their patterns to work. There is no reliance on (complex) implementation logic or inheritance (which actually is implied by implementation logic).

Most of these utilities would be implemented as their own (small) object. This can have various reasons. There could be any number of implementations possible and the implementation must match the context as defined by the caller, so we instantiate the one we need. It might be applicable only on a very short term or in a very specific situation.

Extending the usefulness of objects

We have illustrated which rules/guidelines we can use to minimize the amount of state and logic of an object. There is, however, a flip side. What do we do if, in order to extend the use of an object, we need to implement additional interfaces? Or if we need modified features for a particular application?

Application of design patterns

There are circumstances that would indicate that an additional interface should be implemented. If this interface does not match the original intentions of the concept that is implemented, then this will only increase the amount of overhead and complexity.

This situation is by no means exceptional. Different libraries can define slightly different interfaces even if they are meant to represent the same concept. Instead of attempting to implement all interfaces at the same time on the same object, there is another solution. The Adapter pattern facilitates in the conversion from one public API (either an interface or concrete type) to another interface. This is a very low barrier solution and as such should be preferred over inheriting from or extending an object, because it concerns only the public API.

A similar form of adaptation can be achieved with a decorator (a.k.a. wrapper). This pattern does not actually convert from one interface to another, but rather to the same interface. The added advantage is that it is possible to influence method calls - in whatever way suitable - as they “pass through” the decorator. A well-known example is the Unmodifiable List (as discussed in an earlier article on restricting usage capabilities as part of restricting access) that throws an UnsupportedOperationException for any method that would otherwise have modified the original list’s contents.

Design patterns like these are typical examples of utilities. They only rely on the public API and make small modifications to accommodate for a particular situation. It is a very low-impact solution for adapting types without the need to extend the original object, thus avoiding the risk to corrupt and/or complicate the object’s implementation logic. At the same time, this utility can be reused for different objects as long as they extend the interface that the utility relies on.

Utility objects over utility methods?

There have been comments on how static utility methods are an issue and grossly interfere with testability. However, static utility methods are a direct replacement of in-line logic. So, this should not be any more of an issue than when the logic is written in-line in the calling method.

Utility objects are better for:

  1. Usage patterns with context-dependent logic
  2. Preventing repeat of method arguments (context persisted at creation time)
  3. Making (existing) logic portable

Utility methods are better for:

  1. Context-free usage patterns
  2. “Every-day” use of usage patterns
    Reduced dynamic behavior, thus reduced chance of errors.
  3. Making common usage patterns accessible

Note that Java 8’s default methods, Kotlin’s extension methods are effectively improving support for utility methods. That should count as an endorsement for utility methods.

If-less programming?

With the added level of abstraction, we can create “portable” logic. This portability can be used to create “if-less” constructions. For every case where we normally would use an if-statement, we can ask the caller to provide the logic such that the choice is made by the caller.

If-less programming is kind of the extreme example where we would use implementation objects for each if-statement. In a sufficiently large application, choices need to be made at some point, so we merely move the choice to a different point in code. The choice has to be made at some point.

A better example of an “if-less construction” is the State pattern. Using the State pattern we can group logic that is all tied to the same internal state. When we do not use the State pattern, we will find a large number of identical (or at least very similar) if-conditions. The reason for this is that implementation logic must behave differently based on the state the object is in. The State pattern allows you to group logic with the relevant state composition. Therefore, all logic in a single state implementation is always applicable. Each of the implemented states contains only the relevant logic, completely removing the need for if-conditions that ensure the appropriate state composition.

If-less programming can be value but is no panacea for eradicating branching entirely. The State pattern is a very valuable pattern to simplify code, but it will only work if we can identify multiple (mostly disjunct) internal states.

Better testable?

The fact that utility objects are easily replaceable is used as an argument to always use objects over static methods. This leans heavily on the capability of replacing an object with a mock object that can be scripted to return the desired result. A static utility method is less flexible in this way. This is obvious as utility methods are there for accessibility not portability. Utility methods are there to make usage patterns easily accessible. Alternatively, you could have written the same logic in-line. Testability is not so much affected by the use of a static methods, but rather by the use of complicated objects that are passed to utility methods. A static utility method is very easily testable exactly because it is accessible and pure.

What is affected is the fact that, due to a lack of flexibility in replacing implementations (as is provided with the use of objects), we cannot reduce the extent to which the method is executed. If within the logic of a method, we call 5 other utility methods, then these utility methods will be called. With the use of objects, this is avoided because we can replace those with scripted mocks. Although this is a reasonable solution for reducing the scope, it does so by testing with an alternative implementation.

The effectiveness of the test depends on the accuracy of the scripted mock. The logic that is executed in production, may deviate because it is an alternative implementation to the one we tested with. This is also what causes issues where the more tests you write, the more mocking behavior needs to be maintained to match possibly changing original implementation logic. This is an unfortunate penalty that exists purely because mocks replace original logic.

Mocks are used often for testing, since classes typically are extendable. One of the recommendations when programming Java, is to either design a class for extendability or to prevent classes from being extended. For the latter case, i.e. declaring classes as final, we prevent mockability as we prevent extendability. And this would be - by far - the most prevalent construct. Even though mocking can be convenient, it is not the “go to” solution for every case regarding testing. We should not rely solely on mocks for writing tests.

General guideline

Utility objects help to preserve the “single concern” constraint of the original object. The implementation logic is focused on executing the usage patterns and including expectations at appropriate moments. To make a utility optimally reusable we allow for configuration at time of construction. By defining the minimally required interface for its use, we ensure maximal reuse for other implementing objects.

We prevent further complication by avoiding inheritance for anything that depends only on the public API. Specific to utilities, we do not allow for internal state even in utility objects, as that introduces a concern (management of internal state) to the utility.


This post is part of the Object Oriented Programming series.
Other posts in this series: