Cześć, dzisiaj skupimy się na problemie, w którym jedna klasa, do zrealizowania swojego celu, wykorzystuje inne klasy. Załóżmy, że w czasie ta liczba niezbędnych klas będzie narastała. Na szczęście sygnatura ich metod jest taka sama. My jednak nie chcielibyśmy modyfikować bazowej klasy za każdym razem, gdy dojdzie kolejna implementacja. Jak to rozwiązać? Już przedstawiam jedno z możliwych rozwiązań.

Sytuacja początkowa

Utwórzmy sobie prostą aplikację, w której mamy znowu ServiceA i ServiceB. Klasa o nazwie ServiceC z nich korzysta, aby wykonać swoje zadanie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class ServiceC {

  private final ServiceA serviceA;
  private final ServiceB serviceB;

  public ServiceC(ServiceA serviceA, ServiceB serviceB) {
    this.serviceA = serviceA;
    this.serviceB = serviceB;
  }

  public String returnSomething() {
    return serviceA.returnSomething() + serviceB.returnSomething();
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class ServiceCIntTest {

  @Autowired
  ServiceC serviceC;

  @Test
  void test() {
    assertEquals("AB", serviceC.returnSomething());
  }

}

Przedstawienie problemu

Załóżmy, że dochodzi nam kolejny serwis, ServiceX, który musimy wykorzystać w ServiceC. Wstrzykujemy go jako pole i wykorzystujemy w metodzie returnSomething. Test, który wcześniej napisaliśmy, oczywiście nie przejdzie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class ServiceC {

  private final ServiceA serviceA;
  private final ServiceB serviceB;
  private final ServiceX serviceX;

  public ServiceC(ServiceA serviceA, ServiceB serviceB, ServiceX serviceX) {
    this.serviceA = serviceA;
    this.serviceB = serviceB;
    this.serviceX = serviceX;
  }

  public String returnSomething() {
    return serviceA.returnSomething() + serviceB.returnSomething() + serviceX.returnSomething();
  }

}

Załóżmy, że doszedłby kolejny serwis. Musielibyśmy powtórzyć całą operację od nowa. Co w takim razie możemy zrobić?

Jedno z możliwych rozwiązań

Skoro wszystkie serwisy mają identyczną metodę to możemy po raz kolejny wyciągnąć wspólną abstrakcję o nazwie ReturningSomething (podobnie jak w poprzednim wpisie).

1
2
3
public interface ReturningSomething {
  String returnSomething();
}

Teraz każdy z serwisów musi zaimplementować nową abstrakcję.

1
2
3
4
5
6
7
8
9
@Component
public class ServiceA implements ReturningSomething {

  @Override
  public String returnSomething() {
    return "A";
  }

}

Dochodzimy do clue tego wpisu. Zamiast w ServiceC korzystać z konkretnych implementacji serwisów, możemy skorzystać z kolekcji o typie ReturningSomething. Jeśli chodzi o Springa, to on nam bez żadnego problemu zbierze wszystkie instancje klas spełniających ten kontrakt i zagreguje je w kolekcję.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class ServiceC {

  private final List<ReturningSomething> returnSomethings;

  public ServiceC(List<ReturningSomething> returnSomethings) {
    this.returnSomethings = returnSomethings;
  }

  public String returnSomething() {
    return returnSomethings.stream()
        .map(ReturningSomething::returnSomething)
        .reduce("", (s1, s2) -> s1 + s2);
  }

}

Nam pozostaje zmienienie implementacji w metodzie returnSomething, aby opierała się ona o nową abstrakcję dostępną w ramach klasy ServiceC. Oczywiście należy pamiętać, aby każda z implementacji interfejsu ReturningSomething była beanem Springa.

Uruchamiamy test jako weryfikację czy nic nie popsuliśmy. Przechodzi bez żadnego problemy. Dodając teraz kolejną implementację np. w postaci ServiceY, nic nie musimy zmieniać w ServiceC. I o to nam właśnie chodziło!

Podsumowanie

ServiceC uzyskał stabilną implementację. Jeśli faktycznie taki będzie wektor zmian, to dodawanie kolejnych implementacji nie będzie stanowiło żadnych problemów. Przy okazji Spring wszystko załatwił za nas w tle. On połączył dostępne implementacje i wstrzyknął w oczekiwane przez nas miejsce.

To by było na tyle dzisiaj, dzięki ci za Twój czas i do zobaczenia w kolejnym wpisie, na razie i cześć.