This seems to be a very common question, although it has a very easy answer, in my opinion.
For me, it's about how you see the contract that you design and how you expect it to be implemented and used.
As you note, your example shows a bad way of using functional interfaces. Designing these types just to end up calling if(dt.test(d))
instead of if(d.isEsMamifero())
takes a simple adjective: bad. And this is probably imputable to text books. The problem is most books teach us to use functional interfaces the way we're taught to use interfaces/abstraction in general, and that omits the point and bigger picture of functional programming. Of course one needs to know "how" to implement a functional interface (it's an interface after all), but many books don't tell us where to apply them.
Here's how I explain it to myself (in very basic terms):
1 - See a functional interface as "named logic" (to be implemented on the other side of the contract)
Yes, a functional interface is a type, but it makes more sense to look at a functional interface as named logic. Unlike ordinary types like Serializable
, Collection
, AutoCloseable
, functional intefaces like Tester
(or Predicate
) represent logic (or just code). I know the nuances are are getting subtle, but I believe there's a difference between the traditional OOP abstract "type" and what a functional interface is meant to be.
2 - Isolate the code that implements functional interfaces from the code that consumes it
The problem with using the two in the same component is made obvious in your code. You wouldn't write a functional interface, declare a method that takes one, all just to implement it and pass it to your own method. If you're doing this and only this, you're using abstraction for the wrong reasons, let alone use functional interfaces correctly.
There are tons of examples of proper use of functional interfaces. I'll pick Collection.forEach
with Consumer
:
Collection<String> strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> System.out.println(s));
How does this differ from your design?
- The designer of
Collection.forEach
stops at the contract that takes a Consumer
(they don't use a Consumer
just to name/type a parameter to their own method
Collection.forEach
is an operation that needs customized logic. Just as with
s -> System.out.println(s)
,
this "custom logic" can be
s -> myList.add(s)
or
s -> myList.add(s.toUpperCase())
,
etc., all as per the designer of the client code (long after the interface was designed). The Collection
interface's forEach
method orchestrates iteration and allows the caller to supply the logic invoked in each iteration. That separates concerns.
Stream.filter
with Predicate
is closer to your example, it will be a good idea to contrast how you use Stream.filter
to how you used Tester.test
in your example.
With that said, there are many reasons for/against functional programming, the above focused on the reason for using (or not using) functional interfaces as per your example (specifically, from the perspective of the developer who writes the contract).