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});
}
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 :)
}
}
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);
}
}
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')
}
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)
Thanks for sharing