ใน Java 8 จะดีกว่าการใช้นิพจน์การอ้างอิงเมธอดหรือเมธอดที่ส่งคืนการใช้อินเตอร์เฟสการทำงานหรือไม่?


11

Java 8 เพิ่มแนวคิดของส่วนต่อประสานการทำงานรวมถึงวิธีการใหม่มากมายที่ออกแบบมาเพื่อใช้งานส่วนต่อประสานการทำงาน อินสแตนซ์ของอินเทอร์เฟซเหล่านี้สามารถสร้างได้อย่างกระชับโดยใช้นิพจน์การอ้างอิงเมธอด (เช่นSomeClass::someMethod) และนิพจน์แลมบ์ดา (เช่น(x, y) -> x + y)

เพื่อนร่วมงานและฉันมีความคิดเห็นที่แตกต่างกันเมื่อควรใช้แบบฟอร์มเดียวหรือแบบอื่น (ซึ่ง "ดีที่สุด" ในกรณีนี้จะทำให้ "อ่านได้ง่ายที่สุด" และ "สอดคล้องกับมาตรฐานการปฏิบัติทั่วไป" มากที่สุด เทียบเท่า) โดยเฉพาะกรณีนี้เกี่ยวข้องกับกรณีที่ข้อมูลต่อไปนี้เป็นจริงทั้งหมด:

  • ฟังก์ชั่นที่เป็นปัญหาไม่ได้ใช้งานนอกขอบเขตเดียว
  • การตั้งชื่อให้ช่วยให้สามารถอ่านได้ (ตรงข้ามกับตรรกะที่เรียบง่ายพอที่จะดูว่าเกิดอะไรขึ้นอย่างรวดเร็ว)
  • ไม่มีเหตุผลการเขียนโปรแกรมอื่นทำไมรูปแบบหนึ่งจะดีกว่าอีกรูปแบบหนึ่ง

ความเห็นของฉันในปัจจุบันเกี่ยวกับเรื่องนี้คือการเพิ่มวิธีการส่วนตัวและการอ้างอิงโดยวิธีการอ้างอิงเป็นวิธีที่ดีกว่า รู้สึกว่านี่เป็นวิธีที่คุณสมบัติถูกออกแบบมาให้ใช้และดูเหมือนง่ายขึ้นในการสื่อสารสิ่งที่เกิดขึ้นผ่านชื่อวิธีการและลายเซ็น (เช่น "บูลีน isResultInFuture (ผลผลลัพธ์)" ชัดเจนว่ากำลังส่งคืนบูลีน) นอกจากนี้ยังทำให้วิธีการส่วนตัวนำมาใช้ซ้ำได้มากขึ้นหากการปรับปรุงในอนาคตให้กับชั้นเรียนต้องการใช้ประโยชน์จากการตรวจสอบเดียวกัน แต่ไม่ต้องการเครื่องมือห่อหุ้มส่วนต่อประสานการทำงาน

การตั้งค่าของเพื่อนร่วมงานของฉันคือการมีวิธีการที่ส่งกลับอินสแตนซ์ของอินเทอร์เฟซ (เช่น "Predicate resultInFuture ()") สำหรับฉันแล้วมันรู้สึกว่ามันไม่ได้เป็นวิธีที่จะใช้คุณสมบัติรู้สึก clunkier เล็กน้อยและดูเหมือนว่ามันยากที่จะสื่อสารความตั้งใจผ่านการตั้งชื่อ

เพื่อให้ตัวอย่างคอนกรีตนี้นี่คือรหัสเดียวกันเขียนในสไตล์ที่แตกต่าง:

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

เมื่อเทียบกับ

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

เมื่อเทียบกับ

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

มีเอกสารอย่างเป็นทางการหรือกึ่งทางการหรือแสดงความคิดเห็นว่าแนวทางใดวิธีหนึ่งเป็นที่ต้องการมากกว่านั้นสอดคล้องกับเจตนารมณ์ของนักออกแบบภาษาหรืออ่านได้มากกว่าหรือไม่? การ จำกัด แหล่งที่มาอย่างเป็นทางการมีเหตุผลใดที่ชัดเจนว่าทำไมจึงเป็นแนวทางที่ดีกว่า


1
แลมบ์ดาถูกเพิ่มเข้ากับภาษาด้วยเหตุผลและมันก็ไม่ได้ทำให้ Java แย่ลงไปกว่านี้

@Snowman มันไปได้โดยไม่ต้องพูดอะไร ดังนั้นฉันจึงสันนิษฐานว่ามีความสัมพันธ์โดยนัยระหว่างความคิดเห็นของคุณและคำถามของฉันที่ฉันไม่ได้ดึงดูด จุดที่คุณพยายามทำคืออะไร?
M. Justin

2
ฉันคิดว่าประเด็นที่เขากำลังทำอยู่คือถ้าลูกแกะไม่ได้รับการพัฒนาให้ดีขึ้นพวกเขาก็คงไม่ใส่ใจที่จะแนะนำพวกเขา
Robert Harvey

2
@ RobertHarvey Sure ถึงจุดนั้นทั้งแกะและการอ้างอิงวิธีการได้รับการเพิ่มในเวลาเดียวกันจึงไม่ได้เป็นสภาพที่เป็นมาก่อน แม้ว่าจะไม่มีกรณีนี้ แต่ก็มีบางครั้งที่การอ้างอิงวิธีการจะยังคงดีกว่า เช่นวิธีสาธารณะที่มีอยู่ (เช่นObject::toString) ดังนั้นคำถามของฉันเกี่ยวกับว่ามันดีกว่าในอินสแตนซ์ชนิดนี้ที่ฉันจัดไว้ที่นี่ดีกว่ามีอินสแตนซ์ที่หนึ่งดีกว่าอีกอัน
M. Justin

คำตอบ:


8

ในแง่ของการเขียนโปรแกรมการทำงานสิ่งที่คุณและเพื่อนร่วมงานของคุณจะคุยเป็นจุดฟรีสไตล์มากขึ้นโดยเฉพาะการลดลงของการทางพิเศษแห่งประเทศไทย พิจารณาการมอบหมายสองอย่างต่อไปนี้:

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

เหล่านี้จะเทียบเท่าการดำเนินงานและเป็นคนแรกที่เรียกว่าpointfulสไตล์ในขณะที่สองเป็นจุดฟรีสไตล์ คำว่า "จุด" หมายถึงการโต้แย้งฟังก์ชั่นที่มีชื่อ (นี้มาจากโทโพโลยี) ซึ่งหายไปในกรณีหลัง

โดยทั่วไปแล้วสำหรับฟังก์ชั่นทั้งหมด F แลมบ์ดาแรปเปอร์ที่รับอาร์กิวเมนต์ทั้งหมดที่กำหนดและส่งผ่านไปยัง F ไม่เปลี่ยนแปลงจากนั้นส่งคืนผลลัพธ์ของ F ที่ไม่เปลี่ยนแปลงเหมือนกับตัว F ถอดแลมบ์ดาและใช้ F โดยตรง (ไปจากรูปแบบ pointful กับรูปแบบจุดฟรีบน) จะเรียกว่าการลดลงของการทางพิเศษแห่งประเทศไทย , คำที่เกิดจากแลมบ์ดาแคลคูลัส

มีการแสดงออกที่จุดโทษที่ไม่ได้สร้างขึ้นโดยการลดลงของการทางพิเศษแห่งประเทศไทยเป็นตัวอย่างคลาสสิกมากที่สุดซึ่งเป็นองค์ประกอบของฟังก์ชั่น เมื่อกำหนดฟังก์ชั่นจากประเภท A ถึงประเภท B และฟังก์ชั่นจากประเภท B ถึงประเภท C เราสามารถเขียนฟังก์ชันเหล่านี้เข้าด้วยกันเป็นฟังก์ชันตั้งแต่ประเภท A ถึงประเภท C:

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

Result deriveResult(Foo foo)ตอนนี้สมมติว่าเรามีวิธีการ เนื่องจากเพรดิเคตเป็นฟังก์ชั่นเราสามารถสร้างเพรดิเคตใหม่ที่เรียกครั้งแรกderiveResultจากนั้นทดสอบผลลัพธ์ที่ได้รับ:

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

แม้ว่าการนำไปใช้งานของcomposeรูปแบบที่เป็นประเด็นกับแลมบ์ดาการใช้องค์ประกอบเพื่อกำหนดisFoosResultInFutureนั้นไม่มีค่าใช้จ่ายเนื่องจากไม่มีข้อโต้แย้งที่ต้องกล่าวถึง

การเขียนโปรแกรมแบบฟรีพ้อยท์เรียกอีกอย่างว่าการเขียนโปรแกรมโดยปริยายและสามารถเป็นเครื่องมือที่ทรงพลังในการเพิ่มความสามารถในการอ่านโดยการลบรายละเอียดที่ไม่มีประโยชน์ออกจากคำจำกัดความของฟังก์ชั่น Java 8 ไม่รองรับการเขียนโปรแกรมแบบพ้อยท์ฟรีเกือบจะทั่วถึงเหมือนกับภาษาที่ใช้งานได้มากกว่าอย่าง Haskell แต่กฎที่ดีคือการลดความผิดพลาด ไม่จำเป็นต้องใช้ lambdas ที่ไม่มีพฤติกรรมแตกต่างจากฟังก์ชั่นที่ห่อหุ้ม


1
ผมคิดว่าสิ่งที่เพื่อนร่วมงานและฉันจะคุยของฉันมากขึ้นทั้งสองได้รับมอบหมาย: Predicate<Result> pred = this::isResultInFuture;และPredicate<Result> pred = resultInFuture();ที่ resultInFuture () Predicate<Result>ส่งกลับ ดังนั้นในขณะที่นี่เป็นคำอธิบายที่ดีว่าเมื่อใดที่ต้องใช้การอ้างอิงเมธอดกับการแสดงออกแลมบ์ดา แต่ก็ไม่ครอบคลุมกรณีที่สามนี้
M. Justin

4
@ M.Justin: การresultInFuture();มอบหมายนั้นเหมือนกับการมอบหมายแลมบ์ดาที่ฉันนำเสนอยกเว้นในกรณีของฉันresultInFuture()วิธีการของคุณจะถูกขีดเส้นใต้ ดังนั้นวิธีการนั้นจึงมีการอ้อมสองชั้น (ซึ่งไม่ทำอะไรเลย) - วิธีการสร้างแลมบ์ดาและแลมบ์ดาเอง ยิ่งเลวร้ายลง!
แจ็ค

5

คำตอบปัจจุบันไม่มีคำตอบที่เป็นแก่นแท้ของคำถามซึ่งก็คือคลาสควรมีprivate boolean isResultInFuture(Result result)เมธอดหรือprivate Predicate<Result> resultInFuture()เมธอด ในขณะที่พวกเขาส่วนใหญ่เทียบเท่ามีข้อดีและข้อเสียแต่ละ

วิธีแรกนั้นดูเป็นธรรมชาติมากกว่าถ้าคุณเรียกใช้เมธอดโดยตรงนอกเหนือจากการสร้างการอ้างอิงเมธอด ตัวอย่างเช่นหากคุณทดสอบหน่วยวิธีนี้มันอาจจะง่ายกว่าที่จะใช้วิธีแรก ข้อดีอีกประการหนึ่งของวิธีการแรกคือวิธีการนี้ไม่จำเป็นต้องสนใจว่าจะใช้งานอย่างไรโดยแยกออกจากไซต์การโทร หากภายหลังคุณเปลี่ยนชั้นเรียนเพื่อให้คุณเรียกวิธีการโดยตรง decoupling นี้จะชำระในทางที่เล็กมาก

วิธีแรกยังเหนือกว่าถ้าคุณอาจต้องการปรับวิธีนี้ให้เป็นอินเตอร์เฟสการทำงานที่แตกต่างกันหรืออินเทอร์เฟซของสหภาพที่แตกต่างกัน ตัวอย่างเช่นคุณอาจต้องการให้การอ้างอิงเมธอดเป็นประเภทPredicate<Result> & Serializableซึ่งวิธีที่สองไม่ให้ความยืดหยุ่นในการทำ

ในทางกลับกันวิธีที่สองนั้นเป็นเรื่องที่เป็นธรรมชาติมากขึ้นถ้าคุณตั้งใจจะดำเนินการตามคำสั่งที่สูงขึ้นในภาคแสดง เพรดิเคตมีหลายวิธีที่ไม่สามารถเรียกได้โดยตรงบนการอ้างอิงเมธอด หากคุณตั้งใจจะใช้วิธีการเหล่านี้วิธีที่สองนั้นเหนือกว่า

ในที่สุดการตัดสินใจเป็นเรื่องส่วนตัว แต่ถ้าคุณไม่ได้ตั้งใจจะเรียกวิธีการของเพรดิเคตฉันจะเอนตัวเลือกวิธีแรก


การดำเนินการคำสั่งซื้อที่สูงขึ้นบนเพรดิเคตยังคงมีอยู่โดยการหล่อการอ้างอิงวิธี:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
แจ็ค

4

ฉันไม่ได้เขียนโค้ด Java อีกต่อไป แต่ฉันเขียนเป็นภาษาที่ใช้งานได้เพื่อการดำรงชีพและสนับสนุนทีมอื่น ๆ ที่เรียนรู้การเขียนโปรแกรมใช้งานได้ แลมบ์ดามีจุดอ่านที่อ่อนหวาน สไตล์ที่ยอมรับโดยทั่วไปสำหรับพวกเขาคือคุณใช้พวกเขาเมื่อคุณสามารถอินไลน์พวกเขา แต่ถ้าคุณรู้สึกว่าจำเป็นต้องกำหนดให้กับตัวแปรสำหรับใช้ในภายหลังคุณควรจะกำหนดวิธีการ

ใช่คนกำหนด lambdas ให้กับตัวแปรในแบบฝึกหัดตลอดเวลา เหล่านี้เป็นเพียงบทเรียนเพื่อช่วยให้คุณเข้าใจอย่างถ่องแท้ถึงวิธีการทำงานของมัน พวกเขาไม่ได้ใช้วิธีนั้นในทางปฏิบัติยกเว้นในสถานการณ์ที่ค่อนข้างหายาก (เช่นเลือกระหว่างลูกแกะสองตัวโดยใช้ if expression)

นั่นหมายความว่าแลมบ์ดาของคุณสั้นพอที่จะอ่านได้ง่ายหลังจากอินไลน์เหมือนตัวอย่างจากความคิดเห็นของ @JimmyJames จากนั้นลองทำดู:

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

เปรียบเทียบสิ่งนี้กับความสามารถในการอ่านเมื่อคุณพยายามอินแลมบ์ดาตัวอย่างของคุณ:

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

สำหรับตัวอย่างเฉพาะของคุณฉันจะตั้งค่าสถานะมันเป็นการร้องขอการดึงและขอให้คุณดึงมันออกมาเป็นวิธีการตั้งชื่อเพราะความยาว Java เป็นภาษา verbose ที่น่ารำคาญดังนั้นบางทีในเวลาที่การปฏิบัติเฉพาะภาษาของตัวเองจะแตกต่างจากภาษาอื่น ๆ แต่จนถึงตอนนี้ให้ลูกแกะของคุณสั้นและอินไลน์


2
Re: verbosity - ในขณะที่การเพิ่มฟังก์ชั่นใหม่ไม่ได้ด้วยตัวเองลด verbosity มากพวกเขาอนุญาตให้วิธีการทำเช่นนั้น ตัวอย่างเช่นคุณสามารถกำหนด: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)ด้วยการใช้งานที่ชัดเจนและจากนั้นคุณสามารถเขียนรหัสเช่นนี้: people = sort(people, (a,b) -> b.age() - a.age());ซึ่งเป็น IMO การปรับปรุงที่ยิ่งใหญ่
JimmyJames

นี่เป็นตัวอย่างที่ดีของสิ่งที่ฉันพูดถึง @JimmyJames เมื่อ lambdas สั้นและสามารถ inline พวกเขาเป็นการปรับปรุงที่ดี เมื่อพวกเขาใช้เวลานานมากที่คุณต้องการแยกพวกเขาออกและตั้งชื่อพวกเขาคุณจะดีกว่าแค่กำหนดวิธีการ ขอบคุณที่ช่วยให้ฉันเข้าใจว่าคำตอบของฉันถูกเข้าใจผิดอย่างไร
Karl Bielefeldt

ฉันคิดว่าคำตอบของคุณใช้ได้ ไม่แน่ใจว่าทำไมมันจะถูกลงคะแนน
JimmyJames
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.