środa, 26 marca 2014

SOLIDny kod cz. 6 - Dependency inversion principle

poodwracane?

Zacznijmy od definicji:
Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Jedne i drugie powinny być zależne od pewnych abstrakcji.
Niby proste, ale żeby usunąć wszelkie niejasności to pozwolę sobie na rozwinięcie.

Definicja mówi nam o tym, że główne mechanizmy (moduły wysokiego poziomu) nie powinny być zależne od żadnych elementów, które nie były istotne przy ich projektowaniu (mowa tu o modułach niskiego poziomu, częściach składowych). Powiązania pomiędzy nimi powinny być realizowane za pomocą abstrakcji (interfejsów), dzięki czemu kod jest łatwiejszy przy rozwijaniu oraz modyfikacji, a ewentualne re-użycie mechanizmów nadrzędnych nie pociąga za sobą konieczności kopiowania elementów, które nie są nam do szczęścia potrzebne.

niech przykład przemówi

Mamy do zaimplementowania mechanizm do generowania raportów. Dane są przechowywane w bazie, ewentualne kryteria dotyczące raportu są przesyłane w żądaniu HTTP, a sam raport ma być wygenerowany do formatu PDF.

Klasa tworząca taki raport mogłaby wyglądać tak (a przynajmniej jej część :):
package com.smalaca.dependencyinversion.report;

import com.smalaca.dependencyinversion.http.Request;
import com.smalaca.dependencyinversion.pdf.Writer;
import com.smalaca.dependencyinversion.sql.Query;

public class ReportCreator {

    private Writer writer;
    private Query query;
    private Request request;
 
    public ReportCreator(Writer writer, Query query) {
        this.writer = writer;
        this.query = query;
    }
 
    public void withCriteria(Request request) {
        this.request = request;
    }

    // some code
}

Na pierwszy rzut oka wszystko jest w należytym porządku - odpowiedzialności klas są odpowiednio podzielone, kod jest otwarty na rozszerzenia (warto nadmienić, że Writer, Query oraz Request są interfejsami), możemy założyć, że interfejsy są minimalne i wystarczające. Czyli, że co? Nie ma się do czego przyczepić?

zmiany przychodzą z pomocą

Jak zwykle, problemy zaczynają się w chwili gdy trzeba coś zmienić :)
Załóżmy, że chcemy teraz dodać możliwość generowania raportów do plików w formacie csv. Niby nic trudnego, bo wystarczy napisać coś takiego:
package com.smalaca.dependencyinversion.csv;

public class Writer implements com.smalaca.dependencyinversion.pdf.Writer {
    // some code
}

I zwróćcie uwagę co tutaj się stało - klasa należąca do pakietu dotyczącego generowania dokumentów csv jest zależna od interfejsu z pakietu pdf. Dobra, już widać, że coś jest nie tak, ale nie na takie rzeczy programiści przymykali oczy.
Co jednak w przypadku, gdy tworzę kolejną aplikację i docieram do momentu, gdy napotykam podobne wymaganie z tym, że teraz dane będą wyciągane z plików XML, dokument ma być w formacie xls, a kryteria mogłyby być przechowywane w jakiejś prostej klasie, ponieważ jest to aplikacja desktopowa?
Jesteśmy zmuszeni do wykorzystania czterech pakietów chociaż zależy nam tak naprawdę na mechanizmie, który w pełni znajduje się w pakiecie report.

a rozwiązanie jest proste

Zapewne rozwiązanie nie będzie dla Was jakąkolwiek niespodzianką.
Jedyną rzeczą, którą należy zrobić, to przenieść (i w tym przypadku odrobinę zmienić ch nazwy) interfejsy do tego samego pakietu, co klasa, która z nich korzysta:
package com.smalaca.dependencyinversion.report;

public class ReportCreator {

    private Writer writer;
    private DataRetriever dataRetriever;
    private Criteria criteria;
 
    public ReportCreator(Writer writer, DataRetriever dataRetriever) {
        this.writer = writer;
        this.dataRetriever = dataRetriever;
    }
 
    public void withCriteria(Criteria criteria) {
        this.criteria = criteria;
    }
}
I to naprawdę tyle. Wszystkie zalety początkowego rozwiązania nadal są aktualne. Odpada nam jednak jedna poważna wada - powiązanie z pakietami, z którymi powiązanie jest zbędne, czyli odwróciliśmy zależności - teraz to nie ReportCreator jest zależny od interfejsów rozsianych po różnych pakietach, a klasy, które chcą korzystać z tego mechanizmu są zależne od elementów pakietu, w którym się on (mechanizm) znajduje.

ech, ten przykład

Na sam koniec chciałem przeprosić za przerysowany przykład. Wierzę, że większość z Was od razu umieściłaby interfejsy w odpowiednim pakiecie, jednak chciałem pokazać, że zasady SOLID nie dotyczą jedynie pojedynczych klas, lecz można je z powodzeniem wykorzystywać przy większych częściach aplikacji.