Object Oriented Programming: Concrete types in implementation logic

❝Use of concrete types in implementation logic.❞
Contents

We have looked quite extensively at the difference between implementation logic and usage logic. There is a similar difference using concrete types versus using interfaces. When do you use the concrete type and when do you generalize to the interface? With this article, I will go into the specific use case of concrete types in implementation logic.

Let’s start with a few questions. When you create a new local ArrayList instance, do you declare the variable as the generic List type, or do you use its concrete type ArrayList? And what do you use in case of a field variable in a class? How about when you return an instance from a method? And what about when you receive an instance through a parameter of a method?

Abstracting from concrete types

The reason to abstract away from a single, concrete type is to preserve some flexibility for changing and improving an implementation. Furthermore, it allows you to keep your implementation private. The advantage to this is that usage logic is not forced (by the type system) to be changed, if the type changes in the implementation.

The choice to use an interface follows naturally when you know that your code is going to be called by usage logic. Usage logic may be outside of our own control, and as such we want to preserve backward compatibility. Interfaces allow you to switch implementations without forcing the user to change the usage logic, as long as all implementations satisfy the declared interface type.

We choose an interface that describes the guarantees that we want to provide most accurately. That includes leaving out those guarantees that we do not want to set, even if they are provided due to the current choice of implementation. For example, a list provides an ordered collection of elements and may contain duplicate elements. If order is significant in your logic, then a list is a suitable type. Your user will be working with the instance given its defined type, so the type must be sufficiently accurate to be useful.

State management

This section relies on the notions of internal state and expectations as previously introduced in Object Oriented Programming: Expectations - The Silent Third Characteristic.

In state management, things are different. We have a dedicated bunch of implementation logic whose sole focus is on the implementation of features of the object while preserving state consistency. Implementation logic is always tied to the single state that it manages. Furthermore, the implementation logic is expected to have complete control over this state.

Internal state management

First we will look at internal state. Internal state is managed completely by the implementation logic. We expect consistency to be preserved at any time. And we expect the implementation to be as effective and efficient as possible. To establish an implementation where you have full control over the internal state, you need to preserve the concrete types of the objects in the internal state. That way you can optimally control the implementation, and - more importantly - you have access to all the advantages offered by the selected concrete type. Remember, even if you choose to keep the reference based on the interface type, you will still need to live with all the disadvantages of the chosen concrete type.

As said before, working with the concrete type does mean that with any change of the chosen concrete type, you would need to modify the rest of your code. This means that apart from changing the instantiation and the field declaration, you might also need to make changes in your implementation methods. This is restricted to your implementation methods, though, since we are talking about internal state and that is restricted to the implementation methods, i.e. constrained to the one class. Furthermore, with the need to change the implementation logic, you will encounter all the hot spots in your logic where you need to consider whether the newly selected concrete type will fit.

Let’s illustrate this with an example. A well known implementation in Java called ArrayList. ArrayLists implement the List interface. However, features that are tied to this array-based list implementation are not exposed in the List interface. For example, let’s look at the methods trimToSize and ensureCapacity, which are both specific to the ArrayList.

trimToSize

trimToSize allows you to reduce to amount of capacity in the backing array to exactly the amount needed for the contents of the ArrayList. This is convenient, but in itself not spectacularly useful. However, the remove, removeAll, clear, etc. methods (which is are all defined in the List interface) for this concrete type will not reduce the capacity of the backing array. This is an implementation detail for this specific concrete type, as this behavior is accepted according to the List interface definition.

If we are working with large lists or we tend to add and remove large batches of data, the amount of reserved memory backing the ArrayList will only grow. This is not obvious from the List interface, as this is specific to the concrete type or rather it’s implementation. trimToSize is available in that case to fit the backing array once we determine for ourselves we are done using the ArrayList for “intense modifications”. We can control this and we can determine when it is the appropriate time to call trimToSize, as we provide the implementation logic for our object.

ensureCapacity

ensureCapacity is a convenient method that allows us to tell the ArrayList in advance how many elements it should expect. ArrayLists automatically grow as they are used and more capacity becomes necessary, however ArrayLists start small as not to waste too much memory. In case we know in advance how many elements we need the ArrayList to hold after we are done (i.e. it might take any number of additions before we finally arrive at this number of elements), we can ask it to grow immediately to that size. The ArrayList will grow instantly instead of with multiple incremental steps. Thus we preserve an amount of processing and memory copying overhead that the ArrayList cannot determine, as that would mean predicting usage logic in advance.

Expectations

For expectations this is completely opposite. Expectations are provided by the application/context. We request them in order to interoperate with the “outside world”. This is purely a usage-relationship. We use an interface type to express the required features that we need in order to use it. We can store the reference using the same interface type.

In the implementation logic we may use this expectation and it is sufficient to know the usage contract in order to use it, whatever the concrete implementation backing this usage contract may be.

Methods

We also use types inside methods. So do we use concrete types or interface types inside these methods? And what about method boundaries, both input parameters and return types?

Implementation logic in methods

Methods may use other types in support of the implementation logic. As with internal state, there is no real reason why we need to abstract away from the concrete type. We use the object in preparing a result to update the internal state as per request of the user, or to process requested data from internal state in preparation of the return value to send back to the user.

In the article on utilities we look into making (context-free) “usage patterns” accessible for reuse in multiple locations. These utilities can be used in implementation logic. Given that we are now using a utility method, it makes sense that the utility returns the result through an interface type instead of the concrete type used in the logic.

Input parameters

Input parameters are the entry point to implementation logic for users of an object. The input parameters represent the requirements for (successfully) executing this implementation logic. For each requirement, the chosen type represents the expectations for the data. It serves you well to think about what demands you would want to put on the requirements.

Let’s say you sum a list of doubles. You already possess a list of doubles and now you write the logic for summing this list. The naive first idea would be to write the following method. (Note that for the purpose of generality I am not using the Java stream API in this example.)

public class Test {

  public static void main(String[] args) {
    final ArrayList<Double> list = new ArrayList<>();
    list.add(1.3);
    list.add(1.4);
    list.add(1.7);
    list.add(2.3);
    naiveSum(list);
  }

  public static void naiveSum(final ArrayList<Double> values) {
    double sum = 0;
    for (final double d : values) {
      sum += d;
    }
    System.out.println(Double.toString(sum));
  }
}

naiveSum is a perfectly valid (static) method. However, there are a number of considerations.

Next, let’s look at a more flexible implementation.

public class Test {

  public static void main(String[] args) {
    final ArrayList<Double> list = new ArrayList<>();
    list.add(1.3);
    list.add(1.4);
    list.add(1.7);
    list.add(2.3);
    flexibleSum(list);
  }

  public static void flexibleSum(final Iterable<Double> values) {
    double sum = 0;
    for (final double d : values) {
      sum += d;
    }
    System.out.println(Double.toString(sum));
  }
}

Not only do we make the method more generally applicable, i.e. more types of objects can be provided as the input argument, such as a Collection, Set, etc. You also weaken the requirements itself, thus making it easier for anyone to implement support for the requirements in their custom type if required. Iterable specifies 1 method iterator() that should be implemented, while List specifies 25 methods. (I won’t bore you by listing them all.)

Many languages require you to declare implementation of an interface at compile time. Therefore, there is a little bit less effort involved for built-in types such as the one provided by Java. These have the advantages that they are quite ubiquitous, but in general it is simpler to implement a smaller and simpler interface.

Arguably we could go as far as to accept the Iterator itself, instead of the Iterable. I do not have a good argument for why you should definitely not do that. The only thing I can say right now, is that it is more convenient if you do not need to use the iterator yourself, as this would prevent you from using the for-each language construct.

Return type

As noted in an earlier section, we typically use interfaces to hide the concrete type as to allow ourselves the flexibility to make changes. The return type of a (public-facing) method is on the boundary between the implementation logic and the usage logic. The interface abstracts away from the concrete type used in the implementation as a way to weaken the ties between the usage and the implementation logic.

Choose a return type that most accurately declares all the capabilities of the result that you wish to provide and hides all the implementation details that you wish not to expose.

In case your result is of type ArrayList, you might want to use the generalized interface List as the return type. This way, you provide all of the conceptually relevant features of the result, without allowing the user to depend on the implementation details of the ArrayList type. If at any point you wish to switch to a different implementation, then you are bound only to the guarantees of List, not every (potentially undesirable) implementation detail of ArrayList.

The return type should represent what the return value conceptually represents. In case your package has a specific concrete type, then that might very well be conceptually a perfect fit.

When to use interfaces

The general idea is to go with setting minimal necessary requirements. So, should we use an interface for everything? No, that is certainly not the case.

Your type, as with your usage patterns, are related to the concept that they represent. If we are working on code that works with Lists because the order is significant, then we cannot rely on the more generic type Collection, even though we are allowed to iterate over that. The reason is that we rely on the order to be preserved. This might be the case for a collection, as we cannot be sure what the concrete type is, but it is not guaranteed. The usage pattern might fail if the underlying concrete type does not offer an ordering guarantee. So we choose our interfaces according to guarantees that are necessary. By selecting the best matching interface in terms of features, we can be sure that the language’s type system will enforce it.

Does that mean that we never use a concrete type? No, this is not the case. If only 1 implementation exists, then there might not be a reason for an interface to exist. A concrete type is perfectly valid if it represents the concept. In some cases we implement our own type for a very specific purpose. Then the concrete type is the only perfect match. This is most obvious if we use the type within the package it is defined in. It makes sense that the package that provides the type, will also accept it.

Typically there are two reasons for an interface to exist:

  1. There are multiple implementations available.
    Some code is generalized to work with both implementations by implementing the logic based on the public API that they have in common as defined by the interface.

  2. There is no implementation available.
    In some cases, we request the user to provide an implementation that matches the interface. It is directly related to the use case of the user Therefore, it is not possible to create a (sufficiently generic) implementation that will fit the user’s use case for all possible variations. Instead, we withhold from defining any implementation and instead define the features that we expect of this object: the interface.
    Furthermore, not defining an implementation may help significantly in reducing the complexity of your package, as this may not even be your primary concern.

Furthermore, we can always evolve our code by extracting an interface from the concrete type. We let the interface assume the class name of the concrete type and we create a new concrete type that implements that interface. All of the original logic using the concrete type now is bound to the extracted interface. As long as the logic uses the original type’s public API, then all of these method should now be defined on the interface. As for the concrete type, it is now available with a new name and can be applied everywhere where the interface is requested.

Usage logic in between implementation logic

There is a subtlety with the logic in methods. Often times we write small pieces of usage logic “in line” with the implementation logic. If you would extract those parts of usage logic, for example move them to utility methods, you would typically declare an interface return type for these results. Now that the code is in line with the “using” implementation logic, there is no use for that.

Keep in mind though, if there is a significant amount of usage logic in your class, you might at some point want to split it off to keep the amount of logic in your object manageable. An indicator for this is often when you start to reason about private methods “… that should really return an interface instead of a concrete type”. These private methods will (likely) not use any internal state. (Or can easily be abstracted away from using internal state.) It is an indicator that there is probably a significant amount of usage logic that has nothing to do with the concerns of the object.

Conclusion

Use concrete types in internal state and implementation logic. Use interfaces for expectations, input parameters of public-facing methods and return types of public-facing methods where applicable. If there exists no suitable interface and there is one concrete type that is a perfect representation, then the concrete type is likely the best candidate. There is no need to invent an interface for that, as the concrete type suffices.


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