Interfejs jako typ zmiennej

0

Dlaczego dobrą praktyką jest takie tworzenie listy:

List<String> lista = new ArrayList<>();

zamiast

ArrayList<String> lista = new ArrayList<>();?

Czy jest coś poza tym, że daje nam to elastyczność przy zmianie imlementacji z ArrayList na np. LinkedList? Co jeszcze nam daje typ interfejsowy?

5

daje nam to elastyczność przy zmianie imlementacji

Co więcej, faktyczna implementacja moze nawet jeszcze nie istnieć kiedy piszesz taki kod! Bo twój kod polega tylko na tym że jest tam jakaś lista. Zrobisz np. funkcje sortującą dowolną listę i potem ktoś jutro napisze swoją własna implementacje listy i może ją sortować twoją funkcją!

3

Przykład. Załóżmy, że chcesz liczyć pseudo-średnią arytmetyczną grupy (nie wiesz jeszcze grupy czego), ale niech to będzie grupa czegoś, co zwraca swoje osiągnięcie/rezultat.

/Nie mockuje się czegoś co ma liczyć średnią (5+7)/2 ale chciałem na szybko jakiś nieskomplikowany przykład/

To coś można opisać interfejsem Scorable (internety podają: scorable, a nie scoreable)

@FunctionalInterface
public interface Scorable {

    int getScore();
}

Jeszcze nie wiadomo co to konkretnie będzie. :)

Wiadomo, że na wynikach będzie się robić zestawienia, np. pseudoŚrednia wszystkich w ekipie "Scorables"

public class TeamScores {

    private final List<Scorable> scores = new ArrayList<>();

    public void add(Scorable scorable) {

        if (scorable == null) {
            throw new IllegalArgumentException();
        }

        scores.add(scorable);
    }

    public int naiveArithmeticMean() {
//
    }
}

Żeby sprawdzić, czy dobrze się policzy, to
albo robimy implementację interfejsu Scorable i sprawdzamy
albo nie robimy implementacji i symulujemy zachowanie kiedyś w przyszłosci dodanej implementacji (na razie jej nie mamy), są tylko testy

coś większego, Mockito - rozwlekle

    @Test
    public void fiveAndSevenShouldGiveSix() {

        final Scorable scorable5 = Mockito.mock(Scorable.class);
        Mockito
                .when(scorable5.getScore())
                .thenReturn(5);

        final Scorable scorable7 = Mockito.mock(Scorable.class);
        Mockito
                .when(scorable7.getScore())
                .thenReturn(7);

        teamScores.add(scorable5);
        teamScores.add(scorable7);

        final int mean = teamScores.naiveArithmeticMean();

        assertThat(mean, is(6));
    }

albo po prostu zamiast Mockito zwykły javowy Supplier - zwięźle

    @Test
    public void fiveTenFifteenShouldGiveTen() {

        teamScores.add(() -> 5);
        teamScores.add(() -> 10);
        teamScores.add(() -> 15);

        assertThat(teamScores.naiveArithmeticMean(), is(10));
    }

Jest interfejs jako argument, więc można tymczasowo całą implementację i instancję zastąpić w testach przez krótkie

() -> 5

Co będzie póżniej implementować Scorable to nie ważne, liczy się, że teraz tylko metoda getScore(), którą zdefiniował interfejs Scorable

3

Moja (kontrowersyjna?) opinia: W przypadku zmiennych lokalnych nie ma to w praktyce najmniejszego znaczenia. Co więcej Java 10 wprowadza słowo kluczowe var, domyślam się że w przeciągu następnych kilku lat wszyscy Ci którzy teraz krzyczą żeby używać List<T> zamiast ArrayList<T> będą w niebiosa wychwalać użycie var (które oczywiście ustali typ zmiennej lokalnej jako ArrayList<T>).

Gdzie ma to znaczenie: W parametrach przyjmowanych przez metody/konstruktory oraz w wartościach z nich zwracanych. Niestety są tutaj różne szkoły, często słyszy się że w wewnętrznym (a więc nie publikowanym na zewnątrz z punku widzenia danego modułu) kodzie parametry powinny być jak najbardziej ogólne (a wiec List<T>) a wartości zwracana jak najbardziej szczególne ArrayList<T>. Argumentując to np. tym że w "środku" modułu warto wiedzieć np. czy lista to LinkedList czy ArrayList ze względu na wydajność. [EDIT: Schodząc jeszcze niżej na poziom pojedynczej klasy i prywatnych metod ma to pewien sens].

Inni twierdzą że powyższe to bzdura i wartości zwracane z metod również powinny być jak najogólniejszego typu, co ma na celu ułatwić późniejszą zmianę implementacji metody bez wpływu na jej użytkowników. Sam bardziej skłaniam się ku temu podejściu. [EDIT: Znów można to zaaplikować również do publicznych metod na pojedynczej klasie].

Jedno jest pewne, w części publicznej modułu (API) najeży raczej zwracać interfejsy i klasy bazowy (a wiec typu ogólne). Ułatwi to w przyszłości ewolucję API, bez zmiany kodu po stronie klientów.

Powyższe dotyczy również wyjątków rzucanych przez metody i powoli wchodzi w literkę L z SOLID.

PS. Jedyna zaleta List and ArrayList z przykładu jest taka że jest krótsze. Wiec w moim kodzie przed Java 10 prawie zawsze tak pisałem zeby zaoszczędzić stukania w klawiaturę. W Scali solę wszędzie val.

3

@0xmarcin:

Jak już bardzo szczegółowo brać wydajność i wąską specyfikację wartości zwracanych, to się spytałbym, ile z metod korzystających ze szczegółowo dobranych implementacji przeszło np. Java Microbenchmark Harness i wybrano świadomie, po dłuższych testach wydajnościowych?

Bo się okaże, że wydajność może 30% w górę ale kod w takim miejscu, że nie ma to znaczenia dla reszty.

Jak z pewnym Leadem twierdzącym, że np. split zawsze na Pattern bo jest szybszy.
Przetestowałem to kiedyś, Lead miał oczywiście rację.

        String fox = "The quick brown fox jumps over the lazy dog";

        final Pattern pattern = Pattern.compile("\\s");
        final String[] fastWords = pattern.split(fox);

        final String[] lazyWords = fox.split("\\s");

Dla mnie lazyWords jest oswojone, krótkie, czytelne i w 99% przypadków nie będzie wąskim gardłem.
A Mr. Lead prowadzi krucjatę pod wezwaniem świętej wydajności.

Nie wiem, czy proporcje zasady Pareto nie należy nawet podciągnąć, nie 20% kodu odpowiada za 80% problemów wydajności ale znacznie mniej.
A z "czytelnością sprytnie zoptymalizowanego dla samej optymalizacji" kodu męka na co dzień.

1

Moja (kontrowersyjna?) opinia: W przypadku zmiennych lokalnych nie ma to w praktyce najmniejszego znaczenia.

No w przypadku zmiennych lokalnych nie ma dużego znaczenia, a jak metoda zwraca obiekt utworzony lokalnie to typ jest ograniczony przez sygnaturę metody, więc

public Collection <String> variables() {
 var foo  = new ArrayList<>();
 foo.add("Bar");
 return foo;
}

nie różni się specjalnie z pragmatycznego punktu widzenia od

public Collection <String> variables() {
 Collection<String> foo  = new ArrayList<>();
 foo.add("Bar");
 return foo;
}
1

Jest taka znana Java Championka, Trisha Gee
https://twitter.com/trisha_gee
https://trishagee.com/about-me/

Można wrzucić jej takie tematy.
Ale ostrzegałem, kobieta w tematach kodu radykalna i stanowcza jak Antifa+LGBT+BLM w polityce.

1 użytkowników online, w tym zalogowanych: 0, gości: 1