Aleksandar Stojadinovic
Aleksandar Stojadinovic

Reputation: 5049

Avoiding semantic coupling with Java Collection interfaces

These days I am reading the Code Complete book and I've passed the part about coupling-levels (simple-data-parameter, simple-object, object-parameter and semantic coupling). Semantic being the "worst" kind:

The most insidious kind of coupling occurs when one module makes use not of some syntactic element ofanother module but of some semantic knowledge of another module’s inner workings.

The examples in the book usually lead to run-time failures, and are typical bad code, but today I had a situation that I'm really not sure how to treat.

First I had a class, let's call it Provider fetching some data and returning a List of Foo's.

class Provider
{
   public List<Foo> getFoos()
   {
      ArrayList<Foo> foos = createFoos();//some processing here where I create the objects
      return foos;
   }
}

A consuming class executes an algorithm, processing the Foo's, merging or removing from the List based on some attributes, not really important. The algorithm does all of it's processing with the head of the list. So there is a lot of operations reading/removing/adding to the head.

(I just realized I could have made the algorithm looking like merge sort, recursively calling it on halves of an array, but that doesn't answer my question :) )

I noticed I'm returning an ArrayList so I changed the providing class' getFoos method to return an LinkedList. The ArrayList has O(n) complexity with head removals, while LinkedList has constant complexity. But it then struck me that I am possibly making a semantic dependency. The code will certainly work with both implementations of List, there are no side-effects, but the performance will also be degraded. And I wrote both classes so I can easily understand, but what if a colleague had to do implement the algorithm, or if he uses the Provider as a source for another algorithm which favors random access. If he doesn't bother with the internals, like he should not, I would mess up his algorithm performance.

Declaring the method to return LinkedList is also possible, but what about the "program to interface" principle?

Is there any way to handle this situation, or my design has flaws in the roots?

Upvotes: 2

Views: 281

Answers (3)

ZhongYu
ZhongYu

Reputation: 19682

The general problem is, how does a producer return something in the form that the consumer prefers? Usually the consumer needs to include the preference in the request. For example

  1. as a flag - getFoos(randomOrLinked)
  2. different methods - getFoosAsArrayList(), getFoosAsLinkedList()
  3. pass a function that creates desired List - getFoos(ArrayList::new)
  4. or pass a desired output List - getFoos(new ArrayList())

But, the producer may have the right to say, this is too complicated for me, I don't care. I'll return a form that's suitable for most use cases, and the consumer needs to handle it properly. If I think ArrayList is best, I'll just do it. (Actually you may have a better choice - a ring structure - that suits both of the two use cases in consideration)

Of course, it should be well documented. Or you could be honest and return ArrayList as the method signature, as long as you commit to it. Don't worry too much about "interface" - ArrayList is an interface (in the general sense), Iterable is an interface, so what's so special about the List interface that's between the two.

There can be another criticism on your design - you return a mutable data structure so that the consumer can directly modify. That is less desirable than return a read-only data structure. If you could, you should return a read-only view of the underlying data; the construction of the view should be inexpensive. The consumer needs to do its own copy if it needs a mutable one.

Upvotes: 4

Dathan
Dathan

Reputation: 7456

You need to make a compromise somewhere. In this case, there are two compromises that make sense to me:

  1. If you don't know that all consumers of your Provider will be performing operations that are appropriate to a LinkedList, then stick with the signature that has return type List with implementation that returns ArrayList (ArrayList is a good all-around List implementation). Then within the calling method, wrap the returned List in a LinkedList: LinkedList<Foo> fooList = new LinkedList<>(provider.getFoos()); Make the caller responsible for its own optimizations.
  2. If you know that all consumers of your Provider are going to use it in a LinkedList-appropriate way, then just change the return type to LinkedList -- or add another method that returns a LinkedList.

I strongly prefer the former, but both make sense.

Upvotes: 3

You could have the caller create the LinkedList, instead of the provider - i.e. change

List<Foo> foos = provider.getFoos();

to

List<Foo> foos = new LinkedList<>(provider.getFoos());

Then it doesn't matter what kind of list the provider returns. The downside is that an ArrayList is still created, so there is a tradeoff between efficiency and cleanliness.

Upvotes: 2

Related Questions