Inheritance, composition and default methods
Asked Answered
V

5

11

It is usually admitted that extending implementations of an interface through inheritance is not best practice, and that composition (eg. implementing the interface again from scratch) is more maintenable.

This works because the interface contract forces the user to implement all the desired functionality. However in java 8, default methods provide some default behavior which can be "manually" overriden. Consider the following example : I want to design a user database, which must have the functionalities of a List. I choose, for efficiency purposes, to back it by an ArrayList.

public class UserDatabase extends ArrayList<User>{}

This would not usually be considered great practice, and one would prefer, if actually desiring the full capabilities of a List and following the usual "composition over inheritance" motto :

public class UserDatabase implements List<User>{
  //implementation here, using an ArrayList type field, or decorator pattern, etc.
}

However, if not paying attention, some methods, such as spliterator() will not be required to be overridden, as they are default methods of the List interface. The catch is, that the spliterator() method of List performs far worse than the spliterator() method of ArrayList, which has been optimised for the particular structure of an ArrayList.

This forces the developer to

  1. be aware that ArrayList has its own, more efficient implementation of spliterator(), and manually override the spliterator() method of his own implementation of List or
  2. lose a huge deal of performance by using the default method.

So the question is : is it still "as true" that one should prefer composition over inheritance in such situations ?

Veil answered 6/7, 2015 at 9:20 Comment(11)
To some extent, favoring composition over inheritance has to do with a better implementation strategy (you mentioned some problems). But the real value is the conceptual expressiveness: A database is not a list! This leads to a far better API of your database class.Crucify
This is not the point I'm making, I chose the word Database, but it could have been anything closer to an actual list, say GroceryList.Veil
Actually the sample you brought here has nothing to do with 'composition over inheritance'. What that phrase means, is that instead of extending a class using 'extends' , you should hold an instance of that class and use it where appropriate.Electrocute
Benefit of composition over inheritance is mainly flexibility, so the problem you mentioned doesn't affect the benefit. Be aware that composition over inheritance is not a matter of truth, it is more adequate or not in a given situation.Concierge
so you choosed ArrayList, because you know it is good, but you are too lazy to override default methods and expect something from language designers?Hui
if you want better results, you need to better know the code you use, there is nothing new in default methods, that make contradictions to this statementHui
As @fatman pointed out your example of composition should have been public class UserDatabase{ List<User> aList = new ArrayList<>(); //Other code}Operative
Note that the default spliterator becomes "far worse" only for parallel processing. For sequential it's not much worse than ArrayList spliterator.Ferous
@UlysseMizrahi I don't understand your dilemma. Why does the fact that ArrayList has a faster implementation of splititerator affect your decision to chose between composition over inheritance? What has implementation details got to do with choosing between inheritance and composition? What has default methods got to do with choosing between the two? By relying on implementation details for choosing composition, you kind of defy the whole purpose of composition in the first place. Don't confuse yourself. Keep it simple silly.Raspberry
If you are composing over ArrayList, why couldn't the spliterator() method of UserDatabase delegate to the spliterator() method of the underlying ArrayList?Parliamentarian
@Brian Goetz: as far as I understood, it’s a theoretical question concerning about the possibility that this delegation does not happen because a) the developer missed that or b) such a default method has been added after the delegation code has been written. So the spliterator() is only an example to discuss the design pattern in general.Tripe
T
6

Before start thinking about performance, we always should think about correctness, i.e. in your question we should consider what using inheritance instead of delegation implies. This is already illustrated by this EclipseLink/ JPA issue. Due to the inheritance, sorting (same applies to stream operation) don’t work if the lazily populated list hasn’t populated yet.

So we have to trade off between the possibility that the specializations, overriding the new default methods, break completely in the inheritance case and the possibility that the default methods don’t work with the maximum performance in the delegation case. I think, the answer should be obvious.

Since your question is about whether the new default methods change the situation, it should be emphasized that you are talking about a performance degradation compared to something which did not even exist before. Let’s stay at the sort example. If you use delegation and don’t override the default sorting method, the default method might have lesser performance than the optimized ArrayList.sort method, but before Java 8 the latter did not exist and an algorithm not optimized for ArrayList was the standard behavior.

So you are not loosing performance with the delegation under Java 8, you are simply not gaining more, when you don’t override the default method. Due to other improvements, I suppose, that the performance will still be better than under Java 7 (without default methods).

The Stream API is not easily comparable as the API didn’t exist before Java 8. However, it’s clear that similar operations, e.g. if you implement a reduction by hand, had no other choice than going through the Iterator of your delegation list which had to be guarded against remove() attempts, hence wrap the ArrayList Iterator, or to use size() and get(int) which delegate to the backing List. So there is no scenario where a pre- default method API could exhibit better performance than the default methods of the Java 8 API, as there was no ArrayList-specific optimization in the past anyway.

That said, your API design could be improved by using composition in a different way: by not letting UserDatabase implement List<User> at all. Just offer the List via an accessor method. Then, other code won’t try to stream over the UserDatabase instance but over the list returned by the accessor method. The returned list may be a read only wrapper which provides optimal performance as it is provided by the JRE itself and takes care to override the default methods where feasible.

Tripe answered 6/7, 2015 at 10:30 Comment(3)
That was what I was trying to say in my answer, but that awarded me a downvote. Hoping that your verbosity (and vastly better argumented answer!) will serve you better :-)Norwegian
Wondering if you swapped terms in this sentence: "... the new default methods break completely in the inheritance case and the possibility that they don’t work with the maximum performance in the delegation case." It's the other way around, if using the OPs case. He wanted to inherit ArrayList to gain performance, and when using delegation (implementing the interface) you would get the default method, getting worse performance, possible breaking completely.Norwegian
@oligofren: it’s not swapped, but maybe was not clearly enough. If you use inheritance, you may get specializations of the default methods which work on the internals, ignoring whatever your reason for subclassing was, thus may break. In the delegation scenario, you get the default methods robustly working on the interface alone.Tripe
N
2

I don't really understand the big issue here. You can still back your UserDatabase with an ArrayList even if not extending it, and get the performance by delegation. You do not need to extend it to get the performance.

public class UserDatabase implements List<User>{
   private ArrayList<User> list = new ArrayList<User>();

   // implementation ...

   // delegate
   public Spliterator() spliterator() { return list.spliterator(); }
}

Your two points are not changing this. If you know "ArrayList has its own, more efficient implementation of spliterator()", then you can delegate it to your backing instance, and if you do not know, then the default method takes care of it.

I am still unsure whether it really makes any sense to implement the List interface, unless you are explicitly making a reusable Collection library. Better create your own API for such one-offs that does not come with future problems through the inheritance (or interface) chain.

Norwegian answered 6/7, 2015 at 9:34 Comment(3)
This is actually mentioned in my question. The whole point is that since the implementor is not forced to implement the spliterator() method, it forces him to rely on his knowledge of the ArrayList implementation of spliterator() in order to use its performance, which is not usually desirable.Veil
The question was "is it still as true that one should prefer composition over inheritance in such situations ?". The answer to that was yes. Default methods do not change this. It does not matter if you are using inheritance or composition, you still need to know about the performance hit to design around it.Norwegian
@Norwegian Elegant answer. I posted a comment on the question just to realize someone has already posted an answer that has the exact same reasoning that I have. It's sad to see that even after you hit the nail on the head with such an elegant answer, you still got a downvote. +1 from me to nullify the downvote.Raspberry
F
0

I cannot provide an advice for every situation, but for this particular case I'd suggest not to implement the List at all. What would be the purpose of UserDatabase.set(int, User)? Do you really want to replace the i-th entry in the backing database with the completely new user? What about add(int, User)? It seems for me that you should either implement it as read-only list (throwing UnsupportedOperationException on every modification request) or support only some modification methods (like add(User) is supported, but add(int, User) is not). But the latter case would be confusing for the users. It's better to provide your own modification API which is more suitable for your task. As for read requests, probably it would be better to return a stream of users:

I'd suggest to create a method which returns the Stream:

public class UserDatabase {
    List<User> list = new ArrayList<>();

    public Stream<User> users() {
        return list.stream();
    }
}

Note that in this case you are completely free to change the implementation in future. For example, replace ArrayList with TreeSet or ConcurrentLinkedDeque or whatever.

Ferous answered 6/7, 2015 at 10:24 Comment(1)
@Hui The answer did not really have anything to do with the database per se, but about design issues that comes with having an IS-A relationship.Norwegian
L
0

The selection is simple based on your requirement.

Note - The below is just a use case . to illustrate the difference.

If you want a list that is not going to keep duplicates and going to do a whole bunch of things very much different from ArrayList then there is no use of extending ArrayList because you are writing everything from scratch.

In the above you should Implement List. But if you are just optimizing an implementation of ArrayList then you should copy paste the whole implementation of ArrayList and follow optimization instead of extending ArrayList. Why because multiple level of implementation makes it difficult for someone tries to sort out things.

Eg: A computer with 4GB Ram as parent and Child is having 8 GB ram. It is bad if parent has 4 GB and new Child has 4 GB to make an 8 GB. Instead of a child with 8 GB RAM implementation.

I would suggest composition in this case. But it will change based on the scenario.

Limiter answered 6/7, 2015 at 10:49 Comment(0)
P
0

It is usually admitted that extending implementations of an interface through inheritance is not best practice, and that composition (e.g. implementing the interface again from scratch) is more maintainable.

I don't think that this is accurate at all. For sure there are lots of situations where composition is preferred over inheritance, but there are lots of situations where inheritance is preferred over composition!

Its especially important to realise that the inheritance structure of your implementation classes need not look anything like the inheritance structure of your API.

Does anyone really believe, for example, that when writing a graphical library like Java swing every implementation class should reimplement the paintComponent() method? In fact a whole principal of the design is that when writing paint methods for new classes you can call super.paint() and that insures that all elements in the hierarchy are drawn, as well as handling the complications involving interfacing with the native interface further up the tree.

What is generally accepted is that extending classes not within your control that were not designed to support inheritance is dangerous and potentially a source of irritating bugs when the implementation changes. (So mark your classes as final if you reserve the right to change your implementation!). I doubt Oracle would introduce breaking changes into ArrayList implementation though! Provided you respect its documentation you should be fine....

Thats the elegance of the design. If they decide that there is a problem with the ArrayList, they will write a new implementation class, similar to when they replaced Vector back in the day, and there will be no need to introduce breaking changes.

===============

In your current example, the operative question is: why does this class exist at all?

If you are writing a class which extends the interface of list, which other methods does it implement? If it implements no new methods, what is wrong with using ArrayList?

When you know the answer that you will know what to do. If the answer "I want an object which is basically a list, but has some extra convenience methods to operate on that list", then I should use composition.

If the answer is "I want to fundamentally change the functionality of a list" then you should use inheritance, or implement from scratch. An example might be implementing an unmodifiable list by overriding ArrayList's add method to throw an exception. If you are uncomfortable with this approach you might consider implementing from scratch by extending AbstractList, which exists precisely to be inherited from to minimise the effort of reimplementation.

Pearlypearman answered 6/7, 2015 at 11:16 Comment(2)
Swing is a very bad example. It uses lots of delegation, that’s why subclasses often work without overriding methods. There are data models, selection models, the UI delegation, Border and Icon abstraction, child components, etc. In fact, a lot of these sub classes only exist to make it easier for developers who are used to other frameworks like the plain AWT, etc. E.g. the JButton class adds almost nothing to its base class, however, developers were very surprised if there wasn’t a button class…Tripe
Thats fair. If you can come up with a better example I am happy to change the example. I will rewrite the example.Pearlypearman

© 2022 - 2024 — McMap. All rights reserved.