Service Locator vs Dependency Injection

Service Locator vs Dependency Injection

Service Locator (lokalizator usług) i Dependency Injection (wstrzykiwanie zależności) to wzorce które realizują koncepcję Inversion Of Control (odwrócenie sterowania). Na wstępie zaznaczam, że Service Locator to moim zdaniem antywzorzec i nie powinien być stosowany co udowodnię w tym artykule. Na początek przykładowa implementacja wzorca Service Locator.

Implementacja wzorca Service Locator:

public interface Service {
    String getName();
}

Implementacja przykładowych serwisów:

public class ServiceFirstImpl implements Service {
   public String getName() {
      return "ServiceFirstImpl";
   }
}
public class ServiceSecondImpl implements Service {
   public String getName() {
      return "ServiceSecondImpl";
   }
}

Wyszukiwanie serwisów na podstawie ich nazwy czyli klasa Context:

public class Context {
 
   public Object lookup(String jndiName){
 
      if(jndiName.equalsIgnoreCase("ServiceFirstImpl")){
         return new ServiceFirstImpl();
      }
      else if (jndiName.equalsIgnoreCase("ServiceSecondImpl")){
         return new ServiceSecondImpl();
      }
      return null;        
   }
}

Klasa która dodaje serwisy do pamięci podręcznej – cache:

public class Cache {
 
   private List<Service> services;
 
   public Cache(){
      services = new ArrayList<Service>();
   }
 
   public Service getService(String serviceName){
 
      List<Service> servicesFilter = services.stream().filter(
              service -> service.getName().equalsIgnoreCase(serviceName)
      ).collect(Collectors.toList());
 
      if(servicesFilter.isEmpty()) {
         return null;
      } else {
         return servicesFilter.get(0);
      }
   }
 
   public void addService(Service newService){
 
      boolean exists = false;
 
      for (Service service : services) {
         if(service.getName().equalsIgnoreCase(newService.getName())){
            exists = true;
         }
      }
      if(!exists){
         services.add(newService);
      }
   }
}

Kluczowa klasa ServiceLocator:

public class ServiceLocator {
 
   private static Cache cache;
 
   static {
      cache = new Cache();        
   }
   public static Service getService(String jndiName){
 
      Service service = cache.getService(jndiName);
 
      if(service != null){
         return service;
      }
      Context context = new Context();
      Service service1 = (Service)context.lookup(jndiName);
      cache.addService(service1);
      return service1;
   }
}

przypomnienie – blok statyczny wywoływany jest w momencie pierwszego odwołania do jednej ze składowych statycznych klasy.

Testy aplikacji:

public class ServiceLocatorPatternDemo {
   public static void main(String[] args) {
 
      Service service = ServiceLocator.getService("ServiceFirstImpl");
      System.out.println(service.getName());
      service = ServiceLocator.getService("ServiceSecondImpl");
      System.out.println(service.getName());
      service = ServiceLocator.getService("ServiceFirstImpl");
      System.out.println(service.getName());
      service = ServiceLocator.getService("ServiceSecondImpl");
      System.out.println(service.getName());
   }
}

wynik:

ServiceFirstImpl
ServiceSecondImpl
ServiceFirstImpl
ServiceSecondImpl

W czym tkwi problem?

Dużym problemem użycia wzorca Service Locator w stosunku do wzorca wstrzykiwania zależności jest konieczność modyfikacji kodu źródłowego komponentów w przypadku zamiany lub refaktoryzacji biblioteki realizującej zadania kontenera IoC.

class PDFDocument {
    public void printPDF(Document document) {
        PrintManager printerManager = ServiceLocator.getService("PrinterManagerImpl");
        printerManager.print(document);
    }
}

w sytuacji kiedy wywołujemy metodę prrintPDF i nie został zarejestrowany wcześniej obiekt (z ang. bean) PrinterManagerImpl uzyskujemy błąd na etapie wykonania – Runtime! Należy zatem pamiętać o prawidłowym zarejestrowaniu obiektów co bez wnikania w dokumentację jest po prostu niemożliwe. Ponadto zależności są ukryte w klasach. Tracimy również możliwość testowania kodu z użyciem testów jednostkowych. Kolejna sprawa dotyczy refaktoringu. Jeśli metoda printPDF zostanie zmodyfikowana i zacznie wykorzystywać kolejne zależności to może dojść do sytuacji, że program przestanie działać i to nie na etapie kompilacji, ale na etapie wykonania.

Jeśli jednak zamienimy powyższy kod na:

class PDFDocument {
 
    private PrintManager printerManager;
 
    PDFDocument(PrintManager printerManager){
        this.pronterManager = printerManager;
    }
 
    public void printPDF(Document document) {
        printerManager.print(document);
    }
}

to od razu widać w jaki sposób obiekty powinny być tworzone – że PDFDocument zależy od PrinterManagera. Należy zatem w przypadku testów jednostkowych utworzyć Stub’a dla PrintManagera. Ponadto jesteśmy w stanie wykryć błędy już na etapie kompilacji!

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

Leave a comment

Your email address will not be published.


*