I was reading the docs and spotted something odd about ClassBuilder
. Here’s the example:
builder = Dry::Core::ClassBuilder.new(name: 'User', namespace: Entities)
klass = builder.call
klass.name # => "Entities::User"
klass.superclass.name # => "Entities::User"
Entities::User # => "Entities::User"
klass.superclass == Entities::User # => true
It looks like the builder generates 2 classes. Both have the same name Entities::User
. And one inherits from the other.
I looked in the code and it appears to be the case. Here’s the relevant bit:
def create_named
name = self.name
base = create_base(namespace, name, parent)
klass = ::Class.new(base)
namespace.module_eval do
remove_const(name)
const_set(name, klass)
remove_const(name)
const_set(name, base)
end
klass
end
It first puts the generated class into the namespace under the expected name (to name the class, I presume). Then removes it and puts the base class under the same name.
So now, unless we hold on to the generated class, we can’t get to it by name. And if we try to get it by name, we actually get the base class.
I find this very confusing. Can someone please explain why the generated class has the same name as the base class? More so that it can’t be looked up by that name.
It’s used in rom/rom-sql for building structs that hold data fetched from the persistence layer. The structs are anonymous classes that are technically different, but they must have a common base class where a user can put their methods. See ROM - Structs. We cannot auto-generate meaningful names for subclasses, and, until recently, Ruby didn’t provide a way to assign a nice name to an anonymous class, so I had to use a not-so-well-known hack to work around it.
Now this whole thing can be refactored by using Bug #21094: Module#set_temporary_name does not affect a name of a nested module - Ruby - Ruby Issue Tracking System
I figured that it was used to name the generated classes. It’s just surprising and confusing that the name is reused.
Is there any reason why the classes have to have a name at all? I mean, other than nicer exception call stacks.
Actually, now I wonder if it’s even useful. Say, an exception is risen that references class name. But it points to a wrong thing. E.g. “klass.new.non_method” would raise a NoMethodError
error with a message like "undefined method ‘explode’ for an instance of Entities::User. And indeed the method is not defined in that class. But also if there’s some other error that points to somewhere in the code in the generated class but names the base class. Wouldn’t it make it harder to find the actual error?
I had my concerns as well when I wrote that code. It’s been more than five years, and it turned out that either no users were confused or there are no users at all.
It’s not quite like this; it points to the base class, and it is usually the base class involved (since child classes are auto-generated, there’s little room for error). So, practically speaking, it’s not that bad.
But the base class is empty. I mean, not the parent class, but the base class. All the customizations that can be specified in the block live in the returned generated class, not the saved base class.
Consider this example:
builder = Dry::Core::ClassBuilder.new(name: 'User', namespace: Entities)
customer_class = builder.call
admin_class = builder.call do |klass|
klass.include Sudo
end
Both customer_class
and admin_class
would report they’re Entities::User
. But they may behave differently and this difference would not be reflected in the base Entities::User
that can actually be referenced by name.
And this is the case with each new generated class. We get more and more different classes that all report that they’re some different class.
What I’m trying to say is that it’s doing my head in. I’d rather have them named something like "Generated#{name}#{base.descendants.length}"
or some such.
Please don’t take it personally. It’s not a dig at you. I’m just trying to understand the code and why it’s the way it is. I appreciate your replies.