Method Overriding in Dart Programming Language

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.

t.dev/language" target="_blank" rel="noreferrer noopener">Dart does support method overriding to extend or customize the behavior of the inherited methods from the parent class for better code reusability and flexibility. This article is going to discuss some best practices, use cases, syntax, and basics of method overriding in Dart.

Understanding Method Overriding in Dart Language

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.

  • Customization: Subclasses can alter or extend the behavior of methods inherited from their parent classes to fit specific needs.
  • Polymorphism: Method overriding enables polymorphism, allowing the same method name to invoke different implementations based on the object’s runtime type.
  • Code Reusability: By overriding methods, developers can build upon existing functionality without changing the original code, promoting better code management.

How Method Overriding Works in Dart

Basic Syntax

In Dart, method overriding is straightforward. The key elements are:

  1. Superclass Definition: Define a method in the superclass.
  2. Subclass Implementation: Override the method in the subclass using the @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’.
  • When makeSound is called on an instance of Dog, it uses the overridden implementation.

The @override Annotation

The @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:

  • Provides Clarity: It makes it clear that the method is meant to override a superclass method.
  • Enforces Correctness: The Dart analyzer checks if the method is indeed overriding an existing method, preventing errors if the method signature does not match.

Method Signature

To override a method, the method signature in the subclass must match exactly with the one in the superclass:

  • Method Name: The name must be identical.
  • Return Type: The return type must match.
  • Parameters: The number and type of parameters must be the same.

Dart does not support method overloading (methods with the same name but different parameters), so ensuring a precise match is crucial.

Example of Method Overriding in Dart Programming Language

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:

  • The Animal class has a method makeSound that prints a generic sound.
  • The Dog class extends Animal and overrides the makeSound method to print ‘Bark’.
  • In the main function, when makeSound is called on an Animal instance, it prints ‘Some generic animal sound’.
  • When makeSound is called on a Dog instance, it prints ‘Bark’.
  • Using polymorphism, myPolyAnimal of type Animal is actually an instance of Dog, so it calls the overridden makeSound method in Dog.

Example with Parameters

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:

  • The Printer class has a method printMessage that prints a simple message.
  • The AdvancedPrinter class extends Printer and overrides printMessage to include additional information in the message.
  • Calling printMessage on basicPrinter prints the message as it is.
  • Calling printMessage on advancedPrinter prints the message with additional ‘[INFO]’ prefix.

Example with Method Overriding and Super Calls

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:

  • The Shape class has a method draw that prints ‘Drawing a shape’.
  • The 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.
  • The output includes both the superclass and subclass messages.

Advantages of Method Overriding in Dart Programming Language

Some of the key advantages of method overriding in Dart make it quite flexible and maintainable. Let us look into them in some detail:

Improved Flexibility and Customization

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.');
  }
}

2. Promotes Reusability of Code

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.');
  }
}

3. Facilitates Polymorphism

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
}

4. Supports Extensible Systems

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.');
  }
}

5. Encourages Clean Design Patterns

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.');
  }
}

6. Improves Maintainability

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
  }
}

Disadvantages of Method Overriding in Dart Programming Language

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:

1. Increased Complexity

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
}

2. Potential for Bugs

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
}

3. Difficulty in Debugging

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
}

4. Inheritance Issues

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
}

5. Performance Overhead

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
}

Loss of Original Behavior

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.


Discover more from PiEmbSysTech

Subscribe to get the latest posts sent to your email.

Leave a Reply

Scroll to Top

Discover more from PiEmbSysTech

Subscribe now to keep reading and get access to the full archive.

Continue reading