September 9th, 2024

Design Patterns Are Temporary, Language Features Are Forever

The article examines programming paradigms, emphasizing that modern features in languages like Java 21 can render certain design patterns obsolete, enhancing code readability and maintainability while simplifying problem-solving.

Read original articleLink Icon
Design Patterns Are Temporary, Language Features Are Forever

The article discusses the evolution of programming paradigms, particularly focusing on design patterns and language features. It highlights the temporary nature of design patterns, such as the visitor pattern, which can become obsolete with advancements in language features. The author reflects on their experience with design patterns in Java, noting that while they can simplify certain problems, they can also lead to unnecessary complexity if overused. The introduction of modern features in Java, particularly pattern matching and sealed types in Java 21, is presented as a significant improvement that makes the visitor pattern less relevant. The author shares their journey of learning Rust and discovering pattern matching, which provided a clearer understanding of the visitor pattern. They illustrate this with code examples, comparing the traditional visitor pattern implementation with a more modern approach using Java's new features. The conclusion emphasizes the benefits of modern language features that enhance code readability and maintainability, suggesting that as languages evolve, certain design patterns may become less necessary.

- Design patterns can simplify problem-solving but may lead to complexity if overused.

- Modern language features, like pattern matching in Java 21, can render certain design patterns obsolete.

- The author shares personal experiences with learning Rust and understanding pattern matching.

- Code examples illustrate the differences between traditional and modern implementations.

- The evolution of programming languages enhances code readability and maintainability.

Link Icon 20 comments
By @dgreensp - 3 months
You don’t come across visitors every day, and they aren’t something to reach for instead of a simple switch, but if you are writing a Webpack or ESLint plugin or something like that, you might encounter them for traversing an AST. It’s not just an OO thing, either.

I learned the visitor pattern in pretty ideal circumstances: from a classmate, back in college, when we were writing a compiler together for a class project, and it was just what we needed.

The right conditions for a visitor are when you have a data structure with many different “node” types, and the traversal logic is not simple. In addition, you will be doing many different traversals. You might also be doing transformations on the structure. You might have a library running the traversals/transformations provided by the code that uses the library. There could be correctness or memory management considerations involved that you don’t want the client to have to worry about. You might want to “pipeline” multiple transformations, doing several manipulations in one traversal, in an efficient way. Common traversals might care about only certain types of nodes, like a particular lint rule might just be looking at string literals or references to global variables, but it needs to walk the entire syntax tree of a file of source code.

By @michielderhaeg - 3 months
This reminds me of when I was still in university. During our compilers course we used the "Modern Compiler Implementation in Java" book. Because I was really into FP at the time, I instead used the "Modern Compiler Implementation in ML" version of this book (which was the original, the Java and C versions were made to make it more accessible). We noticed during the course that my version was missing a whole chapter on visitors, likely because ML's pattern matching made this kind off trivial.

One of the other students made a similar remark, that software design patterns are sometimes failures of the language when they have no concise way of expressing these kinds of concepts.

By @smallnamespace - 3 months
I disagree with the main thrust of the article, that the Visitor pattern is primarily a primitive way to do pattern matching.

For one, the Visitor pattern (as written in the article) fails to fulfill a core feature of Rust pattern matching, which is having a compact language to describe when something matches based on values, as opposed to the concrete type.

For another, you could equally well say that if-else blocks or ternary statements are also primitive pattern matching, if you're willing to stretch things that far.

In my view, the core reason people reached for the Visitor pattern in the past is that old Java didn't have convenient lambdas (anonymous functions). Visitors let you create an interface that mimics functional map.

Newer versions of Java have made lambdas much more convenient, so there's less motivation to reach for the Visitor pattern. You also saw this development in C#, where delegates were a language feature that often obviated rolling your own Visitors.

By @082349872349872 - 3 months
I haven't looked at patterns since the GOF days, so maybe they grew closer to Alexander's as they matured, but the original ones were very much ways to speak about things so they'd be politically palatable in an everything-is-an-object world.

You just wanted some data? "Flyweight". You just wanted some functions? "Strategy". etc.

By @miningape - 3 months
I think the visitor mimics pattern matching, but it really behaves/feels different. I think the visitor is really closer to a bridge from OOP land into FP land.

In FP its difficult/impossible to add new types but simple to add new mappings/pipelines for those types.

In OOP its easy to add new types but difficult/impossible to add new mappings/pipelines for those types (try transforming a POJO without lombok @Builder and @Value).

The visitor pattern (an OOP concept) allows us to more easily add a new mapping/pipeline for a set of related types. Normally in OOP this would involve adding a new method to a base class, implementing these new "mappings" in each subclass. With the visitor instead you just implement the new visitor, this allows the logic for this particular "kind" of mapping (visitor) to be grouped together, rather than represented by a method on a base class.

By @vrnvu - 3 months
I always tell people who obsess over design patterns as clean code and good coding practices to remember that design patterns often indicate a lack of language features.

For example, are you using a Builder? Would you use the Builder pattern if the language had named variables in arguments?

My favorite reference on the topic is Peter Norvig's "Design Patterns in Dynamic Languages" (1996!) https://www.norvig.com/design-patterns/

By @segmondy - 3 months
Whenever design patterns come up, folks start talking about language features. First of all, it's about natural language to describe a way of doing something. If you don't have a language to describe a design, then folks could and often would implement incompatible pieces. By having a common language, you solve for that. If I gave you two piece of wood and asked you to join them together. How would you? Does it matter how you join them? It absolute does matter. You might use nail, you might use screw, you might do glue. In carpentry there are design pattern for joints. You can tell someone to use a butt joint, a box joint, a pocket-hole joint, etc. Without these higher level language of design. You will have to explain and you might find out that explanation is not enough, you would need diagrams or models. The same with programming design patterns, without it, you will waste time explaining or drawing diagram. For the professional, these are the colloquial phrases of programming. If you're an enterprise application developer, or a game programmer or a cloud dev, your languages and often used patterns would vary. I say this as someone that learned lisp as a 2nd language a long time and a fan of functional languages. DPs are worth knowing and understanding.
By @svieira - 3 months
Anyone who is interested in pattern matching and the visitor pattern (and the benefits of various encodings of state and behavior) who hasn't seen them should check out:

* Li Haoyi's Zero-Overhead Tree Processing with the Visitor Pattern (https://www.lihaoyi.com/post/ZeroOverheadTreeProcessingwitht...)

* Noel Welsh's Uniting Church and State: FP and OO Together (https://noelwelsh.com/posts/uniting-church-and-state/ or https://www.youtube.com/watch?v=IO5MD62dQbI is you like video)

* Bruno C. d. S. Oliveira and William R. Cook's Extensibility for the Masses Practical Extensibility with Object Algebras (https://www.cs.utexas.edu/~wcook/Drafts/2012/ecoop2012.pdf)

By @syncsynchalt - 3 months
The author took the thesis in one direction, but there's another interesting way to think of it.

C++ is an epitaph for every discarded concept in OO design. Diamond inheritance, generics to a fault, operator overloading to a fault, the richness of STL types. Those "patterns" are dead, but the language features must continue to be supported.

By @kazinator - 3 months
Language features are not forever.

Language features can be deprecated as obsolescent and after a period of obsolescence, removed.

"implicit int" declarations are no longer a C language feature. The (void) parameter list is now obsolescent; as of C23, () means the same thing as (void), like in C++.

Design patterns can end up in libraries. But library and language features are the same. A popular library which everyone uses cannot be thrown out overnight any more than a language feature.

By @bad_user - 3 months
> Now knowing about pattern matching, this is how I came to realise that the visitor pattern was pattern matching in an OO way.

The programming language needs to be very expressive to replace the visitor pattern with pattern matching. For example, you need GADTs. The cool thing about static languages with OOP is that OOP can hide a lot of type-level complexity. Also, in languages with runtimes optimized for virtual calls (e.g., the JVM, V8), pattern matching can have less performance than the visitor pattern, despite pattern matching now being a reality on the JVM at least (e.g., Java, Scala have pattern matching).

The difference between pattern matching and the visitor pattern is the difference between tagless-initial and tagless-final encodings, or data versus Church-encodings. And as a software engineer, it's good to be aware of their strengths and weaknesses.

People making this claim (that design patterns are just missing language features) always mention Visitor, but stop short of mentioning other design patterns, such as Observable/Listener, but also, the even less obvious: Monad or the Free Monad (i.e., still a design pattern, despite being able to comfortably express it in your language).

By @Robin_Message - 3 months
The visitor pattern is a way of getting multiple dispatch in a language with single dispatch.

It can also be seen as a way of encoding a functional solution to the expression problem in an OO language, which is useful when you have a set of objects and want to make it easy to add new operations to them, not create new types of objects.

By @gorkempacaci - 3 months
If I may nitpick, the call to children’s accept methods should be in the visitor, not the parent. Imagine you’re writing an XMLGeneratorVisiter. The visit method for the parent would print <parent>, call child accepts, and then print </parent>. If you do it the way it is done here you lose the control on when the children are visited.

Also, the point of the visitor pattern is not just pattern matching/polymorphism. Of course you could do it with polymorphism or conditionals or whatever. But the visitor pattern reduces coupling and increases cohesion. So you get a more maintainable, testable code base. Pattern matching with a candied switch doesn’t give you that.

By @mhh__ - 3 months
I've seen it said that Alexander working on this stuff just at the moment of peak OOP-timism is a great shame/tragedy. I'm inclined to agree.

Functional purists like to glibly say that their patterns are discovered rather than invented. I used to Pooh Pooh this a bit. Then I had to write a class just to have a function to call. They're right.

By @Mikhail_Edoshin - 3 months
Design patterns are the real language though. A feature of a real language is that you can present a new concept. For example, a promise in an async programming is such a concept and it is not tied to a particular notation. Once you get that concept working in a notation that does not have it, then it can be fused into a notation, but then it becomes sort of frozen and ceases to evolve.
By @digging - 3 months
I got about halfway before completely losing the thread as I had no idea what was being said, unfortunately.

Is "visitor" pattern ever defined in this article?

Is there anything in this article explaining the assertion "Design Patterns Are Temporary, Language Features Are Forever"? I couldn't penetrate the specific example being discussed.

By @weinzierl - 3 months
Unfortunately, I can't remember where I heard that bon mot: "Every design pattern is a bug report against your compiler"
By @andybak - 3 months
> because that's a major L

What's "L"? Am I getting old?

By @estebarb - 3 months
I have a bug/hate relationship with the visitor pattern: how do I implement it without causing a stack overflow in languages without tail recursion optimization? It always ends up in a loop with a lot of ifs inside.