Inversion of Control (IOC) and Hollywood Principle
Author: Mohammad J Iqbal
Hollywood Principle is a design principle, stated as “Don’t call us, we’ll call you” is closely related with “Inversion of Control” Hollywood calls Actors when they need them instead of actor reaches Hollywood.
#Dependency Injection (DI)
Dependency Injection is a software design technique in which the creation and binding of dependencies are done outside of the dependent class. Dependencies are provided already instantiated and ready to be used, hence the term “injection”; in contrast to the dependent class having to instantiate its dependencies internally, and having to know how to configure them, thus causing coupling.
-
High-level modules should not depend on low-level modules. Both should depend on abstractions.
-
Abstractions should not depend on details. Details should depend on abstractions.
#Inversion of Control (IOC)
Inversion of Control is a design principle that inverts the flow of control in a program by decoupling components, making them more flexible and easier to maintain. In a traditional approach, high-level components are in control of the flow and call low-level components. With IoC, this control is inverted, and low-level components are responsible for the flow, allowing high-level components to focus on their core functionalities.
Benefits of IoC: Decoupling, No change of unit test when implementation changes, Scalability, Reusability
Lets try to understand with an Example:
We have a PaymentService class(A High Level Class) that depends on a PaymentProcessor class called “PaypalPaymentProcessor” (Low Level Class). The PaymentService creates object of the PayplePaymentProcessor class and control flow.
package ood.payment;
/* High Level Class */
public class PaymentService1 {
/*High Level Class manaing creation and flow of low level class */
/* Low Level Class */
PaypalPaymentProcessor paypalPaymentProcessor = new PaypalPaymentProcessor();
public PaymentService1(){
}
public boolean createPayment(double amount, String currency, PaymentDetails paymentDetails){
return paypalPaymentProcessor.createPayment(amount,currency,paymentDetails);
}
public boolean refundPayment(String transactionId, double amount){
return paypalPaymentProcessor.refundPayment(transactionId,amount);
}
}
What if in future we need to use a different payment processor? or need to use the payment processor that the client wants?
If we want to keep this design we will need to modify this PaymentService1 class which will break “Open Close Principal” (SOLID)
Using “Inversion of Control” design principle we can write the class in a way so that we need to touch the code of this class when we want to switch a different payment processor or support multiple payment processor.
Lets define an interface IPaymentProcessor:
package ood.payment;
public interface IPaymentProcessor {
public boolean createPayment(double amount, String currency, PaymentDetails paymentDetails);
public boolean refundPayment(String transactionId, double amount);
}
An updated version of PaymentService that uses IPaymentProcessor Interface
package ood.payment;
/* Control is inverted, Instead of PaymentService creating PaymentProcessor, its being created and injected externally */
public class PaymentService {
IPaymentProcessor paymentProcessor;
public PaymentService(){
}
public PaymentService(IPaymentProcessor paymentProcessor){
this.paymentProcessor = paymentProcessor;
}
public void setPaymentProcessor(IPaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public boolean createPayment(double amount, String currency, PaymentDetails paymentDetails){
return paymentProcessor.createPayment(amount,currency,paymentDetails);
}
public boolean refundPayment(String transactionId, double amount){
return paymentProcessor.refundPayment(transactionId,amount);
}
}
Lets look at our payment processors.
package ood.payment;
public class PaypalPaymentProcessor implements IPaymentProcessor{
@Override
public boolean createPayment(double amount, String currency, PaymentDetails paymentDetails) {
return false;
}
@Override
public boolean refundPayment(String transactionId, double amount) {
return false;
}
}
package ood.payment;
public class StripePaymentProcessor implements IPaymentProcessor{
@Override
public boolean createPayment(double amount, String currency, PaymentDetails paymentDetails) {
return false;
}
@Override
public boolean refundPayment(String transactionId, double amount) {
return false;
}
}
Now how we create the objects of these concrete classes of payment processor?
We can use Factory class (Factory Design Pattern) like below:
package ood.payment;
public class PaymentProcessorFactory {
public static IPaymentProcessor createPaymentProcessor(String type) throws Exception {
return switch (type) {
case "paypal" -> new PaypalPaymentProcessor();
case "stripe" -> new StripePaymentProcessor();
default -> throw new Exception("No Payment Processor Found for provided type: " + type);
};
}
}
Driver class:
package ood.payment;
public class Main {
IPaymentProcessor paymentProcessor;
public void processPayment(String processorType,double amount,String currency,PaymentDetails paymentDetails) throws Exception {
paymentProcessor = PaymentProcessorFactory.createPaymentProcessor(processorType);
//Constructor Injection
PaymentService paymentService = new PaymentService(paymentProcessor);
/*
//Setter injection (Strategy Design Pattern)
PaymentService paymentService1 = new PaymentService();
paymentService1.setPaymentProcessor(paymentProcessor);
*/
paymentProcessor.createPayment(amount,currency, paymentDetails);
}
public static void main(String args[]) throws Exception{
Main main = new Main();
main.processPayment("stripe",100.00,"USD",new PaymentDetails());
}
}
The commented lines can be used for setter injection instead of constructor injection (Strategy Design Pattern)
Conclusion: If you are still here, you can understand that this Article is an extension of my previous Article “Programming with Interfaces” :-)