This popular answer on Stack Overflow has this to say about the difference between functional programming and object-oriented programming:
Object-oriented languages are good when you have a fixed set of operations on things, and as your code evolves, you primarily add new things. This can be accomplished by adding new classes which implement existing methods, and the existing classes are left alone.
Functional languages are good when you have a fixed set of things, and as your code evolves, you primarily add new operations on existing things. This can be accomplished by adding new functions which compute with existing data types, and the existing functions are left alone.
Say I have an Animal
interface:
public interface Animal {
public void speak();
}
And I have a Dog
, Cat
, Fish
, and Bird
that all implement the interface. If I want to add a new method to Animal
named jump()
, I would have to go through all of my subclasses and implement jump()
.
The visitor pattern can alleviate this problem, but it seems that with the new functional features introduced in Java 8 we should be able to solve this problem in a different manner. In scala
I could easily just use pattern matching, but Java doesn't really have that yet.
Does Java 8 actually make it any easier to add new operations on existing things?
Lambda expressions can make it easier to set up (very) poor man's pattern matching. Same technique can be used to make a Visitor easier to build.
The additions made to the Java language do not render every old concept outdated. In fact, the Visitor pattern is very good at supporting adding of new operations.
When comparing this pattern with the new Java 8 possibilities, the following becomes apparent:
Iterable.forEach
,Stream.forEach
but alsoStream.reduce
So the new Java 8 features can never act as a drop-in replacement for the Visitor pattern, however, searching for possible synergies is reasonable. This answer discusses possibilities to retrofit an existing API (
FileVisitor
) to enable the use of lambda expressions. The solution is a specialized concrete visitor implementation which delegates to corresponding functions which can be specified for eachvisit
method. If each function is optional (i.e. there is a reasonable default for eachvisit
method), it will come handy if the application is interested in a small subset of the possible actions only or if it wants to treat most of them uniformly.If some of these use cases are regarded “typical”, there might be an
accept
method taking one or more functions creating the appropriate delegating visitor behind the scene (when designing new APIs or improving API under your control). I wouldn’t drop the ordinaryaccept(XyzVisitor)
, however, as the option to use an existing implementation of a visitor should not be underestimated.There’s a similar choice of overloads in the
Stream
API, if we consider aCollector
as a kind of visitor for aStream
. It consists of up to four functions, which is the maximum imaginable for visiting a flat, homogeneous sequence of items. Instead of having to implement that interface, you can initiate a reduction specifying a single function or a mutable reduction using three functions but there are common situations where specifying an existing implementation is more concise, like withcollect(Collectors.toList())
orcollect(Collectors.joining(","))
, than specifying all necessary functions via lambda expressions/ method references.When adding such support to a particular application of the Visitor pattern, it will make the calling site more shiny while the implementation site of the particular
accept
methods always has been simple. So the only part which remains bulky is the visitor type itself; it may even become a bit more complicated when it is augmented with support for functional interface based operations. It is unlikely that there will be a language-based solution for either, simpler creation of such visitors or replacing this concept, in the near future.What you're trying to accomplish, while admirable, isn't a good fit for Java in most cases. But before I get into that...
Java 8 adds default methods to interfaces! You can define default methods based on other methods in the interface. This was already available to abstract classes.
But in the end, you're going to have to provide functionality for each type. I don't see a huge difference between adding a new method and creating a visitor class or partial functions. It's just a question of location. Do you want to organize your code by action or object? (functional or object oriented, verb or noun, etc.)
I suppose the point I'm trying to make is that Java code is organized by 'noun' for reasons that aren't changing any time soon.
The visitor pattern along with static methods are probably your best bet for organizing things by action. However, I think visitors make the most sense when they don't really depend on the exact type of the object they're visiting. For instance, an Animal visitor might be used to make the animal speak and then jump, because both of those things are supported by all animals. A jump visitor doesn't make as much sense to me because that behavior is inherently specific to each animal.
Java makes a true "verb" approach a little difficult because it chooses which overloaded method to run based on the compile time type of the arguments (see below and Overloaded method selection based on the parameter's real type). Methods are only dynamically dispatched based on the type of
this
. That's one of the reasons inheritance is the preferred method of handling these types of situations.You can get around this by using
instanceof
and other forms of reflection.But now you're just doing work the JVM was designed to do for you.
Java has a few tools that make adding methods to a broad set of classes easier. Namely abstract classes and default interface methods. Java is focused on dispatching methods based on the object invoking the method. If you want to write flexible and performant Java, I think this is one idiom you have to adopt.
P.S. Because I'm That Guy™ I'm going to bring up Lisp, specifically the Common Lisp Object System (CLOS). It provides multimethods that dispatch based on all arguments. The book Practical Common Lisp even provides an example of how this differs from Java.