Blog-Archiv

Sonntag, 20. Dezember 2020

Why Delegation is Not Better than Inheritance

The object-oriented idea often is associated with inheritance and obscure hierarchies. Lots of books and web articles recommend to prefer delegation to inheritance. Do they implicitly recommend to not use object-oriented languages?

In this Blog I want to bring more light into the discussion about "inheritance versus delegation", which I consider unbalanced. Mind that, from a technical point of view, both delegation and inheritance are about code reusage. Nevertheless, from the business perspective, inheritance may represent a determining and deliberate concept that is nice to have also in source code.

(Table of contents generated by JavaScript.)

Code Examples

We are going to implement following application by both inheritance and delegation:

public class Main
{
    public static void main(String[] args)  {
        Dog lupo = new Dog();
        lupo.eat("sausage");
        lupo.bark();

        Cat garfield = new Cat();
        garfield.eat("milk");
        garfield.miaow();
    }
}

Both the dog lupo and the cat garfield can eat, but only the dog can bark, and only the cat can miaow. We want to put the reusable eat() logic into a common class, first using inheritance, then delegation.

Inheritance

Inheritance represents the "IS A" concept:

  • A cat is an animal

This is categorization, one of the oldest scientific ways of working, closely related to classification, generalization, abstraction.

class Animal
{
    private String eatenFood;
    
    public void eat(String food)   {
        this.eatenFood = food;
    }

    public String getEatenFood() {
        return eatenFood;
    }
}

class Dog extends Animal
{
    public void bark() {
        System.out.println("Wow Wow");
    }
}

class Cat extends Animal
{
    public void miaow()  {
        System.out.println("Miaaaaaow");
    }
}

Both Dog and Cat inherit from Animal, and thus can eat(). A Dog has the special ability to bark(), a Cat the ability to miaow(). This code is short and concise and contains no duplication of any kind.

Delegation

Delegation can represent both the "HAS A" and the "IS A" concept:

  • A cat has an animal (inside, thus it is an animal)

Doesn't sound very intuitive, but is acceptable from a technical perspective, because inheritance is resolved to delegation by the compiler!

class Animal
{
    private String eatenFood;
    
    public void eat(String food)    {
        this.eatenFood = food;
    }
    
    public String getEatenFood()    {
        return eatenFood;
    }
}

class Dog
{
    private Animal animal = new Animal();
    
    public void eat(String food) {
        animal.eat(food);
    }
    
    public String getEatenFood()    {
        return animal.getEatenFood();
    }
    
    public void bark() {
        System.out.println("Wow Wow");
    }
}

class Cat
{
    private Animal animal = new Animal();
    
    public void eat(String food) {
        animal.eat(food);
    }
    
    public String getEatenFood()    {
        return animal.getEatenFood();
    }
    
    public void miaow()  {
        System.out.println("Miaaaaaow");
    }
}

Both Dog and Cat have an Animal within them, but calls to eat() and getEatenFood() have to be forwarded to that delegation object. This is purely technical code that duplicates Animal method signatures. The specific abilities like bark() and miaow() can hardly be seen, because all the delegate methods are public. (Yes, this clutters code!)

When to Use What

Use delegation when having a clear containment hierarchy like "A cat has a tail".

Deriving a LabeledButton from HorizontalLayout just to be able to use the layout methods without having to create a delegate is not worth it. You would expose the HorizontalLayout API to all callers of LabeledButton. Moreover the next release may demand the layout to be switchable to VerticalLayout at runtime.

Only inherit from a class that you want 100% inside your class. There should not be a single public or protected super-method that doesn't fit to your class.

A Stack with push(), pop(), peek() is not expected to inherit from List with add(), remove(), contains(), as such an API would look absurd for a Stack.
Why extend Thread when you just want some logic to be runnable in a separate thread? Most likely you won't even use the inherited Thread methods in your logic. Better implement Runnable and let the caller decide about a thread of its choice.
UI controllers (callback method containers) are a border case. Practice shows that here inheritance leads to very high and incomprehensible hierarchies that end up to be mixins actually.

When you need to reuse more than one class, use delegation, even if one of the super-classes would fit 100% to your class.

When you need to inherit dynamically at runtime, you need to use delegation, because inheritance is compile-time bound.

Use inheritance in any other case, especially when having a clear inheritance hierarchy like "A cat is a mammal". Inheritance is a much better reusage concept than delegation.

We still live in times of manually written source code, and very frequently the source code is the only representation of a business logic that came from experts that are long gone and left no documentation ("we are agile"). Thus we need well readable und quickly understandable source code without duplications, easily maintainable, because maintenance makes up 70% of software production efforts.

Once again I want to remark that inheritance is resolved to delegation at compile time, so why implement delegation when the compiler can do it for you, without typos and code duplication?

Advantages and Disadvantages

Mind that any such list always is driven by the point of view: A compiler engineer may give other pros and cons than a database expert. I am a business developer.

Inheritance

Advantages

  • You effortlessly get whatever the super-class provides, without any code duplication
  • You could fix bugs or customize behavior in a class you have no editing-access to (external frameworks), when inheriting from it and overriding methods
  • No repetitive delegation code clutters the implementation

Disadvantages

  • You can not inherit at runtime, binding to a super-class at runtime normally is not supported
  • Mostly you can not reuse several super-classes at the same time; some languages provide multiple inheritance (→ mixins), but you are going to be ambiguous, misleading and complex when you use that feature
  • Inheritance is a too intimate relationship, meaning the derived class can bring down the super-class by overrides or use of protected fields that are meant to be internal-only; that can be avoided only by accurate and well written access modifiers and field wrappers in the super-class, see chapter below

Delegation

Advantages

  • You can lazily load the delegate
  • You can bind the super-class delegate at runtime
  • You can simulate multiple inheritance

Disadvantages

  • Delegation is code duplication
  • Mostly the delegate needs an interface, so that all delegators can also implement it and forward to the delegate, which gives an additional interface and lots of purely technical method-forwarding in all delegators
  • When the delegate interface changes, you need to change both the delegate and all delegators
  • Without a delegate interface you can be sure that you will forget to change all delegators when the delegate changes!

How to Avoid Inheritance Break Encapsulation

Inheritance is said to be a too intimate relationship. E.g. you could modify non-final protected fields of a super-class, causing unexpected behavior. You could override a method and forget to call super, or do it in a way the super-class did not expect.

To avoid that, do the following in any super-class:

  1. Try to make all fields private, and as many methods as possible private, package-visible, or at least protected (minimize access modifiers)

  2. Make immutable protected fields final

  3. Make mutable protected fields private and a add a protected final getter for it (when necessary), try to avoid the setter

  4. When a private field has protected getter and setter, the field must not be accessed directly (in its class), only by its getter and setter, as they could be overwritten when being non-final

  5. Make all non-private methods called from constructor final, see here

  6. Add assertions to all non-private methods to uncover illegal object states

  7. Make all non-private methods final that are not explicitly meant to be overwritten, or that may endanger the object's state when overwritten

Should be self-evident, but belongs here: public fields always must be final (immutable)!

Conclusion

We need both of them, inheritance AND delegation!

Obviously delegation is the more flexible concept. But with more freedom comes less security, meaning the code is hard to maintain when having those big delegation sections in it.

Absence of abstraction means low quality of both specification and software. Code without abstractions will contain many duplications due to missing code reusage.

Respect business logic and its concepts. When the specification comes with an "IS A" relation, then use inheritance. When it comes with a "HAS A" relation, use delegation. When it comes with neither, tell them to introduce it :-)




Samstag, 19. Dezember 2020

Java Parallel versus Sequential Stream Performance

In my article about for-loop versus Java 8 stream loop I presented a test showing that a for-loop could be faster than a stream when doing typical sequential work like searching the maximum in a collection of numbers. This was due to the fact that parallel threads may be fast in processing their partition of the collection, but in the end they have to figure out who found the maximum, and this synchronization made the for-loop win the race.

What about another kind of work being done inside the loop? This Blog is about performing a long lasting task for each element of a list, and how this performs when doing it by a for loop, a stream forEach(), or a parallel stream forEach(). Mind that such work must be free of side-effects when done in parallel.

Test Source

The example builds a list of high positive numbers, then loops it and calls a method that counts from zero up to the given number from the list. That method is called longLastingTask(). The loop is performed using three different techniques:

  1. for (long limit : list)
        longLastingTask(limit)
  2. list.stream().forEach(limit -> longLastingTask(limit))
  3. list.parallelStream().forEach(limit -> longLastingTask(limit))

Here is the source code of the performance test, explained step by step. On bottom you find the entire class. There are no external libraries included, just the plain Java runtime environment.

public class LongTaskInLoopPerformance
{
    public static void main(String[] args) {
        System.out.println("Java: "+System.getProperty("java.version"));
        System.out.println("Cores: "+Runtime.getRuntime().availableProcessors());
        
        LongTaskInLoopPerformance streamPerformance = new LongTaskInLoopPerformance(30000);
        streamPerformance.performTests(10);
    }
    
    
    private final Collection<Long> longList;
    
    public LongTaskInLoopPerformance(final int numberOfCountLimits) {
        longList = createTestList(numberOfCountLimits);
    }
    
    private Collection<Long> createTestList(int testDataLength) {
        final List<Long> longList = new ArrayList<Long>(testDataLength);
        for (int i = 0; i < testDataLength; i++)    {
            long random = (long) Math.abs(Math.random() * 100000d);
            if (random > 0)
                longList.add(Long.valueOf(random));  // positive big numbers
        }
        return longList;
    }
    
    // more source goes here ...

}

This is the class skeleton with main method and constructor. The main method outputs the Java version and the number of cores in the machine where the test runs. Then it constructs a test with the length of the count-limit list set to 30000. Finally it runs 10 tests, so that warm-up and other influences are minimized.

In constructor, LongTaskInLoopPerformance builds the list of long numbers containing the limits for the long-lasting count tasks. Such a task will count from zero to the limit it receives as parameter. The list of limits stays unchanged during all test runs.

Next is the sum method. Put this to "// more source goes here ..." in class above:

    public void performTests(int numberOfTests) {
        long sumForLoop = 0L, sumParallelStream = 0L, sumSequentialStream = 0L;
        int numberOfForLoops = 0, numberOfParallelStreams = 0, numberOfSequentialStreams = 0; 
        
        final int numberOfTestTypes = 3;    // for-loop, sequential stream, parallel stream
        numberOfTests *= numberOfTestTypes;
        
        for (int testNumber = 0; testNumber < numberOfTests; testNumber++)   {
            final boolean doForLoop = (testNumber % numberOfTestTypes == 1);    // every 2nd test is for-loop
            final boolean useParallelStream = (testNumber % numberOfTestTypes == 2);    // every 3rd test is parallel
            
            long millis = performTest(doForLoop, useParallelStream);
            
            if (doForLoop)  {
                sumForLoop += millis;
                numberOfForLoops++;
            }
            else if (useParallelStream) {
                sumParallelStream += millis;
                numberOfParallelStreams++;
            }
            else    {
                sumSequentialStream += millis;
                numberOfSequentialStreams++;
            }
        }
        
        System.out.println(numberOfParallelStreams+" parallel stream forEach-loops needed "+sumParallelStream+" millis");
        System.out.println(numberOfForLoops+" for-loops needed "+sumForLoop+" millis");
        System.out.println(numberOfSequentialStreams+" sequential stream forEach-loops needed "+sumSequentialStream+" millis");
    }

This method sets up counters for every type of loop and their time sums. For every test to execute, all three types of loop are run. Every 1st test will be a sequential stream, every 2nd a for-loop, every 3rd a parallel stream. Finally it outputs the time sums for all three loop types.

The next method runs exactly one test:

    private long performTest(final boolean doForLoop, final boolean useParallelStream)    {
        final long before = System.currentTimeMillis();
        
        if (doForLoop)
            for (Long l : longList)
                longLastingTask(l);
        else if (useParallelStream)
            performStreamTest(longList.parallelStream());
        else
            performStreamTest(longList.stream());
        
        return (System.currentTimeMillis() - before);
    }

    private void performStreamTest(Stream<Long> streamOfLong) {
        streamOfLong.forEach(l -> longLastingTask(l));
    }

    private void longLastingTask(long limit)    {
        for (long i = 0; i < limit; i++)
            ;
    }

Here the test loop is executed in three different ways, determined by the parameters to performTest().

The performStreamTest() method contains the forEach(). Both .stream() and .parallelStream() methods produce the same data type Stream<Long>, although its processing differs.

The longLastingTask() method finally counts from zero to the limit it receives, which is a random number between 1 and 100000.


Click here to see the entire test-class for copy & paste.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import java.util.*;
import java.util.stream.Stream;

/**
 * Check whether parallel streams outperform sequential ones and for-loops
 * when performing a long-lasting task with each element of a list.
 */
public class LongTaskInLoopPerformance
{
    public static void main(String[] args) {
        System.out.println("Java: "+System.getProperty("java.version"));
        System.out.println("Cores: "+Runtime.getRuntime().availableProcessors());
        
        LongTaskInLoopPerformance streamPerformance = new LongTaskInLoopPerformance(30000);
        streamPerformance.performTests(10);
    }
    
    
    private final Collection<Long> longList;
    
    public LongTaskInLoopPerformance(final int numberOfCountLimits) {
        longList = createTestList(numberOfCountLimits);
    }
    
    private Collection<Long> createTestList(int testDataLength) {
        final List<Long> longList = new ArrayList<Long>(testDataLength);
        for (int i = 0; i < testDataLength; i++)    {
            long random = (long) Math.abs(Math.random() * 100000d);
            if (random > 0)
                longList.add(Long.valueOf(random));  // positive big numbers
        }
        return longList;
    }
    
    public void performTests(int numberOfTests) {
        long sumForLoop = 0L, sumParallelStream = 0L, sumSequentialStream = 0L;
        int numberOfForLoops = 0, numberOfParallelStreams = 0, numberOfSequentialStreams = 0; 
        
        final int numberOfTestTypes = 3;    // for-loop, sequential stream, parallel stream
        numberOfTests *= numberOfTestTypes;
        
        for (int testNumber = 0; testNumber < numberOfTests; testNumber++)   {
            final boolean doForLoop = (testNumber % numberOfTestTypes == 1);    // every 2nd test is for-loop
            final boolean useParallelStream = (testNumber % numberOfTestTypes == 2);    // every 3rd test is parallel
            
            long millis = performTest(doForLoop, useParallelStream);
            
            if (doForLoop)  {
                sumForLoop += millis;
                numberOfForLoops++;
            }
            else if (useParallelStream) {
                sumParallelStream += millis;
                numberOfParallelStreams++;
            }
            else    {
                sumSequentialStream += millis;
                numberOfSequentialStreams++;
            }
        }
        
        System.out.println(numberOfParallelStreams+" parallel stream forEach-loops needed "+sumParallelStream+" millis");
        System.out.println(numberOfForLoops+" for-loops needed "+sumForLoop+" millis");
        System.out.println(numberOfSequentialStreams+" sequential stream forEach-loops needed "+sumSequentialStream+" millis");
    }
    
    private long performTest(final boolean doForLoop, final boolean useParallelStream)    {
        final long before = System.currentTimeMillis();
        
        if (doForLoop)
            for (Long l : longList)
                longLastingTask(l);
        else if (useParallelStream)
            performStreamTest(longList.parallelStream());
        else
            performStreamTest(longList.stream());
        
        return (System.currentTimeMillis() - before);
    }

    private void performStreamTest(Stream<Long> streamOfLong) {
        streamOfLong.forEach(l -> longLastingTask(l));
    }

    private void longLastingTask(long limit)    {
        for (long i = 0; i < limit; i++)
            ;
    }

}

Results

Here is the output for Java 8:

Java: 1.8.0_121
Cores: 4
10 parallel stream forEach-loops needed 3510 millis
10 for-loops needed 6379 millis
10 sequential stream forEach-loops needed 6724 millis

And here for Java 11:

Java: 11.0.2
Cores: 4
10 parallel stream forEach-loops needed 3774 millis
10 for-loops needed 6312 millis
10 sequential stream forEach-loops needed 6393 millis

Conclusion

For long lasting tasks without side effects, done for each element of a list, turning the list into a parallel stream absolutely makes sense. The parallel stream finished in half the time of the others, the for-loop being a little faster than the sequential stream.