AppSignal's company blog publishes tutorials on Elixir, Ruby on Rails, and JavaScript, with a focus on monitoring and performance.
HN links it for Elixir, Ruby, and Rails articles, such as using Gleam from Elixir and upcoming Rails 7.2 database features.
Inheritance is not the only way to share behavior across different implementations — it'a just the only way available in the traditional 1990s crop of static OOP languages like C++, Java and C#.
There are many other ways to share an implementation of a common feature:
1. Another comment already mentioned default method implementations in an interface (or a trait, since the example was in Rust). This technique is even available in Java (since Java 8), so it's as mainstream as it gets.
The main disadvantage is that you can have just one default implementation for the stop() method. With inheritance you could use hierarchies to create multiple shared implementations and choose which one your object should adopt by inheriting from it. You also cannot associate any member fields with the implementation. On the bright side, this technique still avoids all the issues with hierarchies and single and multiple inheritance.
2. Another technique is implementation delegation. This is basically just like using composition and manually forwarding all methods to the embedded implementer object, but the language has syntax sugar that does that for you. Kotlin is probably the most well-known language that supports this feature[1]. Object Pascal (at least in Delphi and Free Pascal) supports this feature as well[2].
This method is slightly more verbose than inheritance (you need to define a member and initialize it). But unlike inheritance, it doesn't requires forwarding the class's…
> We spend our money wisely. The cost of cloud is grotesque in several key instances. Whether it's big-iron databases, large NVMe storage, or just the latest, fastest compute, we're paying as much to rent the donkey every few months as it would cost to buy it. When that's the case, you should own the donkey! We will spend our money on our own hardware and our own people. Everything else will get the squeeze.
Yet he continues to use Ruby for everything and finds the most asinine justifications and complicated workarounds like Russian doll caching (https://blog.appsignal.com/2018/04/03/russian-doll-caching-in-rails.html, https://signalvnoise.com/posts/3690-the-performance-impact-of-russian-doll-caching) to make a simple page/app performant.
At the end of the day DHH is a person who values his own opinion more than anything else and he will go to lengths to confirm his own bias and justify his ways by whatever argumentation he can scramble together to look less foolish.
It goes without saying that there are a lot of things one would do prior to some of these steps. Because of the complexity these sort of configuration bring into your app one should weigh carefully whether they need these approaches. Database tuning and caching are preferable first steps in an app faced with performance challenges before fancy database routing. As with caching the app awareness of these approaches leaks into your code and can introduce testing challenges and misses in your process.
Maybe I missed it but I’m surprised they didn’t mention Multi-Tenancy with Multi-Database since they have otherwise written about it*. For many apps this is not only a way to keep data isolated but is also a way to mitigate performance challenges that might come with a single large DB.
https://blog.appsignal.com/2020/12/02/building-a-multi-tenant-ruby-on-rails-app-with-subdomains.html
As I said, claiming that ruby doesn't have first-class functions isn't "meaningfully true". Ruby does not have functions, only methods. But that's mostly irrelevant.
https://en.m.wikipedia.org/wiki/First-class_function#Language_support
> The identifier of a regular "function" in Ruby (which is really a method) cannot be used as a value or passed. It must first be retrieved into a Method or Proc object to be used as first-class data. The syntax for calling such a function object differs from calling regular methods.
https://blog.appsignal.com/2018/09/04/ruby-magic-closures-in-ruby-blocks-procs-and-lambdas.html
> Ruby doesn’t have first-class functions, but it does have closures in the form of blocks, procs and lambdas. Blocks are used for passing blocks of code to methods, and procs and lambda’s allow storing blocks of code in variables.
Off the top of my head:
- Understand the event loop to avoid blocking
- Understand the JIT compiler to write compiler optimization-friendly code
- With Typescript: speed up build and dev times using the SWC compiler, understand which types take more compilation time
- Minimize heap allocations to avoid memory leaks: https://blog.appsignal.com/2022/09/28/minimize-heap-allocations-in-nodejs.html
- Write native modules when you need CPU intensive operations (the official doc is quite good: https://nodejs.org/api/addons.html)
- Take care of node_modules (e.g. https://mainmatter.com/blog/2022/09/29/pnpm/)
- With Docker: optimize docker image to only include what you need. E.g. use Alpine if you don't have any particular external dependencies
I've read that and tried it. I hate it.
This recent article has some alternatives https://blog.appsignal.com/2023/05/10/organize-business-logic-in-your-ruby-on-rails-application.html
Their main argument against the first piece is:
> One problem with fat models is that unless you are really disciplined in your code hygiene, they can tend towards an assortment of code smells
And I wholeheartedly agree. With the addition that "code hygiene" if not enforced or encouraged, is always one of the first victims under stress. And every (successful) project will see such stress.
With Rails you have to spend extra effort to keep doing "the right thing", whereas is should cost cost less effort to do that, and extra effort to "do the bad thing".