Dart: Inheritance
In object-oriented programming, inheritance is a core concept and mechanism that allows parent classes to share behaviour with their children. This increases reusability by defining an “is-a” relationship between two classes (as opposed to a “has-a” relationship used in composition). In the natural world, inheritance is seen in evolution: genetic traits are passed down through generations of ancestors.
🚧 Warning
Despite Dart having a multiple-inheritance-like feature via mixins, it is still a single-inheritance language. Each class can only directly inherit from a single parent class.
“Extends” keyword
A class explicitly inherits from another by using the extends
keyword:
class TimeMachine extends DeLorean {
// Steering & accelerator servos for RC driving
// Time circuits
// Flux capacitor
// Mr Fusion
}
You may have seen that your class contains various base method definitions, such as noSuchMethod()
, toString()
and the ==
operator. This is because if you don’t use extends
, your class will implicitly inherit Dart’s base Object
class. Every Dart class has Object
at the root of it’s class hierarchy.
Super
If the parent class’ constructor has parameters, Dart requires you to pass these to the parent using the super()
method.
class DeLorean extends Car {
final bool hasLuggageRack;
final bool hasSideStripes;
DeLorean(this.hasLuggageRack, this.hasSideStripes);
}
class TimeMachine extends DeLorean {
TimeMachine(double receiverFrequency) : super(false, false);
}
You can also call parent methods and fields using super()
.
Overriding behaviour
The subclass can provide completely different state and behaviour to it’s parent. By defining a method with the same name as one found on the parent, this method override will be called in place of the original method:
class TimeMachine extends DeLorean {
void accelerate() {
if (super.speed >= 88) {
activateFluxCapacitor();
}
super.velocity += 10;
}
}
You may have noticed the @override
annotation in the example above. This explicitly indicates that you intended to override something from a parent class.
Mixins
Dart encourages code reuse through mixins. This reduces the need for deeply nested inheritance trees by allowing you to create modules of code which you can bolt on to other classes where it doesn't make sense to use regular inheritance. Think of a mixin as a toolbox of other functions and members that is copied to another class.
On the face of it, this might look like this is an implementation of multiple-inheritance, however the way Dart achieves this is different.
Declaring a mixin
You can create a mixin in one of two ways: you can either use the mixin
keyword,
or the class
keyword if you wish to reuse it as a regular class elsewhere.
Using mixin
restricts you to only using the class as a mixin.
mixin Resonance {
/// The rate at which something is resonating.
double rate;
}
🚧 Warning
Themixin
keyword was introduced in Dart 2.1. Earlier versions usedabstract class
to achieve the same goal, so you may see both variations when looking at sample code.
Adding a mixin to a class
The with
keyword allows you to add the mixin to another class:
class XenCrystalSample with Resonance {
// ...
}
Ordering
The order in which you add mixins to a class is important.
Dart implements multiple-inheritance-like behaviour by using a process called linearisation. Behind the scenes, this process creates a new superclass (a fancy way of saying a parent class) with the mixin applied on top.
mixin Radioactive {
// The level of radiation exposure
double milliSieverts;
}
mixin Resonance {
/// The rate at which the [XenCrystalSample] is resonating.
double rate;
}
class XenCrystalSample with Radioactive, Resonance {
// ...
}
The whole process is linear: it moves from the outermost (Resonance) to the innermost (Radioactive) mixin creating superclasses for each one.
Restricting mixin usage
If you wish to restrict the mixin so it can only be used with a specific type
you can use the on
keyword. This is handy for occasions where your mixin has
to call methods on the class it's being used with.
mixin Resonance on XenCrystalSample {
/// The rate at which the [XenCrystalSample] is resonating.
double rate;
}
class XenCrystalSample with Radioactive, Resonance {
// ...
}
Like most things in programming, you are provided the tools to create what you need. Classes are just another building block in your toolbox, allowing you to model anything from simple to very complex structures.
A word of caution
Generally, class inheritance is often seen as overused, dangerous and harmful in object-oriented programming. You should always prefer composition over inheritance since inheritance introduces a level of complexity and creates coupling between two (or more) classes. This coupling can affect intended behaviour when changes are made and it makes unit testing your code harder.
In addition to providing more robust code, composition can be leveraged to swap collaborating classes at runtime. Just make sure you are programming to abstractions, not concretions. This is a fancy way of saying: use the highest level abstraction as possible in order to make your class designs flexible. For example, the Iterable<>
collection class is a higher-level of abstraction than List<>
. Iterable
is more generic, and List
is more specific. By designing your classes to rely on specifics you are making them inflexible.
While I won't discuss composition over inheritance further than this (not here anyway), you definitely should investigate and understand this principle in order to avoid making a mess.