Despite using composition over inheritance?
If so, is there any solution for it at the language level?
Such thing does not happen with Go, because Go has functions. Not everything needs to be an object, or a method, in the real world. “Classes” and “objects” are very useful but they cannot be used for everything.
Since Golang does not support classes, so inheritance takes place through struct embedding. We cannot directly extend structs but rather use a concept called composition where the struct is used to form other objects. So, you can say there is No Inheritance Concept in Golang.
The fragile base class problem is a fundamental architectural problem of object-oriented programming systems where base classes (superclasses) are considered "fragile" because seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With