Trong Java 8, việc sử dụng các biểu thức tham chiếu phương thức hoặc các phương thức trả về việc triển khai giao diện chức năng có tốt hơn về mặt phong cách không?


11

Java 8 đã thêm khái niệm về các giao diện chức năng , cũng như nhiều phương thức mới được thiết kế để có các giao diện chức năng. Thể hiện của các giao diện này có thể được tạo ra một cách ngắn gọn bằng cách sử dụng các biểu thức tham chiếu phương thức (ví dụ SomeClass::someMethod) và biểu thức lambda (ví dụ (x, y) -> x + y).

Một đồng nghiệp và tôi có ý kiến ​​khác nhau về việc tốt nhất nên sử dụng hình thức này hay hình thức khác (trong đó "tốt nhất" trong trường hợp này thực sự sôi sục đến "dễ đọc nhất" và "phù hợp nhất với thông lệ tiêu chuẩn", vì về cơ bản chúng là khác tương đương). Cụ thể, điều này liên quan đến trường hợp sau đây đều đúng:

  • chức năng trong câu hỏi không được sử dụng bên ngoài một phạm vi
  • đặt tên cá thể giúp dễ đọc (trái ngược với logic là đủ đơn giản để xem nhanh những gì đang diễn ra)
  • không có lý do lập trình nào khác tại sao một hình thức lại thích hợp hơn hình thức kia.

Ý kiến ​​hiện tại của tôi về vấn đề này là việc thêm một phương thức riêng tư và tham khảo rằng bằng cách tham chiếu phương thức, là cách tiếp cận tốt hơn. Cảm giác như đây là cách tính năng được thiết kế để sử dụng và có vẻ dễ dàng hơn để truyền đạt những gì đang diễn ra thông qua tên phương thức và chữ ký (ví dụ: "boolean isResultInFuture (Kết quả kết quả)" rõ ràng đang nói rằng nó sẽ trả về một boolean). Nó cũng làm cho phương thức riêng có thể tái sử dụng nhiều hơn nếu một lớp nâng cao trong tương lai muốn sử dụng cùng một kiểm tra, nhưng không cần trình bao bọc giao diện chức năng.

Sở thích của đồng nghiệp của tôi là có một phương thức trả về thể hiện của giao diện (ví dụ: "Dự đoán kết quảInFuture ()"). Đối với tôi, cảm giác này không hoàn toàn giống như cách sử dụng tính năng này, cảm giác hơi cục mịch và dường như khó thực hiện hơn ý định truyền đạt thông qua việc đặt tên.

Để làm cho ví dụ này cụ thể, đây là cùng một mã, được viết theo các kiểu khác nhau:

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

so với

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

so với

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

Có bất kỳ tài liệu hoặc nhận xét chính thức hoặc bán chính thức nào xung quanh việc một phương pháp được ưu tiên hơn, phù hợp hơn với ý định của nhà thiết kế ngôn ngữ hay dễ đọc hơn không? Chặn một nguồn chính thức, có bất kỳ lý do rõ ràng tại sao một trong những cách tiếp cận tốt hơn?


1
Lambdas đã được thêm vào ngôn ngữ vì một lý do và nó không làm cho Java trở nên tồi tệ hơn.

@Snowman Mà không cần nói. Do đó, tôi cho rằng có một số mối quan hệ hàm ý giữa bình luận của bạn và câu hỏi của tôi mà tôi không hoàn toàn nắm bắt được. Điểm bạn đang cố gắng thực hiện là gì?
M. Justin

2
Tôi cho rằng quan điểm mà anh ta đưa ra là, nếu lambdas không phải là một sự cải thiện hiện trạng, họ sẽ không bận tâm để giới thiệu chúng.
Robert Harvey

2
@RobertHarvey Chắc chắn. Tuy nhiên, đến thời điểm đó, cả lambdas và tham chiếu phương thức đã được thêm vào cùng một lúc, do đó, không phải là hiện trạng trước đây. Ngay cả khi không có trường hợp cụ thể này, đôi khi các tham chiếu phương thức vẫn tốt hơn; ví dụ, một phương thức công khai hiện có (ví dụ Object::toString). Vì vậy, câu hỏi của tôi là nhiều hơn về việc liệu nó tốt hơn trong trường hợp cụ thể mà tôi đặt ra ở đây, hơn là liệu có tồn tại trường hợp nào tốt hơn trường hợp khác hay ngược lại.
M. Justin

Câu trả lời:


8

Về mặt lập trình chức năng, những gì bạn và đồng nghiệp đang thảo luận là phong cách tự do , cụ thể hơn là giảm eta . Hãy xem xét hai bài tập sau:

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

Đây là những hoạt động tương đương, và là người đầu tiên được gọi là có đầu nhọn phong cách trong khi thứ hai là điểm miễn phí phong cách. Thuật ngữ "điểm" dùng để chỉ một đối số chức năng được đặt tên (điều này xuất phát từ cấu trúc liên kết) bị thiếu trong trường hợp sau.

Tổng quát hơn, đối với tất cả các hàm F, một lambda bao bọc lấy tất cả các đối số đã cho và chuyển chúng sang F không thay đổi, sau đó trả về kết quả của F không thay đổi giống hệt với chính F. Loại bỏ lambda và sử dụng F trực tiếp (đi từ kiểu nhọn sang kiểu không có điểm ở trên) được gọi là giảm eta , một thuật ngữ bắt nguồn từ phép tính lambda .

Có các biểu thức miễn phí điểm không được tạo ra bằng cách giảm eta, ví dụ kinh điển nhất trong số đó là thành phần hàm . Cho một hàm từ loại A đến loại B và một chức năng từ loại B đến loại C, chúng ta có thể kết hợp chúng lại với nhau thành một hàm từ loại A đến loại C:

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

Bây giờ giả sử chúng ta có một phương pháp Result deriveResult(Foo foo). Vì Vị ngữ là một Hàm, chúng ta có thể xây dựng một vị từ mới mà lần đầu tiên gọi deriveResultvà sau đó kiểm tra kết quả dẫn xuất:

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

Mặc dù thực hiện các composemục đích sử dụng phong cách có đầu nhọn với lambdas, các sử dụng của soạn thư để xác định isFoosResultInFuturelà điểm miễn phí vì không có đối số cần phải được đề cập.

Lập trình điểm miễn phí còn được gọi là lập trình ngầm , và nó có thể là một công cụ mạnh mẽ để tăng khả năng đọc bằng cách loại bỏ các chi tiết vô nghĩa (ý định chơi chữ) khỏi định nghĩa hàm. Java 8 không hỗ trợ lập trình điểm miễn phí gần như triệt để như nhiều ngôn ngữ chức năng như Haskell làm, nhưng một nguyên tắc tốt là luôn luôn thực hiện giảm eta. Không cần sử dụng lambdas không có hành vi khác với các chức năng mà chúng bao bọc.


1
Tôi nghĩ rằng những gì đồng nghiệp của tôi và tôi đang thảo luận là nhiều hơn hai bài tập này: Predicate<Result> pred = this::isResultInFuture;Predicate<Result> pred = resultInFuture();nơi resultInFuture () trả về a Predicate<Result>. Vì vậy, trong khi đây là một lời giải thích tuyệt vời về thời điểm sử dụng tham chiếu phương thức so với biểu thức lambda, thì nó không hoàn toàn bao gồm trường hợp thứ ba này.
M. Justin

4
@ M.Justin: Bài resultInFuture();tập này giống hệt với bài tập lambda tôi đã trình bày, ngoại trừ trong trường hợp của tôi, resultInFuture()phương pháp của bạn được nội tuyến. Vì vậy, cách tiếp cận đó có hai lớp không xác định (không phải là không làm gì cả) - phương pháp xây dựng lambda và chính lambda. Tệ hơn nữa!
Jack

5

Cả hai câu trả lời hiện tại thực sự giải quyết cốt lõi của câu hỏi, đó là liệu lớp nên có một private boolean isResultInFuture(Result result)phương thức hay một private Predicate<Result> resultInFuture()phương thức. Trong khi chúng phần lớn tương đương, có những lợi thế và bất lợi cho mỗi.

Cách tiếp cận đầu tiên sẽ tự nhiên hơn nếu bạn gọi phương thức trực tiếp ngoài việc tạo tham chiếu phương thức. Ví dụ: nếu bạn kiểm tra đơn vị phương pháp này, có thể dễ dàng sử dụng phương pháp đầu tiên hơn. Một ưu điểm khác của cách tiếp cận đầu tiên là phương pháp này không phải quan tâm đến cách sử dụng, tách nó khỏi trang web cuộc gọi của nó. Nếu sau này bạn thay đổi lớp để bạn gọi phương thức trực tiếp, việc tách rời này sẽ trả hết theo một cách rất nhỏ.

Cách tiếp cận đầu tiên cũng ưu việt hơn nếu bạn có thể muốn điều chỉnh phương pháp này với các giao diện chức năng khác nhau hoặc các hiệp hội giao diện khác nhau. Ví dụ, bạn có thể muốn tham chiếu phương thức thuộc loại Predicate<Result> & Serializable, cách tiếp cận thứ hai không mang lại cho bạn sự linh hoạt để đạt được.

Mặt khác, cách tiếp cận thứ hai sẽ tự nhiên hơn nếu bạn có ý định thực hiện các thao tác bậc cao hơn trên vị từ. Vị ngữ có một số phương thức trên đó không thể được gọi trực tiếp trên tham chiếu phương thức. Nếu bạn có ý định sử dụng các phương pháp này, cách tiếp cận thứ hai là ưu việt.

Cuối cùng quyết định là chủ quan. Nhưng nếu bạn không có ý định gọi các phương thức trên Vị ngữ, tôi sẽ nghiêng về việc thích cách tiếp cận đầu tiên.


Các thao tác bậc cao hơn trên biến vị ngữ vẫn có sẵn bằng cách chuyển tham chiếu phương thức:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack

4

Tôi không viết mã Java nữa, nhưng tôi viết bằng ngôn ngữ chức năng để kiếm sống và cũng hỗ trợ các nhóm khác đang học lập trình chức năng. Lambdas có một điểm ngọt ngào tinh tế dễ đọc. Kiểu thường được chấp nhận cho họ là bạn sử dụng chúng khi bạn có thể nội tuyến chúng, nhưng nếu bạn cảm thấy cần phải gán nó cho một biến để sử dụng sau này, có lẽ bạn chỉ nên xác định một phương thức.

Có, mọi người gán lambdas cho các biến trong hướng dẫn mọi lúc. Đó chỉ là những hướng dẫn để giúp bạn hiểu rõ về cách họ làm việc. Chúng không thực sự được sử dụng theo cách đó trong thực tế, ngoại trừ trong những trường hợp tương đối hiếm (như chọn giữa hai lambdas bằng cách sử dụng biểu thức if).

Điều đó có nghĩa là nếu lambda của bạn đủ ngắn để có thể dễ dàng đọc được sau khi nội tuyến, như ví dụ từ nhận xét của @JimmyJames, thì hãy tìm nó:

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

So sánh điều này với khả năng đọc khi bạn cố gắng nội tuyến lambda ví dụ của bạn:

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

Đối với ví dụ cụ thể của bạn, tôi đích thân gắn cờ nó theo yêu cầu kéo và yêu cầu bạn kéo nó ra một phương thức được đặt tên vì độ dài của nó. Java là một ngôn ngữ dài dòng khó chịu, vì vậy có lẽ trong thời gian, các thực tiễn cụ thể về ngôn ngữ của nó sẽ khác với các ngôn ngữ khác, nhưng cho đến lúc đó, hãy giữ lambdas của bạn ngắn và nội tuyến.


2
Re: độ dài - Trong khi các bổ sung chức năng mới thì không, nhưng bản thân chúng làm giảm nhiều tính dài dòng, chúng cho phép các cách để làm như vậy. Ví dụ, bạn có thể xác định: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)với việc triển khai rõ ràng và sau đó bạn có thể viết mã như thế này: people = sort(people, (a,b) -> b.age() - a.age());đó là một IMO cải tiến lớn.
JimmyJames

Đó là một ví dụ tuyệt vời về những gì tôi đã nói, @JimmyJames. Khi lambdas ngắn và có thể được nội tuyến, chúng là một cải tiến tuyệt vời. Khi chúng kéo dài đến mức bạn muốn tách chúng ra và đặt tên cho chúng, tốt hơn hết là bạn chỉ nên xác định một phương thức. Cảm ơn đã giúp tôi hiểu làm thế nào câu trả lời của tôi bị hiểu sai.
Karl Bielefeldt

Tôi nghĩ rằng câu trả lời của bạn là tốt. Không chắc chắn tại sao nó sẽ được bỏ phiếu xuống.
JimmyJames 4/11/2016
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.