ref –
- https://medium.com/@manoelsrs/dart-extends-vs-implements-vs-with-b070f9637b36
- https://en.wikipedia.org/wiki/Dependency_inversion_principle
- https://stackoverflow.com/questions/3108182/using-parameter-that-implements-multiple-interfaces-pre-generics
- https://flutterbyexample.com/lesson/abstract-classes-and-interfaces
Dart has no interface keyword. Instead, all classes implicitly define an interface. Therefore, you can implement any class.
You can create an abstract class to be extended by children abstract classes, (or implemented) by a concrete class. Abstract classes can contain abstract methods (with empty bodies).
1 2 3 4 5 6 7 8 9 |
abstract class Describable { void describe(); void describeWithEmphasis() { print('========='); describe(); print('========='); } } |
Let’s first take a look at base classes and inheritance. We have a base class Vehicle with a constructor and some properties.
1 2 3 4 5 6 7 |
class Vehicle { // constructor Vehicle(this.color); // use property color for constructor injection final String color; final String definition = 'Vehicle'; } |
We then create a Car class and extends from this Vehicle.
1 2 3 |
class Car extends Vehicle { Car(String color) : super(color); // Since its parent class needs color, we must pass it in via constructor } |
Furthermore, we create a Hatch, and then extends from Car.
1 2 3 |
class Hatch extends Car { Hatch(String color) : super(color); // same as Car } |
Finally, Hatch, being on the grand child inherits properties from both parent and grandparent.
1 2 3 4 5 |
main() { final hatch = Hatch('red'); print('Result: ${hatch is Vehicle}'); // Output -> Result: true } |
Inheritance allows us to create new classes that reuse, extend and/or modify pre-existing classes behavior. The pre-existing class is called superclass and the new class we’re creating is called derived class.
In Dart, we can inherit only one superclass, but inheritance is transitive.
If the class Hatch is derived from the class Car and this class is derived from class Vehicle, then Hatch will be derived from Vehicle.
Use extends to create derived class, and super when you want to refer to the superclass. When class Car extends Vehicle, all properties, variables, functions implemented in class Vehicle will be available in class Car.
Suppose you want to create your own Car class, without inhering all the properties, variables and functions of the Vehicle class, but you want to inherit only the Vehicle type.
To do this, the Car class must implement the Vehicle interface by using abstract classes.
Abstract classes cannot be Instantiated!
Follows the concept of Dependency Inversion Principle
Given an abstract class Shape, we cannot instantiate it. We can only extend from it.
1 2 3 4 |
abstract class Shape { double getArea(); String name = ''; } |
We create the shapes that must implement this abstraction:
Circle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import 'shape.dart'; class Circle implements Shape { double side = 0.0; String name = 'Circle'; Circle({required double side}) { this.side = side; } // required by abstract class getArea() { return this.side * 3.14158; } } |
Square
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import 'shape.dart'; class Square implements Shape { double side = 0.0; String name; // declared as instructed by abstract Shape String getName() => name; void setName(newName) { name = newName; } // default our name as Square Square({required double side, this.name = 'Square'}) { this.side = side; this.name = name; } getArea() { return this.side * this.side; } } |
main.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import 'Square.dart'; import 'shape.dart'; import 'Circle.dart'; void printArea(Shape shape) { print('Area of Shape is: ${shape.getArea()}'); } void main() { final s = Square(side: 8.8); printArea(s); final c = Circle(side: 3); printArea(c); } |
We can implement abstract classes from more than one interface
…however, we can only extend from one parent class
Let’s take a look at the basics
We have abstract class A with a()
1 2 3 |
abstract class A { void a(); } |
and abstract class B with b()
1 2 3 |
abstract class B { void b(); } |
When we have a class AB that implements both A and B, it means we must implement both a() and b()
1 2 3 4 |
class AB implements A, B { void a() {} // must implement a() void b() {} // must implement b() } |
If we do not, it will complain. Because essentially we conform to the interfaces. It’s a pact with our abstraction that we will be implementing them.
Even if the abstract interface method is implemented, we must implement it.
Concrete classes can be instantiated. We extend from concrete classes.
Abstract classes CANNOT be instantiated. We implement abstract classes.
Passing instances that implements an abstract class
When we’re dealing with abstractions, we can specify that the object that is being passed in must implement a certain abstract class.
In our example, we have abstract class Person. In our parameter for function gate, we say that the parameter named person is an object that must implement Person.
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 |
abstract class Person { void introduce() {} } class Kid implements Person { void introduce() { print('Hello there! Im a Kid!'); } } void door(Person person) { person.introduce(); } void gate({person: Person}) { person.introduce(); } void main() { print("Hello, World!"); Person p = Kid(); door(p); gate(person: p); } |
So we create an instance of class Kid, which implements Person. We then pass kid into the parameter and it will be valid to be used. This encourages dependency inversion principle where modules are loosely coupled.
But what if we want it to conform to multiple interfaces?
ref – https://stackoverflow.com/questions/3108182/using-parameter-that-implements-multiple-interfaces-pre-generics
There are basically three ways to go about doing this:
1) Use function parameters to check against each abstraction, and pass in the same object.
So we have our interfaces
1 2 3 4 5 6 7 |
abstract class Connector { void connects(); } abstract class Thudder { void thuds(); } |
We have a Person class that implements both abstract classes
1 2 3 4 5 6 7 8 |
class Person implements Connector, Thudder { void connects() { print('Person wants to connect'); } void thuds() { print('Person thuds'); } } |
We have a function that takes in both interfaces. Just with different names. Each param checks on one abstraction, and since we want to check on two, then we just use two params for it.
It does this during compile time.
Then if both passes, then just use your object to access whatever abstract method you need.
1 2 3 4 |
void connectIt({connector: Connector, thudder: Thudder}) { connector.connects(); thudder.thuds(); } |
In main, we just pass in the same object. The params of connectIt will check each abstraction for us.
1 2 3 4 5 |
void main() { print("Hello, World!"); Person p = Person(); connectIt(connector: p, thudder: p); } |
2) Create another abstract class that implements both
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 |
abstract class Connector { void connects(); } abstract class Thudder { void thuds(); } abstract class IConnectAndThud implements Connector, Thudder { } void connectIt({iCAT: IConnectAndThud}) { iCAT.connects(); iCAT.thuds(); } class Person implements IConnectAndThud { void connects() { print('Person wants to connect'); } void thuds() { print('Person thuds'); } } |
3) Simply use type Person instead of an abstraction in connectIt’s parameter definition.
1 2 3 4 5 6 7 8 9 10 11 |
void anotherWay({person: Person}) { person.connects(); person.thuds(); } void main() { print("Hello, World!"); Person p = Person(); anotherWay(person: p); } |
Intricacies
As an abstract child class, if we were to simply use the parent abstract class’s property, it will display the value there. So the parent abstract class will need to initialize it from property declaration, or constructor initialization.
As an abstract child class, if we over-ride property from parent, and try to use it, we’ll get an error. This is because when we use over-ride, we’re declaring the property on the child class, instead of using the parent class’s property.
As an abstract child class, if we did over-ride property from parent, we need to make sure we initialize it either from declaration:
or constructor: