Always depend on Abstractions, a Dart example.
Tags: softwareengineering, dart, solidprinciples, dependencyinversion
Published: 2024-01-15 13:18:19

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.



```dart
abstract class SmsSender {

   void sendSms({String from, String to});
}
```

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.



```dart
class TwilioSender implements SmsSender {

   @override
   void sendSms({String from, String to})
   {
    //your sms complex logic :)
   }
}
```

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



```dart
class SmsService {

  final SmsSender smsSender;

   SmsService({
     required this.smsSender,
  });


   void sendSms({String from, String to})
   {
     smsSender.sendSms(from: from, to: to);
   }
}
```

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](https://en.wikipedia.org/wiki/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

```dart
 void main(){

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

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

}
```

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.