- allows us to remove the hard-coded dependencies
- and make our application
loosely coupled
,extendable
andmaintainable
.
we will see simple example Without (DI) and then how to use dependency injection pattern to achieve (loose coupling and extendability) in the application.
- Let’s say we have an application where we consume EmailService to send emails.
- Normally we would implement this like below.
class EmailService {
void sendEmail(String message, String receiver) {
//logic to send email
print("Email sent to " + receiver + " with Message=" + message);
}
}
- EmailService class holds the logic to send an email message to the recipient email address.
- Our application code will be like below.
class MyApplication {
EmailService _email = EmailService();
void processMessages(String msg, String rec) {
//do some msg validation, manipulation logic etc
_email.sendEmail(msg, rec);
}
}
- Our client code that will use MyApplication class to send email messages will be like below.
void main() {
MyApplication app = MyApplication();
app.processMessages("Hi Pankaj", "[email protected]");
}
-
there seems nothing wrong with the above implementation.
-
But above code logic has certain limitations.
-
MyApplication class is
- (1-responsible to initialize the email service)
- and (2-then use it).
- This leads to hard-coded dependency.
- If we want to switch to some other advanced email service in the future,
- it will require code changes in MyApplication class.
- This makes our application hard to extend and if email service is used in multiple classes then that would be even harder.
- If we want to extend our application to provide an additional messaging feature,
- such as SMS or Facebook message
- then we would need to write another application for that.
- This will involve code changes in application classes and in client classes too.
- Testing the application will be very difficult since our application is directly creating the email service instance.
- There is no way we can mock these objects in our test classes.
- One can argue that we can remove the email service instance creation from MyApplication class
- by having a constructor that requires email service as an argument.
- One can argue that we can remove the email service instance creation from MyApplication class by having a constructor that requires
email service
as an argument.
class MyApplication {
EmailService _email;
MyApplication(this._email);
void processMessages(String msg, String rec) {
//do some msg validation, manipulation logic etc
_email.sendEmail(msg, rec);
}
}
-
Service Class
: should be designed with base class or interface.- It’s better to prefer interfaces or abstract classes that would define contract for the services.
-
Client classes
: should be written in terms of service interface. -
Injector classes
: that will initialize the services and then the Client classes. -
For our case, we can have MessageService that will declare the contract for service implementations.
abstract class MessageService {
void sendMessage(String msg, String rec);
}
- Now let’s say we have
(Email and SMS services)
that implement the above interfaces.
class EmailServiceImpl implements MessageService {
@override
void sendMessage(String msg, String rec) {
//logic to send email
print("Email sent to $rec with Message=$msg ");
}
}
class SMSServiceImpl implements MessageService {
@override
void sendMessage(String msg, String rec) {
//logic to send SMS
print("SMS sent to $rec with Message=$msg ");
}
}
- We are not required to have base interfaces for Client classes
- but I will have a Client interface declaring contract for Client classes.
abstract class Client {
void processMessages(String msg, String rec);
}
/// My Client class implementation is like below.
class MyDIApplication implements Client {
MessageService _service;
MyDIApplication(this._service);
@override
void processMessages(String msg, String rec) {
//do some msg validation, manipulation logic etc
_service.sendMessage(msg, rec);
}
}
- Notice that our application class is just using the service.
- It does not initialize the service that leads to better “separation of concerns”.
- Also use of service interface allows us to easily test the application by mocking the MessageService and bind the services at runtime rather than compile time.
- Let’s have an interface MessageServiceInjector with method declaration that returns the Client class.
abstract class MessageServiceInjector {
Client getClient();
}
- Now for every service,
- we will have to create injector classes like below.
class EmailServiceInjector implements MessageServiceInjector {
@override
Client getClient() {
return MyDIApplication(EmailServiceImpl());
}
}
class SMSServiceInjector implements MessageServiceInjector {
@override
Client getClient() {
return MyDIApplication(SMSServiceImpl());
}
/// Now let’s see how our client applications will use the application with a simple program.
void main() {
String msg = "Hi Pankaj";
String email = "[email protected]";
String phone = "4088888888";
MessageServiceInjector injector;
Client app;
//Send email
injector = EmailServiceInjector();
app = injector.getClient();
app.processMessages(msg, email);
//Send SMS
injector = SMSServiceInjector();
app = injector.getClient();
app.processMessages(msg, phone);
}