28 Oct 2023
Examples of Dependency Inversion(DI):
Example 1: Dependency on Abstraction
Problem Statement: The OrderService directly depends on a concrete Database class, which violates the Dependency Inversion Principle.
// Problem: High-level module directly depends on a low-level module
class OrderService {
private Database database;
// Constructor creates a specific Database instance (low-level module)
public OrderService() {
this.database = new Database();
}
// High-level module uses the specific Database instance
public void saveOrder(Order order) {
database.save(order);
}
}
Solution Description: In the solution, we introduce an abstraction (interface Database) and use dependency injection. This follows the Dependency Inversion Principle. The high-level module (OrderService) no longer creates the specific Database instance but accepts it through the constructor, allowing different database implementations to be injected.
// Solution: Introduce an abstraction (Database) and use dependency injection
interface Database {
void save(Order order);
}
class OrderService {
private Database database;
// Constructor takes a Database instance (high-level module is not responsible for creating it)
public OrderService(Database database) {
this.database = database;
}
// High-level module uses the Database abstraction
public void saveOrder(Order order) {
database.save(order);
}
}
Example 2: Tight Coupling
Problem Statement: The problem in this example is the tight coupling between the high-level module (OrderService) and a specific low-level module (MySQLDatabase).
// Problem: Tight coupling between high-level module and specific low-level module
class OrderService {
private MySQLDatabase database;
public OrderService() {
this.database = new MySQLDatabase();
}
public void saveOrder(Order order) {
database.save(order);
}
}
Solution Description: In the solution, we introduce an interface (Database) that the MySQLDatabase class implements. The OrderService now depends on the Database interface, and you can inject different database implementations. This makes it easy to change the database type without altering the high-level module, aligning with the Dependency Inversion Principle.
// Solution: Introduce an abstraction (Database) and use dependency injection
interface Database {
void save(Order order);
}
class MySQLDatabase implements Database {
public void save(Order order) {
// Save order in MySQL
}
}
class OrderService {
private Database database;
public OrderService(Database database) {
this.database = database;
}
public void saveOrder(Order order) {
database.save(order);
}
}
Example 3: Inflexible Code
Problem Statement: The original ReportGenerator class is tightly coupled to the PDF format, making it challenging to support other report formats.
// Problem: The ReportGenerator is tightly coupled to PDF format
class ReportGenerator {
public void generatePDFReport(ReportData data) {
// Generate a PDF report
}
}
Solution Description: The solution introduces an interface (ReportFormatter) that defines the generateReport method. Two implementations, PDFReportFormatter and CSVReportFormatter, are created to handle different report formats. The ReportGenerator now depends on the ReportFormatter interface, making it flexible and adhering to the Dependency Inversion Principle. You can easily extend the code to support additional report formats by creating new implementations of the ReportFormatter interface.
// Solution: Abstract report generation and provide flexibility for various output formats
interface ReportFormatter {
void generateReport(ReportData data);
}
class PDFReportFormatter implements ReportFormatter {
public void generateReport(ReportData data) {
// Generate a PDF report
}
}
class CSVReportFormatter implements ReportFormatter {
public void generateReport(ReportData data) {
// Generate a CSV report
}
}
class ReportGenerator {
private ReportFormatter formatter;
public ReportGenerator(ReportFormatter formatter) {
this.formatter = formatter;
}
public void generateReport(ReportData data) {
formatter.generateReport(data);
}
}
Example 4: Direct Instantiation
Problem Statement: The NotificationService directly creates an instance of EmailSender, tightly coupling the high-level module to a specific notification method.
// Problem: NotificationService directly creates an EmailSender instance
class NotificationService {
private EmailSender emailSender;
public NotificationService() {
this.emailSender = new EmailSender();
}
public void sendNotification(String message) {
emailSender.sendEmail(message);
}
}
Solution Description: The solution introduces an interface, NotificationSender, that defines the send method for notifications. The EmailSender class implements this interface. The NotificationService now depends on the NotificationSender interface and accepts an instance of it through its constructor. This allows for easy switching between different notification methods (e.g., SMS, Push Notifications) without modifying the high-level module, aligning with the Dependency Inversion Principle.
// Solution: Inject the dependency and use an interface for flexibility
interface NotificationSender {
void send(String message);
}
class EmailSender implements NotificationSender {
public void send(String message) {
// Send an email
}
}
class NotificationService {
private NotificationSender sender;
public NotificationService(NotificationSender sender) {
this.sender = sender;
}
public void sendNotification(String message) {
sender.send(message);
}
}
Example 5: Hard-Coded Dependencies
Problem Statement: The BillingService class has hard-coded dependencies on specific tax and payment processors, making it inflexible and difficult to switch between different processors.
// Problem: BillingService has hard-coded dependencies on specific processors
class BillingService {
private TaxProcessor taxProcessor;
private PaymentProcessor paymentProcessor;
public BillingService() {
this.taxProcessor = new GSTTaxProcessor();
this.paymentProcessor = new CreditCardPaymentProcessor();
}
public void processPayment(Order order) {
double tax = taxProcessor.calculateTax(order);
paymentProcessor.processPayment(order, tax);
}
}
Solution Description: In the solution, we introduce two interfaces, TaxProcessor and PaymentProcessor, defining methods for tax calculation and payment processing. Two classes, GSTTaxProcessor and CreditCardPaymentProcessor, implement these interfaces. The BillingService now depends on these interfaces and accepts instances of the processors through its constructor. This change allows for flexibility in choosing different tax and payment processors without modifying the high-level module, complying with the Dependency Inversion Principle.
// Solution: Use dependency injection and abstractions for flexibility
interface TaxProcessor {
double calculateTax(Order order);
}
interface PaymentProcessor {
void processPayment(Order order, double tax);
}
class GSTTaxProcessor implements TaxProcessor {
public double calculateTax(Order order) {
// Calculate tax using GST rules
}
}
class CreditCardPaymentProcessor implements PaymentProcessor {
public void processPayment(Order order, double tax) {
// Process payment using a credit card
}
}
class BillingService {
private TaxProcessor taxProcessor;
private PaymentProcessor paymentProcessor;
public BillingService(TaxProcessor taxProcessor, PaymentProcessor paymentProcessor) {
this.taxProcessor = taxProcessor;
this.paymentProcessor = paymentProcessor;
}
public void processPayment(Order order) {
double tax = taxProcessor.calculateTax(order);
paymentProcessor.processPayment(order, tax);
}
}
In this completed example, the BillingService class depends on the TaxProcessor and PaymentProcessor interfaces, and instances of specific processors are injected through its constructor. This design follows the Dependency Inversion Principle, allowing for flexibility and easy substitution of different tax and payment processors without modifying the high-level module.
Example 6: Car Manufacturing
Problem Statement: In a car manufacturing system, the high-level module responsible for assembling and testing cars directly depends on low-level modules for specific components like engines, tires, and electronics. This tight coupling makes it challenging to switch between different component suppliers or manufacturing processes.
// Problem: High-level module directly depends on low-level modules for components and assembly
class CarAssemblyLine {
private Engine engine;
private Tire tire;
private Electronics electronics;
public CarAssemblyLine() {
this.engine = new Engine();
this.tire = new Tire();
this.electronics = new Electronics();
}
// Methods for assembling and testing cars
// ...
}
Solution Description: To adhere to the Dependency Inversion Principle, we can introduce abstractions and use dependency injection. Define interfaces for components like engines, tires, and electronics, allowing different component suppliers and manufacturing processes. The high-level car assembly module should depend on these interfaces and accept instances of them through the constructor. This approach decouples the car manufacturing system from specific component implementations and makes it more flexible.
// Interfaces for car components
interface Engine {
void start();
void stop();
}
interface Tire {
void inflate();
void deflate();
}
interface Electronics {
void installNavigationSystem();
void installInfotainmentSystem();
}
class CarAssemblyLine {
private Engine engine;
private Tire tire;
private Electronics electronics;
public CarAssemblyLine(Engine engine, Tire tire, Electronics electronics) {
this.engine = engine;
this.tire = tire;
this.electronics = electronics;
}
// Methods for assembling and testing cars using engine, tire, and electronics components
// ...
}
In this example, applying the Dependency Inversion Principle allows for flexibility in choosing component suppliers and manufacturing processes, as the high-level module depends on abstractions (interfaces) rather than specific component implementations. This makes it easier to adapt the car manufacturing process to different suppliers and manufacturing methods.