Dependency Inversion Principle (DIP):

SOLID - SRP

The Dependency Inversion Principle (DIP) states that high-level modules/classes should not depend on low-level modules/classes directly. Instead, both should depend on abstractions (interfaces or abstract classes). It also suggests that abstractions should not depend on details, but details should depend on abstractions. This principle helps to achieve loose coupling and promotes easier maintenance, testing, and flexibility in software systems.

Let's consider a practical use case to understand the Dependency Inversion Principle:

Use Case: Order Processing System

Example: Suppose we are developing an order processing system that handles various tasks related to order fulfillment, such as creating orders, calculating prices, and sending notifications.

# Without applying the DIP:

In a traditional implementation, the high-level OrderProcessor class might directly depend on low-level classes like OrderRepository, PriceCalculator, and EmailService. This tightly couples the high-level module to the specific implementations, making it difficult to change or extend them.

# Applying the DIP:

To apply the DIP, we can introduce interfaces/abstractions and invert the dependencies:

Syntax:
                
    public interface OrderRepository {
        void createOrder(Order order);
        // other methods related to order persistence
    }

    public interface PriceCalculator {
        double calculatePrice(Order order);
        // other methods related to price calculations
    }

    public interface NotificationService {
        void sendNotification(String message);
        // other methods related to sending notifications
    }

    public class OrderProcessor {
        private OrderRepository orderRepository;
        private PriceCalculator priceCalculator;
        private NotificationService notificationService;

        public OrderProcessor(
                OrderRepository orderRepository,
                PriceCalculator priceCalculator,
                NotificationService notificationService
        ) {
            this.orderRepository = orderRepository;
            this.priceCalculator = priceCalculator;
            this.notificationService = notificationService;
        }

        public void processOrder(Order order) {
            orderRepository.createOrder(order);
            double price = priceCalculator.calculatePrice(order);
            // perform other order processing tasks
            notificationService.sendNotification("Order processed successfully");
        }
    }
                
            

In this example, we introduce interfaces (OrderRepository, PriceCalculator, NotificationService) that define the expected behavior for order persistence, price calculations, and notification services. The high-level OrderProcessor class depends on these abstractions instead of concrete implementations.

The dependencies are inverted through the constructor injection in the OrderProcessor class. It no longer depends on specific low-level classes but relies on the interfaces. This allows for flexibility in choosing different implementations at runtime, enabling easier testing, extensibility, and maintenance.

For instance, we can have concrete classes that implement these interfaces, such as MySQLOrderRepository, TaxBasedPriceCalculator, and EmailNotificationService. By injecting the appropriate implementations into the OrderProcessor class, we can easily switch between different persistence strategies, pricing algorithms, or notification mechanisms without modifying the high-level OrderProcessor code.

The Dependency Inversion Principle helps in decoupling modules, promoting modular design, and facilitating easier maintenance, testing, and flexibility in the system. It reduces the impact of changes in the low-level modules on the high-level modules and allows for easy substitution of implementations without affecting the overall system.