Teoria
Odwracanie zależności to ostatnia z reguł SOLID, jest ona jedną z ważniejszych zasad programowania obiektowego i można pokusić się o stwierdzenie, że jest to fundament obecnego podejścia do pisania oprogramowania. Dwa jej główne założenia to:
- moduły wyższego poziomu, nie powinny być zależne od tych z poziomu niższego, Jedne i drugie powinny zależeć od abstrakcji (np. interfejsów)
- abstrakcje nie powinny zależeć od szczegółów, to szczegóły (konkretne implementacje) zależą od abstrakcji
Przykład
Rozważmy przykład z mojej aplikacji pogodowej (można podejrzeć w dziale portfolio). Najpierw złe podejście. Mamy klasę WeatherService, która jest odpowiedzialna za dostarczanie aktualnej pogody oraz prognozy na kilka kolejnych dni. Dane pobieramy z zewnętrznego serwisu pogodowego poprzez API. Weather to klasa zawierająca zestaw danych pogodowych.
public class Weather{
//Weather data class
}
public class WeatherService{
private WeatherClient weatherClient = new WeatherClient();
public Weather getWeather(){
return weatherClient.getWeather();
}
public List<Weather> getForecast(){
return weatherClient.getForecast();
}
}
public class WeatherClient{
public Weather getWeather(){
System.out.println("Returning today's weather data");
return new Weather();
}
public List<Weather> getForecast(){
System.out.println("Returning forecast data");
return new List<Weather>;
}
}Jak widzimy na powyższym przykładzie, w klasie WeatherService mamy utworzoną instancje WeatherClient, która zawiera funkcjonalności z pobieraniem danych pogodowych z zewnętrznego API. Mamy tutaj bezpośrednią zależność modułu wyższego poziomu, od modułu poziomu niższego. Stwarza to też kolejny problem w momencie gdy chcielibyśmy rozbudować aplikację o pobieranie danych z innych serwisów pogodowych. W celu pozbycia się błędów, możemy wprowadzić pośredniczący interfejs, który będzie implementowany przez konkretne API. Natomiast w klasie WeatherService, przy pomocy techniki zwanej wstrzykiwaniem zależności, umieścimy wspomniany interfejs w konstruktorze.
public class Weather{
//Weather data class
}
public interface IWeatherClient {
Weather getWeather();
List<Weather> getForecast();
}
public class WeatherService{
private IWeatherClient iWeatherClient;
public WeatherService(IWeatherClient iWeatherClient){
this.iWeatherClient = iWeatherClient;
}
public Weather getWeather(){
return weatherClient.getWeather();
}
public List<Weather> getForecast(){
return weatherClient.getForecast();
}
}
public class WeatherClient implements IWeatherClient{
@Override
public Weather getWeather(){
System.out.println("Returning today's weather data");
return new Weather();
}
@Override
public List<Weather> getForecast(){
System.out.println("Returning forecast data");
return new List<Weather>;
}
}Dzięki takiemu zabiegowi, aplikacja stałą się dużo łatwiejsza w rozbudowie. Uzyskujemy rozdzielenie modułów przez zastosowanie dodatkowego poziomu abstrakcji, co wbrew pozorom tylko ułatwia zrozumienie programu i poprawia jego czytelność. Takie podejście zapewnia również łatwiejsze przenoszenie kodu do innych projektów.
Był to już ostatni z serii artykułów na temat zasad programowania obiektowego SOLID. Mam nadzieję, że w prosty sposób udało mi się przybliżyć Tobie zagadnienia związane z tą praktyką. Jeśli nie zapoznałeś się jeszcze z pozostałymi artykułami, to gorąco do tego zachęcam.