Does fragile base class issue exist in Go?

2019-02-18 09:30发布

问题:

Despite using composition over inheritance?

If so, is there any solution for it at the language level?

回答1:

As VonC wrote, but I'd like to point out something.

The fragile base class problem is often blamed on virtual methods (dynamic dispatch of methods – this means if methods can be overridden, the actual implementation that has to be called in case of such an overridden method can only be decided at runtime).

Why is this a problem? You have a class, you add some methods to it, and if MethodA() calls MethodB(), you can't have any guarantee that the MethodB() you wrote will be called and not some other method of a subclass that overrides your MethodB().

In Go there is embedding, but there is no polymorphism. If you embed a type in a struct, all the methods of the embedded type get promoted and will be in the method set of the wrapper struct type. But you can't "override" the promoted methods. Sure, you can add your own method with the same name, and calling a method by that name on the wrapper struct will invoke your method, but if this method is called from the embedded type, that will not be dispatched to your method, it will still call the "original" method that was defined to the embedded type.

So because of this, I'd say the fragile base class problem is only present in a quite mitigated form in Go.

Example

Demonstrating the problem in Java

Let's see an example. First in Java, because Java "suffers" from this kind of problem. Let's create a simple Counter class and a MyCounter subclass:

class Counter {
    int value;

    void inc() {
        value++;
    }

    void incBy(int n) {
        value += n;
    }
}

class MyCounter extends Counter {
    void inc() {
        incBy(1);
    }
}

Instantiating and using MyCounter:

MyCounter m = new MyCounter();
m.inc();
System.out.println(m.value);
m.incBy(2);
System.out.println(m.value);

The output is as expected:

1
3

So far so good. Now if the base class, Counter.incBy() would be changed to this:

void incBy(int n) {
    for (; n > 0; n--) {
        inc();
    }
}

The base class Counter still remains flawless and operational. But the MyCounter becomes malfunctioning: MyCounter.inc() calls Counter.incBy(), which calls inc() but due to dynamic dispatch, it will call MyCounter.inc()... yes... endless loop. Stack overflow error.

Demonstrating the lack of the problem in Go

Now let's see the same example, this time written in Go:

type Counter struct {
    value int
}

func (c *Counter) Inc() {
    c.value++
}

func (c *Counter) IncBy(n int) {
    c.value += n
}

type MyCounter struct {
    Counter
}

func (m *MyCounter) Inc() {
    m.IncBy(1)
}

Testing it:

m := &MyCounter{}
m.Inc()
fmt.Println(m.value)
m.IncBy(2)
fmt.Println(m.value)

Output is as expected (try it on the Go Playground):

1
3

Now let's change Counter.Inc() the same way we did in the Java example:

func (c *Counter) IncBy(n int) {
    for ; n > 0; n-- {
        c.Inc()
    }
}

It runs perfectly, the output is the same. Try it on the Go Playground.

What happens here is that MyCounter.Inc() will call Counter.IncBy() which will call Inc(), but this Inc() will be Counter.Inc(), so no endless loop here. Counter doesn't even know about MyCounter, it does not have any reference to the embedder MyCounter value.



回答2:

The Fragile base class problem is when a seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction.

As mentioned in this tutorial:

For all intents and purposes, composition by embedding an anonymous type is equivalent to implementation inheritance. An embedded struct is just as fragile as a base class.