DEV Community

Cover image for Always depend on Abstractions, a Dart example.
Ethiel ADIASSA
Ethiel ADIASSA

Posted on • Edited on

Always depend on Abstractions, a Dart example.

Abstractions provide a layer of separation between complex underlying details and higher-level functionalities.

In Software Engineering it is always stated and recommended to depend on abstractions rather than concretions. It means your modules should not rely on concrete implementations, on the contrary, they must know no implementation details, complexity, or underlying mechanism of their dependencies. This complies with the Dependency Inversion Principle (DIP) of SOLID.

By programming to abstractions rather than concrete implementations, Dart developers can

  • create code that is decoupled and easy to maintain;
  • write more reusable code by encapsulating common functionalities;
  • write unit tests easily and efficiently ;
  • write code adaptable to change, shielded from the need to make extensive changes throughout the codebase.

Now, let's dive into a practical example with explanations. For this, we'll use Dart programming language.

Let's say you were to write a simple app to send SMS keeping in mind the principle we've seen above.

abstract class SmsSender {

   void sendSms({String from, String to});
}
Enter fullscreen mode Exit fullscreen mode

First, we have an abstraction of our SMS sender, it sets common functionalities every SMS sender should have. This is an abstract class and cannot be instantiated, but every SmsSender (child) should implement the sendSms method and define its low-level implementation details.

class TwilioSender implements SmsSender {

   @override
   void sendSms({String from, String to})
   {
    //your sms complex logic :)
   }
}
Enter fullscreen mode Exit fullscreen mode

The TwilioSender is a SmsSender, hence must implement sendSms method. It can be used interchangeably with its parent class without any unexpected behavior.

class SmsService {

  final SmsSender smsSender;

   SmsService({
     required this.smsSender,
  });


   void sendSms({String from, String to})
   {
     smsSender.sendSms(from: from, to: to);
   }
}
Enter fullscreen mode Exit fullscreen mode

Now, we have a dedicated service to send an SMS that depends on our abstract SmsSender class, which can be a Twilio sender or any of your choice. This is called Dependency Injection. This is the most important aspect since the SmsService module doesn't depend on any concrete implementation of SmsSender. This enables further scalability and adaptability to change.

Let's see a simple usage of those classes.

NB: I'm not using a dedicated dependency injection library and leaving some implementations for the sake of simplicity

 void main(){

  //inject TwilioSender inside SmsService
  SmsService = SmsService(TwilioSender());

  smsService.sendSms(to: '+22809453945', from: '+2250503244804')

}
Enter fullscreen mode Exit fullscreen mode

The TwilioSender can easily be replaced later by any other SMS sender. The SmsService doesn't and needs not to know the implementation details of an SMS sender, it simply wants to send an SMS.

That's the beauty of this principle.

In the complex and ever-changing field of software engineering, embracing abstractions is not merely a convenience but a necessity. Abstractions simplify complexity, enhance productivity, promote code reusability, and contribute to the maintainability and scalability of software systems.

I hope you understand and will keep in mind this in your coding endeavor.

Top comments (1)

Collapse
 
semperfi_13 profile image
Adamou Nikiema πŸ‡§πŸ‡«

Thanks for sharing