Wstęp
Wywoływanie kodu niskopoziomowego w modułach wysokopoziomowych niesie za sobą ryzyko nieprzewidzianych zmian w kodzie z logiką biznesową, uniemożliwienia testowania jednostkowego i zwiększenia kosztów utrzymania.
Moduł wysokopoziomowy to moduł, który zawiera reguły biznesowe i logikę aplikacji, która bardzo rzadko ulega zmianom (jedynie gdy zmieniane są wymagania). Przykładem może być np. logika zapisywania stworzonego dokumentu. Reguła ta raczej nie jest zmienna, ponieważ nie usuwamy możliwości zapisywania plików w oprogramowaniu a jego przebieg jest prawie zawsze taki sam: naciśnij zapisz -> wybierz miejsce zapisu -> nazwij plik -> zapisz. Zmienić się może lokalizacja przycisku zapisz, nazwa, wygląd, skrót klawiszowy, okno do wyboru miejsca, rozszerzenia, formaty i wiele innych ale sam sposób zapisywania jest niezmienny od wielu lat.
Niskopoziomowe moduły to takie, które zawierają szczegóły implementacji o których klient lub użytkownik nie ma w ogóle pojęcia np. sposób zapisywania plików na poziomie użytej biblioteki. Moduły niskopoziomowe mogą się zmieniać w czasie np. ze względu na zmiany systemowe, zmiany sposobu przechowywania plików, ze względu na wydajność lub nową wersję biblioteki lub platformy. Jako niskopoziomowy kod uważamy wszystkie klasy z zewnętrznych bibliotek, frameworków, sdk na które nie mamy wpływu.
Przykład 1: Zapisywanie dokumentów
Spójrz na poniższy przykład zapisywania dokumentów, który może być częścią większego systemu:

Jak widać na rys. 1.1 w systemie może istnieć wiele różnych formatów plików oraz mogą być zapisywane w różnym formacie. Na powyższym diagramie umieściłem jedynie 2 formaty plików dla zobrazowania omawianego problemu. Spójrzmy na implementację klasy PDFSaver
.
PDFSaver
Klasa PDFSaver
implementuje interfejs DocumentSaver
i jest odpowiedzialna za zapisywanie obiektów typu Document
jako plik PDF na dysku lokalnym. Interfejs DocumentSaver
powoduje, że klasa PDFSaver
spełnia zasadę OCP i możemy w przypadku jakiś zmian stworzyć nową implementację bez konieczności edytowania PDFSaver
. Rozważmy uproszczoną wersję klasy PDFSaver
:
public class PDFSaver implements DocumentSaver { public static final String EXTENSION = ".pdf"; private Document document; private PDFGeneratorFactory pdfGeneratorFactory; private File saveCatalogue; public PDFSaver(Document document, File saveCatalogue, PDFGeneratorFactory pdfGeneratorFactory) { this.document = document; this.pdfGeneratorFactory = pdfGeneratorFactory; this.saveCatalogue = saveCatalogue; } @Override public void saveDocument() throws CannotSaveFileException { byte[] pdfData = generatePdfFormat(); saveFile(pdfData); } private byte[] generatePdfFormat() { Generator generator = pdfGeneratorFactory.getGenerator(document); return generator.generate(); } private void saveFile(byte[] pdfData) throws CannotSaveFileException { File pdfFile = createNewFile(); writeDataToFile(pdfData, pdfFile); } private void writeDataToFile(byte[] pdfData, File pdfFile) throws CannotSaveFileException { try (FileOutputStream fileOutputStream = new FileOutputStream(pdfFile)) { fileOutputStream.write(pdfData); } catch (IOException e) { throw new CannotSaveFileException(); } } private File createNewFile() { String fileName = createFilename(); return new File(saveCatalogue, fileName); } private String createFilename() { return document.getTitle() + EXTENSION; } }
Zauważcie, że powyższa klasa jest zależna od klas FileOutputStream
, File
i PDFGeneratorFactory
.
PDFGeneratorFactory
jest klasą odpowiedzialną za stworzenie obiektu Generator
, który dokonuje konwersji obiektu typu Document
do formatu PDF. PDFGeneratorFactory
jest także interfejsem dlatego można dostarczać różne jego implementacje do PDFSaver
bez konieczności edytowania jej.
FileOutputStream
oraz File
są standardowymi klasami Javy odpowiedzialnymi za operacje na plikach i raczej są klasami niezmiennymi tzn. istnieje małe prawdopodobieństwo, że zostaną usunięte.
Na pierwszy rzut oka klasa wydaje się być napisana dobrze, ponieważ wszystkie zależności są wstrzykiwane do klasy. W razie większych zmian dzięki interfejsowi można stworzyć nową implementację nie naruszając OCP. Nic bardziej mylnego, jest to tylko pozornie dobre rozwiązanie, które w przypadku większej skali może sprawić nam niemałe problemy.
Co jest zatem nie tak z tym kodem? Gdzie jest haczyk?
Wyobraź sobie powyższy przykład ale w większej skali. W systemie jest ok. 100 klas umożliwiających zapisywanie plików w różnych formatach. Pewnego dnia na jakimś spotkaniu projektowym zostaje dodane nowe wymaganie, które mówi, że wszystkie pliki w systemie mają być zapisywane tylko i wyłącznie w chmurze do której jest dostarczone zewnętrzne API.
W momencie gdy system jest zaimplementowany tak jak w powyższym przykładzie to aby wprowadzić zmianę będzie trzeba stworzyć 100 nowych klas zapisujących pliki lub naruszyć zasadę OCP dokonując modyfikacji w istniejących klasach. Gdy zmieni się usługodawca chmury i będzie wymagana ponowna zmiana API to nastąpi znowu konieczność zmiany 100 klas. Jednym słowem każda zmiana sposobu zapisywania plików pociągnie za sobą ok. 100 różnych zmian. Tracimy przy tym mnóstwo pieniędzy, czasu oraz elastyczność na poziomie kodu oraz biznesu.
Oczywiście Uncle Bob rusza z pomocą i serwuje zasadę Dependecy Inversion Principle (DIP) czyli główny temat tego artykułu. A mówi ona, że
- Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. I jedne, i drugie powinny zależeć od abstrakcji.
- Abstrakcje nie powinny zależeć od szczegółów. To szczegóły powinny zależeć od abstrakcji
Powyższy problem jest wynikiem bezpośredniej zależności PDFSaver
od klas niskiego poziomu:

na rys. 1.2 widać, że PDFSaver
z pakietu com.company.savers
jest zależny od niskopoziomowej klasy File
i FileOutputStream
z oddalonego pakietu java.io
. Klasy File
i FileOutputStream
„niestety” nie są abstrakcją, dzięki której można byłoby żonglować implementacjami.
Standardowo do rozwiązania takiego problemu używa się wzorca Fasada, który ukrywa implementację zapisywania plików udostępniając jedynie metody wyższego poziomu. Rozwiązałoby to nasz problem ale PDFSaver
nadal nie spełniałoby DIP. Dlaczego? Fasada byłaby tworzona bazując na implementacji zapisywania plików za pomocą File
oraz FileOutputStream
i istniałby ryzyko, że zaprojektowana Fasada nie pasowałaby do funkcjonalności zapisywania danych w chmurze. Jak już wspomniałem w artykule o SRP (Single Responsibility Principle) kluczem do sukcesu nie są same wzorce projektowe ale ich odpowiednie użycie.
Nie pójdę tym tokiem myślenia, że abstrakcję tworzy się pod kątem kodu, który ma pod tą abstrakcją się kryć. Abstrakcje nie powinny zależeć od szczegółów. To szczegóły powinny zależeć od abstrakcji!
Zasada odwracania zależności przyznaje pierwszeństwo modułom wyższego poziomu nad modułami niższego poziomu. Z tego wynika, że abstrakcje powinny zależeć od modułów wysokopoziomowych i to na ich bazie powinny być tworzone. Zasada ta nazywa się zasadą odwracania zależności, ponieważ odwracamy zależność modułu wysokopoziomowego od modułu niskopoziomowego na zależność modułu niskopoziomowego od abstrakcji wysokopoziomowej.
Żeby stworzyć wysokopoziomową abstrakcję, trzeba odpowiedzieć na pytanie: Jakie potrzeby ma klasa PDFSaver
względem nowej abstakcji?
Zapisywanie pliku odbywa się tuż po konwersji obiektu typu Document
do tablicy bajtów, która reprezentuje plik. Następnie tablica bajtów zapisywana jest do pliku z pomocą klasy File
oraz FileOutputStream
. W przypadku zmiany docelowego miejsca zapisania plików nadal będziemy operować na bajtowej reprezentacji pliku.
Nowy interfejs będzie miał metodę do zapisania tablicy bajtów. Dodatkowo wraz z tablicą bajtów dostarczymy obiekt typu Metadata
, który przechowuje metadane pliku takie jak rozszerzenie, nazwę pliku, autora itp. oraz obiekt typu Destination
, który reprezentuje docelowe miejsce zapisu (może to być katalog, tabela w bazie danych lub metoda API)
Budując interfejs FileSaver
w oparciu o klasę, która będzie się nim posługiwała pozwala na uzależnienie szczegółów od wysokopoziomowego interfejsu. Tworząc zależność w ten sposób wymuszamy aby zewnętrzne biblioteki oraz kod niższego poziomu dostosował się do modułów wysokopoziomowych a nie nasze moduły wysokopoziomowe do obcych bibliotek.

Utworzenie interfejsu FileSaver
pozwoliło ukryć pod nim niskopoziomową implementację LocalDriveFileSaver
i uniezależnić klasę PDFSaver
od niskopoziomowych operacji. PDFSaver
jest zależny od interfejsu FileSaver
a nie od obcych klas File
i FileOutputSteam
Takie rozwiązanie pozwala na zmianę sposobu zapisywania plików w całym systemie poprzez dodanie nowej implementacji FileSaver
.
Warstwy kodu
Aby stosowanie DIP nie nastręczało wielu problemów (w szczególności w zespołach wieloosobowych) powinniście jawnie określić strukturę warstw abstrakcji w projekcie. Opis warstw oraz cechy przynależnych do nich klas powinny znajdować się w dokumentacji lub ogólnodostępnym dokumencie aby każdy z członków zespołu mógł się z nim zapoznać i w razie wątpliwości czy sporów stanowiłby on źródło cennej wiedzy.
W przeważającej większości projektów wystarczy aby system składał się z 3 warstw abstrakcji:
- Warstwa Logiki Biznesowej / Domenowa (Warstwa wysokiego poziomu) – najwyższa warstwa abstrakcji w projekcie zawierająca logikę biznesową systemu. Nie znajdziecie tutaj niskopoziomowego kodu związanego z konwersjami, zapisywaniem plików czy budowaniem ciągów znaków. Implementacje metod na tym poziomie abstrakcji najczęściej będą bardzo krótkie i będą zawierały wywołania metod z interfejsów, które komunikują się z niższymi warstwami. Kod w tej warstwie będzie najrzadziej podlegał modyfikacjom. Jedynym powodem do zmiany tych klas będzie zmiana wymagań funkcjonalnych. Nie chcemy, żeby najważniejsza część systemu czyli sposób działania aplikacji był podatny na jakiekolwiek zmiany związane ze zmianą wersji API lub biblioteki. Warstwa ta nie powinna zależeć od platformy i zewnętrznych bibliotek.
- Warstwa Mechanizmu (Warstwa średniego poziomu) – warstwa ta zawiera mechanizmy aplikacji czyli algorytmy związane ze sposobem działania aplikacji. Znajduje się tutaj kod odpowiedzialny za pobieranie danych ze źródeł (także wykorzystując interfejsy) oraz obsługę błędów wysokiego poziomu. Jest to warstwa która pośredniczy warstwie Logiki Biznesowej z warstwą Bibliotek.
- Warstwa Bibliotek (Warstwa niskiego poziomu) – tutaj znajdują się algorytmy najniższego poziomu, tzn wszystkie rodzaju szyfrowania, konwersje, parsowanie, pobieranie danych, odwołania do API, operacje na klasach zewnętrznych bibliotek, obsługa błędów HTTP. Może to Was zdziwić ale tutaj znajdują się także wszystkie operacje na elementach bezpośrednio związanych z UI. Czemu? Ponieważ zależą one od platformy oraz od wersji platformy, a przecież nie możemy uzależniać wyższych warstw od elementów które są zmienne w czasie (w szczególności od elementów UI które zmieniają się prawie co roku w przypadku Androida).
Jak zidentyfikować zależności w modułach wysokopoziomowych, które powinno się odwrócić?
- Jeśli w konstruktorze jakiś parametr nie jest interfejsem oznacza to, że klasa zależy od implementacji a nie od abstrakcji.
- Jeśli w klasie znajduje się słowo kluczowe
new
możemy mieć prawie całkowitą pewność, że nasza klasa nie spełnia DIP. Wyjątek stanowią klasy trwałe takie jakString
,Integer
,StringBuilder
etc. oraz klasy prywatne, które służą tylko i wyłącznie do zwiększenia czytelności implementacji. Trzeba jednak pamiętać, że wszystko zależy od kontekstu. Np. przechowywanie sformatowanego tekstu w klasieString
lubSpannedString
w bardziej zaawansowanych edytorach tekstu może być niewystarczające i w późniejszej fazie projektu będzie konieczność podmiany dużej części implementacji. - Jeśli w liście importu pakietów znajdują się pakiety z zewnętrznych bibliotek lub pakiety SDK np.
android.view
- Jeśli w napisaniu testu do klasy przeszkodą jest któraś z zależności
- Wyjątek stanowią oczywiście biblioteki będące rozszerzeniami języka lub implementacją określonych wzorców projektowych, takim przykładem może być RxJava lub Retrolambda – chociaż tutaj też zalecałbym rozsądek, nadmierne używanie jakiejkolwiek biblioteki może doprowadzić do sytuacji w, której aktualizacja bez kompatybilności wstecznej spowoduje pojawienie się ogromnych kosztów. Tak jak miało to miejsce w aktualizacji RxJava 1 na RxJava 2.
Przykład 2: Tabela ASCI z poprzedniego artykułu
DIP w UserTableScreenWriter
Zastosuję powyższą zasadę w kodzie z poprzednich 2 artykułów o SRP i OCP. Cały kod źródłowy przykładu sprzed refaktoryzacji znajdziesz tutaj:
https://github.com/androidCoder-pl/SOLID/tree/OCP
a tutaj kod po refaktoryzacji:
https://github.com/androidCoder-pl/SOLID
Kod ma już dobrze zorganizowaną strukturę, jest elastyczny i posiada testy do większości klas. Klasa UsersTableScreenWriter
nie ma testów i nie spełnia 5 zasady SOLID, ponieważ w 18 linii wypisujemy dane na ekran przy pomocy niskopoziomowego obiektu PrintStream
pobieranego za pomocą zmiennej System.out
:
public class UsersTableScreenWriter implements UsersWriter { private UsersToStringConverter stringConverter; public UsersTableScreenWriter(UsersToStringConverter userTable) { this.stringConverter = userTable; } @Override public void writeData(List<User> usersData) { System.out.println(stringConverter.toString(usersData)); } }
Powyższa klasa ściśle zależy od niskopoziomowej klasy systemowej, gdyby trzeba było wypisać dane na ekranie podłączonym poprzez port COM lub USB musielibyśmy utworzyć nową klasę. Możemy tego uniknąć poprzez stworzenie abstrakcji AlphanumericScreen
przeznaczonej do wyświetlania danych w formacie alfanumerycznym na ekranie.
public interface AlphanumericScreen { void print(String text); }
Następnie należy stworzyć implementację, która będzie wypisywała na ekranie znaki alfanumeryczne. Oczywiście w stworzonej klasie nie użyjemy bezpośrednio metody System.out.println
, tylko wstrzykniemy tę zależność w konstruktorze, tak aby klasa była testowalna i bardziej uniwersalna.
public class DefaultScreen implements AlphanumericScreen { private PrintStream printStream; public DefaultScreen(PrintStream printStream) { this.printStream = printStream; } @Override public void print(String text) { printStream.println(text); } }
public class DefaultScreenTest { private PrintStream printStream; private DefaultScreen defaultScreen; private ByteArrayOutputStream outputStream; @Before public void setUp() { outputStream = new ByteArrayOutputStream(); printStream = new PrintStream(outputStream); defaultScreen = new DefaultScreen(printStream); } @Test public void shouldPrintAndMoveToNextLine() { String something = "Some text to print".trim(); defaultScreen.print(something); String result = new String(outputStream.toByteArray(), StandardCharsets.UTF_8); assertEquals("\n", result.substring(result.length() - 1)); } }
Zabieg ten sprawił, ze klasa UsersTableScreenWriter
uniezależniła się od komponentów niskopoziomowych i spełnia zasadę DIP. Stworzenie interfejsu AlphanumericScreen
sprawiło, że można teraz wyświetlić tabelę na dowolnym monitorze o ile dostarczymy implementację interfejsu. Klasę można już bez przeszkód przetestować tworząc mock dla AlphanumericScreen
.
public class UsersTableScreenWriter implements UsersWriter { private UsersToStringConverter stringConverter; private AlphanumericScreen alphanumericScreen; public UsersTableScreenWriter( UsersToStringConverter userTable, AlphanumericScreen alphanumericScreen) { this.stringConverter = userTable; this.alphanumericScreen = alphanumericScreen; } @Override public void writeData(List<User> usersData) { alphanumericScreen.print(stringConverter.toString(usersData)); } }
public class UsersTableScreenWriter implements UsersWriter { private UsersToStringConverter stringConverter; public UsersTableScreenWriter(UsersToStringConverter userTable) { this.stringConverter = userTable; } @Override public void writeData(List<User> usersData) { System.out.println(stringConverter.toString(usersData)); } }
public class UsersTableScreenWriterTest { private String mockTable = "[TABLE]"; private UsersToStringConverter stringConverter = new MockUsersToStringConverter(listOfUsers(), mockTable); private SpyAlphanumericScreen alphanumericScreen = new SpyAlphanumericScreen(); private UsersTableScreenWriter screenWriter; @Before public void setUp() { screenWriter = new UsersTableScreenWriter(stringConverter, alphanumericScreen); } @Test public void shouldWriteUsersDataOnAlphanumericScreen() throws Exception { screenWriter.writeData(listOfUsers()); alphanumericScreen.verifyPrinted(mockTable); } private List<User> listOfUsers() { List<User> users = new ArrayList<>(); users.add(new User("Avery", "Horn", "a123")); users.add(new User("Henry", "Black", "a125")); return users; } }
DIP w JsonFileUsersData
public class JsonFileUsersData implements UsersData { private List<User> users; public JsonFileUsersData(File file, Gson gson) { try (FileReader fileReader = new FileReader(file)) { convertJsonToUserList(gson, fileReader); } catch (IOException e) { makeListEmpty(); } } private void convertJsonToUserList(Gson gson, FileReader fileReader) { User[] usersList = gson.fromJson(fileReader, User[].class); users = Arrays.asList(usersList); } private void makeListEmpty() { users = Collections.emptyList(); } @Override public List<User> getUsers() { return users; } }
W kontekście DIP klasa JsonFileUsersData
jest brzydką implementacją, ponieważ zależy od dwóch niskopoziomowych klas – jedną jest klasa File
a drugą Gson
.
Jakie korzyści przyniesie zastosowanie DIP?
W przypadku chęci korzystania z zaszyfrowanych plików, skompresowanych lub umieszczonych w chmurze nie trzeba byłoby tworzyć nowej implementacji JsonFileUsersData
a jedynie nową implementację JsonData
, która może dostarczać w dowolny sposób dane w formacie JSON. Podmiana biblioteki do konwersji z formatu na JSON też byłaby banalnie prosta poprzez napisanie nowej implementacji JsonConverter
.
Stwórzmy zatem interfejs JsonData
, który będzie dostarczał JSONa:
public interface JsonData { String getJsonString(); }
Oraz naiwną (bez obsługi błędów) implementację klasy implementującej JsonData
:
public class JsonFile implements JsonData { private File file; public JsonFile(File file) { this.file = file; } @Override public String getJsonString() { try { byte[] jsonBytes = Files.readAllBytes(file.toPath()); return new String(jsonBytes, StandardCharsets.UTF_8); } catch (IOException e) { return ""; } } }
public class JsonFileTest { private String jsonString = "{\"data\":\"json\"}"; private File file; private JsonFile jsonFile; @Before public void setUp() throws Exception { createTestFile(); } @After public void tearDown() { deleteTestFile(); } private void deleteTestFile() { file.deleteOnExit(); } private void createTestFile() throws IOException { new File("build/tmp/testFiles").mkdir(); file = new File("build/tmp/users.json"); Writer writer = new FileWriter(file); writer.write(jsonString); writer.close(); } @Test public void shouldReturnEmptyStringWhenIOExceptionccuOrsDuringOpenFile(){ jsonFile = new JsonFile(new File("not/known/path/users.json")); String result = jsonFile.getJsonString(); assertEquals("", result); } @Test public void shouldReturnJsonStringWhenFileIsNotEmpty() throws Exception { jsonFile = new JsonFile(file); String results = jsonFile.getJsonString(); assertThat(results, is(jsonString)); } }
Następnie stwórzmy interfejs JsonConverter
oraz jego implementację GsonConverter
:
public interface JsonConverter { <T> T fromJson(JsonData jsonData, Class<T> classOf); }
public class GsonConverter implements JsonConverter { private Gson gson; public GsonConverter(Gson gson) { this.gson = gson; } @Override public <T> T fromJson(JsonData jsonData, Class<T> classOf) { return gson.fromJson(jsonData.getJsonString(), classOf); } }
public class GsonConverterTest { private Gson gson = new Gson(); private StubUserJsonData jsonData = new StubUserJsonData(); private GsonConverter gsonConverter; @Before public void setUp() { gsonConverter = new GsonConverter(gson); } @Test public void gsonShouldParseJsonFromJsonDataObject() { User actualUser = gsonConverter.fromJson(jsonData, User.class); assertThat(actualUser, is(jsonData.getUser())); } }
Na końcu dodajemy wstrzykiwanie zależności w JsonFileUsersData
nowo utworzonych abstrakcji:
public class JsonFileUsersData implements UsersData { private List<User> users; private JsonConverter jsonConverter; private JsonData jsonData; public JsonFileUsersData(JsonData jsonData, JsonConverter jsonConverter) { this.jsonConverter = jsonConverter; this.jsonData = jsonData; convertJsonToUserList(); } private void convertJsonToUserList() { User[] usersList = jsonConverter.fromJson(jsonData, User[].class); users = usersList != null ? Arrays.asList(usersList) : Collections.emptyList(); } @Override public List<User> getUsers() { return users; } }
public class JsonFileUsersData implements UsersData { private List<User> users; public JsonFileUsersData(File file, Gson gson) { try (FileReader fileReader = new FileReader(file)) { convertJsonToUserList(gson, fileReader); } catch (IOException e) { makeListEmpty(); } } private void convertJsonToUserList(Gson gson, FileReader fileReader) { User[] usersList = gson.fromJson(fileReader, User[].class); users = Arrays.asList(usersList); } private void makeListEmpty() { users = Collections.emptyList(); } @Override public List<User> getUsers() { return users; } }
public class JsonFileUsersDataTest { private StubUsersJsonData jsonData = new StubUsersJsonData(); private JsonConverter jsonConverter; @Test public void shouldReturnEmptyListWhenJsonConverterReturnNull() { jsonConverter = new StubNullJsonConverter(); JsonFileUsersData jsonFileUsersData = new JsonFileUsersData(jsonData, jsonConverter); List<User> users = jsonFileUsersData.getUsers(); assertNotNull(users); assertTrue(users.isEmpty()); } @Test public void shouldReturnListWhenJsonConverterReturnObjects() { jsonConverter = new GsonConverter(new Gson()); JsonFileUsersData jsonFileUsersData = new JsonFileUsersData(jsonData, jsonConverter); List<User> users = jsonFileUsersData.getUsers(); assertThat(users, is(jsonData.getUsers())); } }
Niewątpliwą korzyścią zastosowania DIP w powyższym przykładzie jest zwiększenie czytelności samej klasy JsonFileUsersData
i uproszczenie testów.
Podsumowanie
Stosując SRP (Single Responsibility Principle) i OCP (Open/Close Principle) kod staje się bardziej czytelny i logiczny, stosując DIP (Dependency Inversion Principle) w kodzie powstały nowe abstrakcyjne klasy, które zmniejszają prawdopodobieństwo wystąpienia kaskadowych zmian w kodzie. Aktualizacja bibliotek, zmiany API czy SDK nie wpłyną na główną logikę systemu a jedynie na implementacje interfejsów, które są małe.
Gdy sukcesywnie i mądrze zastosujecie DIP w projekcie minimalizujecie ryzyko wystąpienia błędów podczas aktualizowania bibliotek, ulepszania wydajności czy zmiany interfejsu graficznego aplikacji. Zobaczycie też, że wiele problemów po prostu znika a utrzymanie projektu staje się o wiele przyjemniejsze.
Tworząc interfejs do modułu wysokopoziomowego należy upewnić się czy na pewno jest on stworzony z myślą o klasie korzystającej z niego a nie na bazie implementacji, która ma pod tym interfejsem się kryć. Złe zaplanowanie interfejsu da nam mierne korzyści i ryzyko kaskadowych zmian tak jakby w ogóle interfejsu nie było.
Bibliografia i polecane źródła
[1] https://keyholesoftware.com/2014/02/17/dependency-injection-options-for-java/
[2] https://www.youtube.com/watch?v=gwIS9cZlrhk&t=611s
[3] http://blog.cleancoder.com/
[4] Zwinne wytwarzanie oprogramowania. Najlepsze zasady, wzorce i praktyki, Robert C. Martin, Helion 2015
[5] Czysty kod. Podręcznik dobrego programisty, Robert C. Martin, Helion 2014
[6] Refactoring: Improving the Design of Existing Code, Martin Fowler
[7] https://www.youtube.com/watch?v=t86v3N4OshQ
One thought on “SOLID cz.3 – DIP Dependency Inversion Principle”