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 } }
- Changes to Service don't directly affect CustomService
- 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