Graduate Program KB

Clean Architecture

Chapter 11 - DIP: Dependency Inversion Principle

  • DIP states "code that implements a high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies"
    • Flexible systems should rely on abstractions, not concrete implementations
  • Statically typed languages like Java should refer to abstract declarations, nothing concrete. Same concept can be applied in dynamically typed languages
    public interface PaymentMethod { 
        public void processPayment(double amount); 
    }
    
    public class CreditCardPayment implements PaymentMethod { 
        @Override
        public void processPayment(double amount) { // Credit card stuff }
    }
    
    public class PayPalPayment implements PaymentMethod { 
        @Override
        pubic void processPayment(double amount) { // Paypal stuff }
    }
    
    public class PaymentProcessor {
        private PaymentMethod paymentMethod; // Depends on abstraction
    
        public PaymentProcessor(PaymentMethod paymentMethod) {
            this.paymentMethod = paymentMethod;
        }
    
        public void makePayment(double amount) {
            paymentMethod.processPayment(amount);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            PaymentMethod creditCard = new CreditCardPayment();
            PaymentProcessor processor =  new PaymentProcessor(creditCard);
            processor.makePayment(100.0); // Credit card method
    
            PaymentMethod paypal = new PayPalPayment();
            processor = new PaymentProcessor(paypal);
            processor.makePayment(100.0); // Paypal method
        }
    }
    
  • Realistically, can't avoid depending on some concrete implementations
    • Ex. String in Java is concrete but shouldn't be avoided
    • However, String is built-in to Java and less subject to change
    • The same cannot be said for concrete code written by general programmers as they are volatile to active change from development

Stable Abstractions

  • Achieving stable architecture requires avoiding dependance of volatile, concrete implementations in favor of abstract interfaces
  • Coding practices:
    • Don't refer to volatile concrete classes: Refer to abstract interfaces instead
    • Don't derive from volatile concrete classes: Avoid deriving from volatile concrete classes to reduce dependency on specific implementations
      • Changes to Service don't directly affect CustomService
        public class Service() {
            public void action() {}
        }
        
        // Does not extend Service 
        public class CustomService() {
            private Service service;
        
            public CustomService(Service service) {
                this.service = service;
            }
        
            public void action() {
                service.action(); // Composition to delegate work to another service
                
                // Additional custom actions
            }
        }
        
    • Don't override concrete functions: Prevent inheriting unwanted dependencies, use abstract methods
      public class Animal {
          public void eat() {
              System.out.println('Animal is eating.');
          }
      
          public void makeSound() {
              System.out.println('Animal makes sound.');
          }
      }
      
      public class Dog extends Animal() {
          @Override
          public void makeSound() {
              System.out.println('Dog barks.');
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              Animal dog = new Dog();
              dog.eat(); // 'Animal is eating.'
          }
      }
      
    • Never mention the name of anything concrete and volatile
      • PaymentProcessor depends on PaymentMethod, not CreditCardPayment or PaypalPayment

Factories

  • Creating an object requires a source code dependency on the concrete definition of that object

    • Use an Abstract Factory to manage this dependency
    public class Application {
        private Service service;
    
        constructor(ServiceFactory factory) {
            this.service = factory.makeSvc();
        }
    }
    
    public interface ServiceFactory {
        public Service makeSvc();
    }
    
    public ServiceFactoryImpl implements ServiceFactory {
        @Override
        public Service makeSvc() {
            return new ConcreteImpl();
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            ServiceFactory factory = new ServiceFactoryImpl();
            Application app = new Application(factory);
        }
    }
    
  • The boundary in Figure 11.1 separates the abstract and concrete components

    • Looking at the dependency flow, all high-level and low-level concrete components depend on abstract components
    • Abstract components define the high-level business rules
    • Concrete components contain the low-level implementation details
    • Flow of control refers to how abstract components interact with low-level concrete components
    • Dependency inversion is about ensuring that high-level modules manage the flow of control but remain independent of concrete implementations

Concrete Components

  • In Figure 11.1, the ServiceFactoryImpl concrete component violates DIP as it depends on the concrete implementation of service
    • The solution is to isolate them into separated, small concrete components
    • Not all DIP violations can be removed, concrete classes need to be instantiated somewhere
  • The main function is a concrete component present in most systems
    • Ex. main will create an instance of ServiceFactoryImpl

Conclusion

  • DIP is important for determining how we organise source code dependencies
  • The architectural boundary is important to recognise, signifying the divide between abstract and concrete components
    • All concrete classes depending only on abstractions allows for easy extension
  • It enables us to begin creating the main functionality of a domain for an application
    • Creating an in-memory repository before thinking about ORM and database solutions
    • Create a mock API for testing purposes first