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 articleThe 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.
Related
I Probably Hate Writing Code in Your Favorite Language
The author critiques popular programming languages like Python and Java, favoring Elixir and Haskell for immutability and functional programming benefits. They emphasize personal language preferences for hobby projects, not sparking conflict.
Objective-C is like Jimi Hendrix (2014)
The author compares Jimi Hendrix's influence on guitar music to Objective-C's impact on programming, noting how perceptions of its features have evolved as newer languages emerged, reflecting on programming history.
C Isn't a Programming Language Anymore (2022)
The article examines the shift in perception of C from a programming language to a protocol, highlighting challenges it poses for interoperability with modern languages like Rust and Swift.
Don't write Rust like it's Java
The author discusses transitioning from Java to Rust, highlighting Rust's type safety, differences in traits and interfaces, complexities of ownership, and the importance of embracing Rust's unique features for effective programming.
From Julia to Rust
The article outlines the author's transition from Julia to Rust, highlighting Rust's memory safety features, design philosophies, and providing resources for learning, while comparing code examples to illustrate syntax differences.
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.
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.
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.
You just wanted some data? "Flyweight". You just wanted some functions? "Strategy". etc.
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.
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/
* 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)
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.
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.
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).
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.
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.
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.
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.
What's "L"? Am I getting old?
Related
I Probably Hate Writing Code in Your Favorite Language
The author critiques popular programming languages like Python and Java, favoring Elixir and Haskell for immutability and functional programming benefits. They emphasize personal language preferences for hobby projects, not sparking conflict.
Objective-C is like Jimi Hendrix (2014)
The author compares Jimi Hendrix's influence on guitar music to Objective-C's impact on programming, noting how perceptions of its features have evolved as newer languages emerged, reflecting on programming history.
C Isn't a Programming Language Anymore (2022)
The article examines the shift in perception of C from a programming language to a protocol, highlighting challenges it poses for interoperability with modern languages like Rust and Swift.
Don't write Rust like it's Java
The author discusses transitioning from Java to Rust, highlighting Rust's type safety, differences in traits and interfaces, complexities of ownership, and the importance of embracing Rust's unique features for effective programming.
From Julia to Rust
The article outlines the author's transition from Julia to Rust, highlighting Rust's memory safety features, design philosophies, and providing resources for learning, while comparing code examples to illustrate syntax differences.