Java 8'de, işlevsel arabirimin bir uygulamasını döndüren yöntem başvuru ifadelerini veya yöntemleri kullanmak stilistik olarak daha mı iyi?


11

Java 8, fonksiyonel arayüzler konseptinin yanı sıra fonksiyonel arayüzler almak için tasarlanmış çok sayıda yeni yöntem ekledi . Bu arayüzlerin örnekleri, yöntem referans ifadeleri (örn. SomeClass::someMethod) Ve lambda ifadeleri (örn. (x, y) -> x + y) Kullanılarak kısa bir sürede oluşturulabilir .

Bir meslektaşım ve ben bir formun veya başka bir formun ne zaman kullanılmasının en iyi olduğu konusunda farklı görüşlere sahibiz (bu durumda "en iyisi" gerçekten "en okunabilir" ve "en çok standart uygulamalara uygun" olarak kaybolur, temelde aksi halde eşdeğer). Özellikle, bu aşağıdakilerin doğru olduğu durumu içerir:

  • söz konusu işlev tek bir kapsam dışında kullanılmaz
  • örneğe bir ad vermek okunabilirliğe yardımcı olur (örneğin, mantığın bir bakışta neler olduğunu görecek kadar basit olması yerine)
  • bir formun diğerine tercih edilmesinin başka programlama nedenleri yoktur.

Konuyla ilgili şu anki görüşüm, özel bir yöntem eklemenin ve yöntem referansı ile atıfta bulunmanın daha iyi bir yaklaşım olduğu yönündedir. Özelliğin nasıl kullanılmak üzere tasarlandığı gibi geliyor ve neler olup bittiğini yöntem adları ve imzalarla iletmek daha kolay görünüyor (örneğin, "boolean isResultInFuture (Sonuç sonucu)" açıkça bir boole döndürdüğünü söylüyor). Ayrıca, sınıfta gelecekteki bir geliştirme aynı denetimi kullanmak istiyorsa, ancak işlevsel arabirim sarmalayıcısına ihtiyaç duymuyorsa özel yöntemi daha yeniden kullanılabilir hale getirir.

Meslektaşımın tercihi, arabirimin örneğini döndüren bir yönteme sahip olmaktır (örneğin, "Predicate resultsInFuture ()"). Bana göre bu, özelliğin nasıl tasarlandığı gibi değil, biraz daha hantal görünüyor ve adlandırma yoluyla gerçekten iletişim kurmak daha zor gibi görünüyor.

Bu örneği somutlaştırmak için, farklı stillerle yazılmış aynı kod şöyledir:

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
    });
  }
}

Bir yaklaşımın daha çok tercih edileceği, dil tasarımcılarının amaçlarıyla daha uyumlu mu yoksa daha okunabilir mi olduğu konusunda resmi veya yarı resmi belgeler veya yorumlar var mı? Resmi bir kaynağı yasaklamak, birinin daha iyi bir yaklaşım olmasının açık nedenleri var mı?


1
Lambdas bir nedenden dolayı dile eklendi ve Java'yı daha da kötüleştirmek değildi.

@Snowman Bu söylemeye gerek yok. Bu nedenle, yorumunuz ile sorum arasında tam olarak yakalamadığım bazı zımni bir ilişki olduğunu varsayıyorum. Ne yapmaya çalışıyorsun?
M. Justin

2
Demek istediğim şu ki, lambdalar statüko için bir gelişme olmasaydı, onları tanıtmak için rahatsız olmazlardı.
Robert Harvey

2
@RobertHarvey Tabii. Bununla birlikte, bu noktaya, hem lambdalar hem de yöntem referansları aynı anda eklendi, bu yüzden ikisi de daha önce statüko değildi. Bu özel durum olmasa bile, yöntem referanslarının hala daha iyi olacağı zamanlar vardır; örneğin, mevcut bir halka açık yöntem (ör. Object::toString). Bu yüzden sorum, burada yerleştirdiğim bu özel örnek türünde daha iyi olup olmadığı hakkında, birinin diğerinden daha iyi olduğu örneklerin olup olmadığından daha fazla veya tam tersi.
M. Justin

Yanıtlar:


8

Fonksiyonel programlama açısından, siz ve meslektaşınız üzerinde tartıştığınız şey noktadan bağımsız bir tarz , daha özel olarak eta azaltımıdır . Aşağıdaki iki ödevi göz önünde bulundurun:

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

Bunlar operasyonel olarak eşdeğerdir ve ilk olarak adlandırılır pointful saniyede iken tarzıdır noktası serbest stil. "Nokta" terimi, ikinci durumda eksik olan adlandırılmış bir işlev bağımsız değişkenini (bu topolojiden gelir) belirtir.

Daha genel olarak, tüm F fonksiyonları için, verilen tüm argümanları alan ve F'ye değişmeden geçiren, daha sonra F'nin değişmeyen sonucunu döndüren bir sarıcı lambda, F ile aynıdır. Lambda'nın çıkarılması ve doğrudan F'nin kullanılması (noktasal stillerden yukarıdaki noktasız stile kadar), lambda hesabından kaynaklanan bir terim olan eta azaltımı olarak adlandırılır .

Eta indirgeme ile yaratılmayan, en klasik örneği fonksiyon kompozisyonu olan noktasız ifadeler vardır . A tipinden B tipine bir fonksiyon ve B tipinden C tipine bir fonksiyon verildiğinde, bunları birlikte A tipinden C tipine bir fonksiyonda oluşturabiliriz:

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

Şimdi bir yöntemimiz olduğunu varsayalım Result deriveResult(Foo foo). Bir Tahmin bir İşlev olduğu için, önce çağrılan deriveResultve sonra türetilen sonucu test eden yeni bir yüklem oluşturabiliriz :

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

Her ne kadar uygulama arasında composelambdas ile kullanımları pointful stil, kullanım tanımlamak için ileti yazma isFoosResultInFuturenoktası serbesttir hiçbir argüman söz gerekmektedir çünkü.

Noktasız programlama da örtük programlama olarak adlandırılır ve işlev tanımından anlamsız (pun amaçlı) ayrıntıları kaldırarak okunabilirliği artırmak için güçlü bir araç olabilir. Java 8, Haskell gibi daha işlevsel diller kadar noktasız programlamayı desteklemez, ancak iyi bir kural her zaman eta azaltma yapmaktır. Sardıkları işlevlerden farklı davranışları olmayan lambdas kullanmaya gerek yoktur.


1
İş arkadaşımın ve benim tartıştığımız şeyin bu iki ödevden daha fazlası olduğunu düşünüyorum: Predicate<Result> pred = this::isResultInFuture;ve Predicate<Result> pred = resultInFuture();sonuçInFuture () 'nin a Predicate<Result>. Bu nedenle, yöntem başvurusunun bir lambda ifadesine karşı ne zaman kullanılacağına dair büyük bir açıklama olsa da, bu üçüncü durumu tam olarak kapsamaz.
M. Justin

4
@ M.Justin: resultInFuture();Ödev sunduğum lambda ödevi ile aynı, benim durumumda resultInFuture()yöntem satır içi ise. Böylece bu yaklaşımın iki dolaylı katmanı vardır (ikisi de hiçbir şey yapmaz) - lambda oluşturma yöntemi ve lambda'nın kendisi. Daha da kötüsü!
Jack

5

Mevcut cevapların hiçbiri aslında sorunun özünü ele almaz, bu da sınıfın bir private boolean isResultInFuture(Result result)metoda mı yoksa bir private Predicate<Result> resultInFuture()metoda mı sahip olması gerektiğidir . Büyük ölçüde eşdeğer olmalarına rağmen, her birinin avantajları ve dezavantajları vardır.

Bir yöntem başvurusu oluşturmanın yanı sıra yöntemi doğrudan çağırırsanız ilk yaklaşım daha doğal olur. Örneğin, bu yöntemi birim olarak test ederseniz, ilk yaklaşımı kullanmak daha kolay olabilir. İlk yaklaşımın bir diğer avantajı, yöntemin nasıl kullanıldığına dikkat etmek zorunda olmaması ve onu çağrı sitesinden ayırmasıdır. Daha sonra yöntemi doğrudan çağırmak için sınıfı değiştirirseniz, bu ayırma işlemi çok küçük bir şekilde karşılığını verir.

Bu yöntemi, farklı işlevsel arayüzlere veya farklı arayüz birliklerine uyarlamak isteyebiliyorsanız, ilk yaklaşım da üstündür. Örneğin, yöntem başvurusunun Predicate<Result> & Serializable, ikinci yaklaşımın size ulaşma esnekliği sağlamadığı türde olmasını isteyebilirsiniz .

Öte yandan, yüklem üzerinde daha yüksek dereceli işlemler yapmak istiyorsanız ikinci yaklaşım daha doğaldır. Predicate üzerinde doğrudan bir yöntem başvurusunda çağrılamayan birkaç yöntem vardır. Bu yöntemleri kullanmayı düşünüyorsanız ikinci yaklaşım daha üstündür.

Nihayetinde karar özneldir. Ancak, Predicate'te yöntem çağırmayı düşünmüyorsanız, ilk yaklaşımı tercih etmeye yönelirim.


Yüklem üzerinde daha yüksek mertebeden işlemler hala yöntem referansı ile yapılabilir:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack

4

Artık Java kodu yazmıyorum, ancak yaşamak için işlevsel bir dilde yazıyorum ve ayrıca fonksiyonel programlamayı öğrenen diğer ekipleri de destekliyorum. Lambdaların narin tatlı bir okunabilirlik noktası var. Onlar için genel olarak kabul edilen stil, bunları satır içine alabileceğiniz zaman kullanmanızdır, ancak daha sonra kullanmak için bir değişkene atamanız gerektiğini düşünüyorsanız, muhtemelen sadece bir yöntem tanımlamanız gerekir.

Evet, insanlar her zaman öğreticilerdeki değişkenlere lambdas atarlar. Bunlar sadece nasıl çalıştıklarını tam olarak anlamanıza yardımcı olacak öğreticilerdir. Nispeten nadir durumlar (aslında bir if ifadesi kullanarak iki lambda arasında seçim yapmak gibi) dışında pratikte bu şekilde kullanılmazlar.

Bu, lambda'nızın, @JimmyJames'in yorumundaki örnek gibi, satırdan sonra kolayca okunabilecek kadar kısa olduğu anlamına gelir:

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

Örnek lambda'nızı satır içine almaya çalıştığınızda bunu okunabilirlikle karşılaştırın:

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

Özel örneğiniz için, kişisel olarak bir çekme isteği üzerine işaretler ve uzunluğu nedeniyle adlandırılmış bir yönteme çekmenizi isterim. Java sinir bozucu derecede ayrıntılı bir dildir, bu yüzden belki de zaman içinde kendi dile özgü uygulamaları diğer dillerden farklı olacaktır, ancak o zamana kadar kuzularınızı kısa ve satır içi tutun.


2
Ynt: ayrıntı düzeyi - Yeni işlevsel eklemeler kendi başlarına fazla ayrıntı düzeyini azaltmazken, bunu yapmanın yollarını sağlarlar. Örneğin: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)açık bir uygulama ile tanımlayabilirsiniz ve daha sonra şöyle bir kod yazabilirsiniz: people = sort(people, (a,b) -> b.age() - a.age());bu büyük bir iyileştirme IMO'sudur.
JimmyJames

Bu neden bahsettiğimin harika bir örneği @JimmyJames. Lambdalar kısa ve eğimli olduğunda, büyük bir gelişmedir. Onları ayırmak ve onlara bir isim vermek istediğiniz kadar uzun sürdüklerinde, sadece bir yöntem tanımlamanız daha iyi olur. Cevabımın nasıl yanlış yorumlandığını anlamama yardımcı olduğunuz için teşekkürler.
Karl Bielefeldt

Bence cevabınız gayet iyi. Neden oylanacağından emin değilim.
JimmyJames
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.