Introduction to Method Overriding in Dart Programming Language
Method Overriding in Dart Programming Language is a concept in OOP that allows a subclass to provide an implementation that is already provided by its superclass.
Method Overriding in Dart Programming Language is a concept in OOP that allows a subclass to provide an implementation that is already provided by its superclass.
Method overriding occurs when a subclass implements a method with the same name, return type, and parameters as a method in its superclass. This allows the subclass to provide a new behavior or modify the existing behavior defined in the superclass.
In Dart, method overriding is straightforward. The key elements are:
@override
annotation.class Animal {
void makeSound() {
print('Some generic animal sound');
}
}
class Dog extends Animal {
@override
void makeSound() {
print('Bark');
}
}
void main() {
Animal myDog = Dog();
myDog.makeSound(); // Output: Bark
}
In this example:
Animal
class has a method makeSound
that prints a generic sound.Dog
class extends Animal
and overrides makeSound
to print ‘Bark’.makeSound
is called on an instance of Dog
, it uses the overridden implementation.@override
AnnotationThe @override
annotation is used to mark a method in the subclass as an override of a method from the superclass. It is not mandatory but highly recommended. This annotation:
To override a method, the method signature in the subclass must match exactly with the one in the superclass:
Dart does not support method overloading (methods with the same name but different parameters), so ensuring a precise match is crucial.
example where we have a base class Animal
and a subclass Dog
that overrides a method from the base class.
// Base class
class Animal {
void makeSound() {
print('Some generic animal sound');
}
}
// Subclass
class Dog extends Animal {
@override
void makeSound() {
print('Bark');
}
}
void main() {
Animal myAnimal = Animal();
myAnimal.makeSound(); // Output: Some generic animal sound
Dog myDog = Dog();
myDog.makeSound(); // Output: Bark
// Using polymorphism
Animal myPolyAnimal = Dog();
myPolyAnimal.makeSound(); // Output: Bark
}
Explanation:
Animal
class has a method makeSound
that prints a generic sound.Dog
class extends Animal
and overrides the makeSound
method to print ‘Bark’.main
function, when makeSound
is called on an Animal
instance, it prints ‘Some generic animal sound’.makeSound
is called on a Dog
instance, it prints ‘Bark’.myPolyAnimal
of type Animal
is actually an instance of Dog
, so it calls the overridden makeSound
method in Dog
.Here’s an example where the method in the superclass and subclass have parameters:
// Base class
class Printer {
void printMessage(String message) {
print('Printer: $message');
}
}
// Subclass
class AdvancedPrinter extends Printer {
@override
void printMessage(String message) {
print('AdvancedPrinter: [INFO] $message');
}
}
void main() {
Printer basicPrinter = Printer();
basicPrinter.printMessage('Hello World'); // Output: Printer: Hello World
AdvancedPrinter advancedPrinter = AdvancedPrinter();
advancedPrinter.printMessage('Hello World'); // Output: AdvancedPrinter: [INFO] Hello World
}
Explanation:
Printer
class has a method printMessage
that prints a simple message.AdvancedPrinter
class extends Printer
and overrides printMessage
to include additional information in the message.printMessage
on basicPrinter
prints the message as it is.printMessage
on advancedPrinter
prints the message with additional ‘[INFO]’ prefix.You can also call the superclass method within the overridden method using the super
keyword:
// Base class
class Shape {
void draw() {
print('Drawing a shape');
}
}
// Subclass
class Circle extends Shape {
@override
void draw() {
super.draw(); // Call the superclass method
print('Drawing a circle');
}
}
void main() {
Circle myCircle = Circle();
myCircle.draw();
// Output:
// Drawing a shape
// Drawing a circle
}
Explanation:
Shape
class has a method draw
that prints ‘Drawing a shape’.Circle
class extends Shape
and overrides draw
to include additional functionality. It calls super.draw()
to execute the method from the Shape
class before adding its own message.Some of the key advantages of method overriding in Dart make it quite flexible and maintainable. Let us look into them in some detail:
Method overriding allows subclasses to provide a specific implementation for a method that might already be provided by its superclass. That permits subclasses to adapt or extend behavior as needed without requiring changes in the base class itself. Suppose, for instance, you have a base class Vehicle which contains the abstract method startEngine. Now sub-classes of Vehicle, such as Car and Motorcycle can override methods to start their own engine depending on the type of engine start-up required.
class Vehicle {
void startEngine() {
print('Starting engine...');
}
}
class Car extends Vehicle {
@override
void startEngine() {
print('Starting car engine with ignition key.');
}
}
class Motorcycle extends Vehicle {
@override
void startEngine() {
print('Starting motorcycle engine with button.');
}
}
By using method overriding, developers can create a base class with common functionality and then extend it in subclasses. This promotes code reusability as the common behavior is written once in the superclass and extended or modified as needed in the subclasses. It reduces code duplication and makes the codebase easier to manage.
class Animal {
void eat() {
print('Eating food...');
}
}
class Dog extends Animal {
@override
void eat() {
print('Dog is eating dog food.');
}
}
class Cat extends Animal {
@override
void eat() {
print('Cat is eating cat food.');
}
}
Method overriding makes it possible to interact with objects based on the type of their superclass rather than being an instance of a particular class. This enables the creation of a single method call on which different implementations can act based on the object’s actual runtime type. This simplifies code and allows for greater flexibility due to the fact that the same method name can carry out different tasks depending on the subclass.
void makeAnimalSound(Animal animal) {
animal.makeSound();
}
void main() {
Animal myDog = Dog();
Animal myCat = Cat();
makeAnimalSound(myDog); // Output: Bark
makeAnimalSound(myCat); // Output: Meow
}
Extensibility in systems is indirectly supported by method overriding, as that enables addition of new functionalities without necessarily rewriting the existing code. As usual, this is very helpful when working with big systems that may require adding some new features or subclasses. An already available codebase can easily be extended through the generation of new subclasses which may override methods to introduce new behaviors.
class Employee {
void work() {
print('Employee is working.');
}
}
class Manager extends Employee {
@override
void work() {
print('Manager is managing the team.');
}
}
class Developer extends Employee {
@override
void work() {
print('Developer is writing code.');
}
}
Method overriding plays an important role in various design patterns like Template Method Pattern, Strategy Pattern, and Command Pattern. In each of these design patterns, the concept remains the same, which is defining a common interface or abstract class and allowing concrete subclasses to override methods to provide specific implementations. This leads to cleaner, maintainable, and organized code.
abstract class CoffeeTemplate {
void makeCoffee() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addCondiments();
}
void boilWater();
void brewCoffeeGrinds();
void pourInCup();
void addCondiments();
}
class CoffeeWithHook extends CoffeeTemplate {
@override
void boilWater() {
print('Boiling water.');
}
@override
void brewCoffeeGrinds() {
print('Dripping Coffee through filter.');
}
@override
void pourInCup() {
print('Pouring coffee into cup.');
}
@override
void addCondiments() {
print('Adding sugar and milk.');
}
}
Method overriding aids in maintainability because it puts changes in a local scope, without having to make any kind of modifications to the code. The moment some kind of modification is required, you could override methods in some subclasses without even touching either the superclass or any other part of your application. This way, individual components of an application become somewhat easier to handle and update.
class BaseClass {
void process() {
// Base processing logic
}
}
class SubClassA extends BaseClass {
@override
void process() {
// Custom processing logic for SubClassA
}
}
class SubClassB extends BaseClass {
@override
void process() {
// Custom processing logic for SubClassB
}
}
While method overriding in Dart has a lot of advantages, there are a few potential disadvantages and challenges that come along. Understanding these may assist you in giving a more informed judgment on when and how to use method overriding within your Dart applications. Here are some major drawbacks:
Method overriding can take the quality of a code base to another dimension when considering deep inheritance hierarchies. Sometimes, tracing the mechanisms associated with overridden approaches in order to find out their interaction on many tiers of the hierarchy may be hard. This level of complexity will make the code more difficult to understand and maintain, especially for new developers who join the project.
class A {
void method() {
print('Method in A');
}
}
class B extends A {
@override
void method() {
print('Method in B');
}
}
class C extends B {
@override
void method() {
print('Method in C');
}
}
// The method call might be confusing if it's not well documented:
void main() {
C c = C();
c.method(); // Output: Method in C
}
Overriding methods can also introduce bugs in those cases where the overridden method is supposed to follow some sort of contract as defined by its superclass. For instance, if a method in a superclass is supposed to maintain certain invariants or side effects, then a subclass might accidentally violate those expectations, thus producing subtle bugs that could be hard to diagnose.
class Base {
void calculate() {
print('Calculating in Base');
}
}
class Derived extends Base {
@override
void calculate() {
// Missing some essential steps
print('Calculating in Derived');
}
}
// This can cause unexpected results:
void main() {
Derived d = Derived();
d.calculate(); // Output: Calculating in Derived
}
Debugging method overriding issues can be hard as it involves the need for an understanding of the different implementations of the overridden methods invoked during runtime. At times, the real source of an error cannot be conveniently traced back through the class hierarchy from the location of an overridden method.
class Parent {
void show() {
print('Parent');
}
}
class Child extends Parent {
@override
void show() {
print('Child');
}
}
// Debugging can be complicated if issues arise with overridden methods
void main() {
Parent p = Child();
p.show(); // Output: Child
}
Method overriding can sometimes bring in design issues if inheritance is applied poorly. That could well be when a class is inheriting from more than one class- maybe using mixin or interface inheritance, and method overriding may lead to ambiguous or conflicting behavior unless carefully handled.
mixin MixinA {
void method() {
print('MixinA');
}
}
mixin MixinB {
void method() {
print('MixinB');
}
}
class Example with MixinA, MixinB {
@override
void method() {
// Which mixin method will be used?
print('Example');
}
}
void main() {
Example e = Example();
e.method(); // Output: Example
}
Method overriding introduces a layer of indirection, as the method call is dynamically dispatched at runtime. In some performance-critical applications, this overhead may be significant compared to static method calls. While the impact is usually minimal in most cases, it can become a concern in high-performance or real-time systems.
class Base {
void performAction() {
// Base action
}
}
class Derived extends Base {
@override
void performAction() {
// Overridden action
}
}
void main() {
Base b = Derived();
b.performAction(); // Potential performance impact
}
When a method is overridden, the original behavior defined in its superclass might get lost unless properly preserved or referenced using super. This could result in some very key functionality from the superclass being inadvertently omitted or changed in the subclass.
class Base {
void perform() {
print('Base perform');
}
}
class Derived extends Base {
@override
void perform() {
// Base behavior is not included
print('Derived perform');
}
}
void main() {
Derived d = Derived();
d.perform(); // Output: Derived perform
}
Conclusion:
While method overriding is a powerful feature in Dart that provides flexibility and supports polymorphism, it also introduces certain challenges and potential disadvantages. These include increased complexity, potential for bugs, difficulty in debugging, inheritance issues, performance overhead, and loss of original behavior. By being aware of these disadvantages, you can take appropriate measures to mitigate them and ensure that method overriding is used effectively in your Dart applications.
Subscribe to get the latest posts sent to your email.