Czy w Javie 8 stylistycznie lepiej jest używać wyrażeń referencyjnych metod lub metod zwracających implementację interfejsu funkcjonalnego?


11

Java 8 dodała koncepcję funkcjonalnych interfejsów , a także wiele nowych metod, które zostały zaprojektowane do przyjmowania funkcjonalnych interfejsów. Wystąpienia tych interfejsów można zwięźle utworzyć za pomocą wyrażeń referencyjnych metod (np. SomeClass::someMethod) I wyrażeń lambda (np (x, y) -> x + y.).

Wspólnie z koleżanką mamy różne opinie na temat tego, kiedy najlepiej użyć takiej lub innej formy (gdzie „najlepsze” w tym przypadku naprawdę sprowadza się do „najbardziej czytelnych” i „najbardziej zgodnych ze standardowymi praktykami”, ponieważ zasadniczo są inaczej odpowiednik). W szczególności dotyczy to przypadku, gdy spełnione są następujące warunki:

  • funkcja, o której mowa, nie jest używana poza jednym zakresem
  • nadanie instancji nazwy poprawia czytelność (w przeciwieństwie do np. logiki, która jest wystarczająco prosta, aby zobaczyć, co się dzieje na pierwszy rzut oka)
  • nie ma innych powodów programowania, dla których jedna forma byłaby lepsza od drugiej.

Moja obecna opinia w tej sprawie jest taka, że ​​dodanie metody prywatnej i odwołanie się do niej poprzez odniesienie do metody jest lepszym podejściem. Wygląda na to, że właśnie w ten sposób zaprojektowano tę funkcję i łatwiej jest komunikować o tym, co się dzieje za pomocą nazw metod i podpisów (np. „Boolean isResultInFuture (wynik wyniku)” wyraźnie mówi, że zwraca wartość logiczną). Powoduje to również, że metoda prywatna jest bardziej przydatna, jeśli przyszłe rozszerzenie klasy chce skorzystać z tej samej kontroli, ale nie potrzebuje funkcjonalnego opakowania interfejsu.

Mój kolega preferuje metodę, która zwraca instancję interfejsu (np. „Predicate resultInFuture ()”). Dla mnie wydaje się, że nie jest to dokładnie tak, jak ta funkcja była przeznaczona do użycia, wydaje się nieco dziwniejsza i wydaje się, że trudniej jest naprawdę komunikować zamiar poprzez nazewnictwo.

Aby uczynić ten przykład konkretnym, oto ten sam kod napisany w różnych stylach:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

Czy istnieje jakaś oficjalna lub półoficjalna dokumentacja lub komentarze dotyczące tego, czy jedno podejście jest bardziej preferowane, bardziej zgodne z intencjami projektantów języków, czy bardziej czytelne? Czy poza oficjalnym źródłem istnieją wyraźne powody, dla których lepiej byłoby podejść?


1
Lambdas zostały dodane do języka z jakiegoś powodu i nie miało to pogorszyć Javy.

@Snowman To oczywiste. Dlatego zakładam, że istnieje pewna domniemana zależność między twoim komentarzem a moim pytaniem, której nie do końca rozumiem. Jaki jest cel, który starasz się osiągnąć?
M. Justin

2
Zakładam, że chodzi mu o to, że gdyby lambdas nie poprawiły status quo, nie zawracałyby sobie głowy ich wprowadzeniem.
Robert Harvey

2
@RobertHarvey Sure. Do tego momentu jednak dodano zarówno lambdas, jak i referencje metod, więc nie było wcześniej status quo. Nawet bez tego szczególnego przypadku są chwile, w których odwołania do metod byłyby jeszcze lepsze; na przykład istniejąca metoda publiczna (np Object::toString.). Moje pytanie dotyczy więc bardziej tego, czy lepiej jest w tym konkretnym typie instancji, który tu przedstawiam, niż czy istnieją sytuacje, w których jedna jest lepsza od drugiej, lub odwrotnie.
M. Justin,

Odpowiedzi:


8

Jeśli chodzi o programowanie funkcjonalne, to, co rozmawiasz z kolegą, to styl bez punktów , a ściślej eta . Rozważ następujące dwa zadania:

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

Są one operacyjnie równoważne, a pierwszy nazywa się stylem punktowym , a drugi stylem pozbawionym punktów . Termin „punkt” odnosi się do nazwanego argumentu funkcji (wywodzącego się z topologii), którego brakuje w tym drugim przypadku.

Mówiąc bardziej ogólnie, dla wszystkich funkcji F owijająca lambda, która przyjmuje wszystkie podane argumenty i przekazuje je do F bez zmian, a następnie zwraca wynik F bez zmian, jest identyczna z samą F. Usunięcie lambda i bezpośrednie użycie F (przejście ze stylu punktowego do stylu wolnego od punktów powyżej) nazywa się redukcją eta , terminem wynikającym z rachunku lambda .

Istnieją wyrażenia bez punktów, które nie są tworzone przez redukcję eta, najbardziej klasycznym przykładem jest składanie funkcji . Biorąc pod uwagę funkcję od typu A do typu B oraz funkcję od typu B do typu C, możemy połączyć je razem w funkcję od typu A do typu C:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

Załóżmy teraz, że mamy metodę Result deriveResult(Foo foo). Ponieważ predykat jest funkcją, możemy zbudować nowy predykat, który najpierw wywołuje, deriveResulta następnie testuje uzyskany wynik:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

Choć realizacja od composezastosowań pointful stylu z lambdas The korzystanie z Compose do zdefiniowania isFoosResultInFuturejest punkt wolny, ponieważ żadne argumenty muszą być wymienione.

Programowanie bez punktowe jest również nazywane programowaniem ukrytym i może być potężnym narzędziem do zwiększania czytelności poprzez usuwanie bezcelowych (zamierzonych słów) szczegółów z definicji funkcji. Java 8 nie obsługuje programowania bez punktów prawie tak dokładnie, jak bardziej funkcjonalne języki, takie jak Haskell, ale dobrą zasadą jest zawsze przeprowadzanie redukcji eta. Nie ma potrzeby używania lambd, które nie zachowują się inaczej niż funkcje, które zawijają.


1
Myślę, że to, o czym rozmawiamy z kolegą, to bardziej te dwa zadania: Predicate<Result> pred = this::isResultInFuture;i Predicate<Result> pred = resultInFuture();gdzie returnInFuture () zwraca a Predicate<Result>. Chociaż jest to świetne wyjaśnienie, kiedy należy użyć odwołania do metody w porównaniu do wyrażenia lambda, nie obejmuje ona jednak tego trzeciego przypadku.
M. Justin,

4
@ M.Justin: resultInFuture();Przypisanie jest identyczne z przedstawionym przeze mnie lambda, z wyjątkiem mojego przypadku, że twoja resultInFuture()metoda jest wbudowana. Tak więc podejście to ma dwie warstwy pośrednie (z których żadna nic nie robi) - metodę konstruowania lambda i samą lambda. Nawet gorzej!
Jack

5

Żadna z obecnych odpowiedzi w rzeczywistości nie dotyczy rdzenia pytania, czyli tego, czy klasa powinna mieć private boolean isResultInFuture(Result result)metodę, czy private Predicate<Result> resultInFuture()metodę. Chociaż są one w dużej mierze równoważne, każda z nich ma swoje zalety i wady.

Pierwsze podejście jest bardziej naturalne, jeśli wywołujesz metodę bezpośrednio oprócz tworzenia odwołania do metody. Na przykład, jeśli przetestujesz tę metodę w jednostce, łatwiej będzie zastosować pierwsze podejście. Kolejną zaletą pierwszego podejścia jest to, że metoda nie musi dbać o to, jak jest używana, oddzielając ją od strony wywołującej. Jeśli później zmienisz klasę, aby wywołać metodę bezpośrednio, to oddzielenie płatności zwróci się w bardzo niewielkim stopniu.

Pierwsze podejście jest również lepsze, jeśli chcesz dostosować tę metodę do różnych interfejsów funkcjonalnych lub różnych związków interfejsów. Na przykład możesz chcieć, aby odwołanie do metody było typu Predicate<Result> & Serializable, czego drugie podejście nie zapewnia elastyczności.

Z drugiej strony drugie podejście jest bardziej naturalne, jeśli zamierzasz wykonywać operacje wyższego rzędu w predykacie. Predicate ma kilka metod, których nie można wywołać bezpośrednio w odwołaniu do metody. Jeśli zamierzasz użyć tych metod, drugie podejście jest lepsze.

Ostatecznie decyzja jest subiektywna. Ale jeśli nie zamierzasz wywoływać metod w Predicate, skłaniam się ku preferowaniu pierwszego podejścia.


Operacje wyższego rzędu w predykacie są nadal dostępne po rzuceniu odwołania do metody:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack

4

Nie piszę już kodu Java, ale piszę w funkcjonalnym języku dla życia, a także wspieram inne zespoły, które uczą się programowania funkcjonalnego. Jagnięta mają delikatną słodką czytelność. Ogólnie przyjętym stylem jest to, że używasz ich, gdy możesz je wstawić, ale jeśli czujesz potrzebę przypisania jej do zmiennej do późniejszego wykorzystania, prawdopodobnie powinieneś po prostu zdefiniować metodę.

Tak, ludzie przez cały czas przypisują lambdy do zmiennych w samouczkach. To tylko samouczki, które pomogą ci dokładnie zrozumieć, jak działają. W rzeczywistości nie są one używane w ten sposób, z wyjątkiem stosunkowo rzadkich okoliczności (takich jak wybór między dwoma lambdami za pomocą wyrażenia if).

Oznacza to, że jeśli twoja lambda jest na tyle krótka, że ​​po wstawieniu jest czytelna, jak w przykładzie z komentarza @JimmyJames, to idź:

people = sort(people, (a,b) -> b.age() - a.age());

Porównaj to z czytelnością, gdy próbujesz wstawić swoją przykładową lambda:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

W twoim konkretnym przykładzie osobiście oflaguję go na żądanie ściągnięcia i poproszę o wyciągnięcie go do nazwanej metody ze względu na jego długość. Java jest irytująco pełnym językiem, więc być może z czasem jej specyficzne dla danego języka praktyki będą się różnić od innych języków, ale do tego czasu utrzymuj swoje lambdasy krótkie i proste.


2
Re: gadatliwość - podczas gdy nowe dodatki funkcjonalne same w sobie nie zmniejszają gadatliwości, pozwalają na to. Na przykład możesz zdefiniować: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)za pomocą oczywistej implementacji, a następnie możesz napisać taki kod: people = sort(people, (a,b) -> b.age() - a.age());co jest dużym ulepszeniem IMO.
JimmyJames,

To świetny przykład tego, o czym mówiłem, @JimmyJames. Gdy lambdas są krótkie i można je wstawić, stanowią wielką poprawę. Kiedy stają się tak długie, że chcesz je oddzielić i nadać im nazwę, lepiej zdefiniuj metodę. Dziękuję za pomoc w zrozumieniu, w jaki sposób moja odpowiedź została źle odczytana.
Karl Bielefeldt,

Myślę, że twoja odpowiedź była w porządku. Nie jestem pewien, dlaczego zostanie odrzucony.
JimmyJames,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.