Skip to main content

Practice Problems for Dependency Inversion Principle

Dependency Inversion Principle (DIP)

Practice Problems

To master the Dependency Inversion Principle (DIP), it's important to engage in practical exercises and projects. Here are some coding exercises, sample projects, quizzes, and multiple-choice questions (MCQs) to help reinforce your understanding.

Code Refactoring Exercise

Given:

A code snippet that violates DIP by having the high-level module depend directly on the low-level module:

class CreditCardPayment {
public void process() {
System.out.println("Processing credit card payment");
}
}

class PaymentProcessor {
private CreditCardPayment payment;

public PaymentProcessor() {
this.payment = new CreditCardPayment();
}

public void processPayment() {
payment.process();
}
}

Challenge: Refactor the code to adhere to DIP.

Refactored:

// Abstraction
interface PaymentMethod {
void process();
}

// High-Level Module
class PaymentProcessor {
private PaymentMethod payment;

public PaymentProcessor(PaymentMethod payment) {
this.payment = payment;
}

public void processPayment() {
payment.process();
}
}

// Low-Level Module
class CreditCardPayment implements PaymentMethod {
@Override
public void process() {
System.out.println("Processing credit card payment");
}
}

class PayPalPayment implements PaymentMethod {
@Override
public void process() {
System.out.println("Processing PayPal payment");
}
}

// Usage
public class Main {
public static void main(String[] args) {
PaymentMethod paymentMethod = new CreditCardPayment();
PaymentProcessor processor = new PaymentProcessor(paymentMethod);
processor.processPayment();

paymentMethod = new PayPalPayment();
processor = new PaymentProcessor(paymentMethod);
processor.processPayment();
}
}

Design Challenge

Challenge: Design a class hierarchy for a notification system that adheres to DIP. The system should have a high-level module that depends on abstractions for sending notifications, and low-level modules for email, SMS, and push notifications.

Example:

// Abstraction
interface NotificationService {
void sendNotification(String message);
}

// High-Level Module
class NotificationManager {
private NotificationService notificationService;

public NotificationManager(NotificationService notificationService) {
this.notificationService = notificationService;
}

public void notifyUser(String message) {
notificationService.sendNotification(message);
}
}

// Low-Level Modules
class EmailNotification implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Sending email: " + message);
}
}

class SMSNotification implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Sending SMS: " + message);
}
}

class PushNotification implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Sending push notification: " + message);
}
}

// Usage
public class Main {
public static void main(String[] args) {
NotificationService notificationService = new EmailNotification();
NotificationManager manager = new NotificationManager(notificationService);
manager.notifyUser("Your order has been shipped!");

notificationService = new SMSNotification();
manager = new NotificationManager(notificationService);
manager.notifyUser("Your package is out for delivery!");
}
}

Coding Exercises

  • Exercise 1: Implement a Logging System:
    • Description: Create an abstraction for a logging service. Implement this abstraction in different classes for file logging, database logging, and console logging. Ensure that the high-level module depends on the abstraction.
    • Example:
// Abstraction
interface Logger {
void log(String message);
}

// High-Level Module
class LogManager {
private Logger logger;

public LogManager(Logger logger) {
this.logger = logger;
}

public void logMessage(String message) {
logger.log(message);
}
}

// Low-Level Modules
class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Logging to file: " + message);
}
}

class DatabaseLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Logging to database: " + message);
}
}

class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Logging to console: " + message);
}
}

// Usage
public class Main {
public static void main(String[] args) {
Logger logger = new FileLogger();
LogManager logManager = new LogManager(logger);
logManager.logMessage("This is a file log message");

logger = new DatabaseLogger();
logManager = new LogManager(logger);
logManager.logMessage("This is a database log message");

logger = new ConsoleLogger();
logManager = new LogManager(logger);
logManager.logMessage("This is a console log message");
}
}

Sample Projects

  • Project 1: Inventory Management System:
    • Description: Design an inventory management system with abstractions for product management, inventory tracking, and order processing. Implement these abstractions in different classes for managing products, tracking inventory levels, and processing orders. Ensure that the high-level module depends on the abstractions.
    • Example:
// Abstractions
interface ProductManagement {
void addProduct(String product);
void removeProduct(String product);
}

interface InventoryTracking {
void updateInventory(String product, int quantity);
int checkInventory(String product);
}

interface OrderProcessing {
void processOrder(String product, int quantity);
}

// High-Level Module
class InventoryManager {
private ProductManagement productManagement;
private InventoryTracking inventoryTracking;
private OrderProcessing orderProcessing;

public InventoryManager(ProductManagement productManagement, InventoryTracking inventoryTracking, OrderProcessing orderProcessing) {
this.productManagement = productManagement;
this.inventoryTracking = inventoryTracking;
this.orderProcessing = orderProcessing;
}

public void manageInventory(String product, int quantity) {
productManagement.addProduct(product);
inventoryTracking.updateInventory(product, quantity);
orderProcessing.processOrder(product, quantity);
}
}

// Low-Level Modules
class SimpleProductManagement implements ProductManagement {
@Override
public void addProduct(String product) {
System.out.println("Adding product: " + product);
}

@Override
public void removeProduct(String product) {
System.out.println("Removing product: " + product);
}
}

class SimpleInventoryTracking implements InventoryTracking {
@Override
public void updateInventory(String product, int quantity) {
System.out.println("Updating inventory for product: " + product + ", quantity: " + quantity);
}

@Override
public int checkInventory(String product) {
System.out.println("Checking inventory for product: " + product);
return 100; // Sample return value
}
}

class SimpleOrderProcessing implements OrderProcessing {
@Override
public void processOrder(String product, int quantity) {
System.out.println("Processing order for product: " + product + ", quantity: " + quantity);
}
}

// Usage
public class Main {
public static void main(String[] args) {
ProductManagement productManagement = new SimpleProductManagement();
InventoryTracking inventoryTracking = new SimpleInventoryTracking();
OrderProcessing orderProcessing = new SimpleOrderProcessing();

InventoryManager manager = new InventoryManager(productManagement, inventoryTracking, orderProcessing);
manager.manageInventory("Product1", 10);
}
}

Quizzes

  • Quiz 1: Identifying DIP Violations:
    • Question: Given the following code, identify the DIP violation and suggest a way to fix it.
class FileStorage {
public void save(String data) {
System.out.println("Saving data to file");
}
}

class DataManager {
private FileStorage storage;

public DataManager() {
this.storage = new FileStorage();
}

public void saveData(String data) {
storage.save(data);
}
}
  • Answer:
// Abstraction
interface Storage {
void save(String data);
}

// Low-Level Module
class FileStorage implements Storage {
@Override
public void save(String data) {
System.out.println("Saving data to file");
}
}

// High-Level Module
class DataManager {
private Storage storage;

public DataManager(Storage storage) {
this.storage = storage;
}

public void saveData(String data) {
storage.save(data);
}
}

// Usage
public class Main {
public static void main(String[] args) {
Storage storage = new FileStorage();
DataManager dataManager = new DataManager(storage);
dataManager.saveData("Sample data");
}
}
  • Quiz 2: DIP and Abstractions:
    • Question: How does depending on abstractions rather than concrete implementations support the Dependency Inversion Principle (DIP)? Provide examples.
    • Answer:
      • Explanation: Depending on abstractions ensures that high-level modules are not tied to specific implementations. This decoupling allows for easier replacement or extension of low-level modules without affecting the high-level modules. For example, a PaymentProcessor class depending on a PaymentMethod interface can support multiple payment methods like CreditCardPayment and PayPalPayment without needing changes to the PaymentProcessor class.

Multiple-Choice Questions (MCQs)

  • MCQ 1: What does the Dependency Inversion Principle state?

    • A) High-level modules should not depend on low-level modules. Both should depend on abstractions.
    • B) Classes should inherit from a single base class.
    • C) Methods should only be implemented once.
    • D) Classes should be open for extension but closed for modification.

    Answer: A) High-level modules should not depend on low-level modules. Both should depend on abstractions.

  • MCQ 2: Which of the following is a violation of DIP?

    • A) A class that depends on an interface.
    • B) A class that directly depends on a concrete implementation.
    • C) A class that uses dependency injection.
    • D) A class that implements multiple interfaces.

    Answer: B) A class that directly depends on a concrete implementation.

  • MCQ 3: How can you implement DIP in a large system?

    • A) By creating a single interface with all methods.
    • B) By using dependency injection frameworks to manage dependencies.
    • C) By avoiding the use of interfaces.
    • D) By directly creating dependencies within high-level modules.

    Answer: B) By using dependency injection frameworks to manage dependencies.

By working through these extensive practice problems, quizzes, and MCQs, you'll gain hands-on experience with the Dependency Inversion Principle, helping you to apply it effectively in your projects. These exercises will deepen your understanding and improve your ability to create maintainable, scalable, and flexible software systems.