3 ms·
The biggest downfall in my experience has been it can be a massive pain to find out where a method is defined in a huge codebase, especially with all the crazy
by qq99 1y ago
The biggest downfall in my experience has been it can be a massive pain to find out where a method is defined in a huge codebase, especially with all the crazy ways in which one can declare methods. You can spend a non-trivial amount of time just trying to find the definition for a method.
- snkzxbs 1y agoSorry if this sounds like a stupid question but - is there no "Go to definition" command in an IDE that can help with something like this? I mean, I understand that there is, but it doesn't work well with Ruby. Why?
- azuanrb 1y agoMainly because of the dynamically typed nature of the language. Not limited to Ruby/Rails. My colleagues used RubyMine because of this. I'm using Neovim with LSP, it's ok but nowhere near Go for example.
- Rhainur 1y agoOther people have mentioned "dynamic typing" as being the reason for this, but that's not actually true. The real reason is two Ruby features: `define_method` and `method_missing`. If you have a class `Customer` with a field `roles` that is an array of strings, you can write code like this class Customer ROLES = ["superadmin", "admin", "user"] ROLES.each do |role| define_method("is_#{role}?") do roles.include?(role) end end end In this case, I am dynamically defining 3 methods `is_superadmin?` `is_admin?` and `is_user?`. This code runs when the class is loaded by the Ruby interpreter. If you were just freshly introduced into this codebase, and you saw code using the `is_superadmin?` method, you would have no way of knowing where it's defined by simply grepping. You'd have to really dig into the code - which could be more complicated by the fact that this might not even be happening in the Customer class. It could happen in a module that the Customer class includes/extends. The other feature is `method_missing`. Here's the same result achieved by using that instead of define_method: class Customer ROLES = ["superadmin", "admin", "user"] def method_missing(method_name, *args) if method_name.to_s =~ /^is_(\w+)\?$/ && ROLES.include?($1) roles.include?($1) else super end end end Now what's happening is that if you try to call a method that isn't explicitly defined using `def` or the other `define_method` approach, then as a last resort before raising an error, Ruby checks "method_missing" - you can write code there to handle the situation. These 2 features combined with modules are the reason why "Go to Definition" can be so tricky. Personally, I avoid both define_method and method_missing in my actual code since they're almost never worth the tech debt. I have been developing in Rails happily for 15+ years and only had one or two occasions where I felt they were justified and the best approach, and that code was heavily sprinkled with comments and documentation.
- t-writescode 1y agoTo add, the above code is a pretty near approximation of the literal code inside the devise codebase, which is a very standard Ruby auth system. See here: https://github.com/heartcombo/devise/blob/main/lib/devise/controllers/helpers.rb#L115 https://github.com/heartcombo/devise/blob/main/lib/devise/co... def self.define_helpers(mapping) #:nodoc: mapping = mapping.name class_eval <<-METHODS, __FILE__, __LINE__ + 1 def authenticate_#{mapping}!(opts = {}) That code is *literally* calling class_eval with a multi-line string parameter, where it inlines the helper name (like admin, user, whatever), to grow the class at runtime. It hurts my soul.
- snkzxbs 1y agoThank you both for the time you spent explaining this.
- chris12321 1y agoIt's been widely understood in the Ruby community for some time now that metaprogramming—like in the example above—should generally be limited to framework or library code, and avoided in regular application code. Dynamically generated methods can provide amazing DX when used appropriately. A classic example from Rails is belongs_to, which dynamically defines methods based on the arguments provided: class Post < ApplicationRecord belongs_to :user end This generates methods like: post.user - retrieves the associated user post.user=(user) - sets the associated user post.user_changed? - returns true if the user foreign key has changed.
- t-writescode 1y agoAren’t all these enhancement methods that are added dynamically to every ActiveRecord a major reason why regular AR calls are painfully slow and it’s better to use .pluck() instead? One builds a whole object from pieces, the other vomits put an array?
- chris12321 1y ago