Reputation: 5049
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
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
getFoos(randomOrLinked)
getFoosAsArrayList(), getFoosAsLinkedList()
getFoos(ArrayList::new)
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
Reputation: 7456
You need to make a compromise somewhere. In this case, there are two compromises that make sense to me:
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.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
Reputation: 58909
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