Registering Classes Instead of Instances

Hi

I’m trying to move my application to dry-system. I regiestred my models by “config.component_dirs.add” and when i try to call my models, i get instances of these classes, but there are methods in the classes that I use.

Is there clean way to register my models?

Yes, this is supported via the instance proc.

This was mentioned in the CHANGELOG for 0.23

config.component_dirs.add "app" do |dir|
  dir.instance = proc do |component|
    if component.identifier.include?("entities")
      component.loader.constant(component)
    else
      component.loader(component)
    end
  end
end

Another approach you can take is using the zeitwerk plugin so you can reference the constants directly.

3 Likes

I tried to implement the loading according to your example, but dir.instance never called. I’ve added puts to Proc and didn’t see any output in the console, my models loaded as instances :^( (I use the last version of dry-system)

UPD: Sorry, I didn’t expect that dir.instance calls when I try to get access to my component. It’s working. Thank you :^)

I don’t recommend using both class and instance methods. It defeats the purpose of using dry-system with DI as you end up with code coupled to class constants anyway.

@solnic WDYM with using both class and instance methods? You mean you don’t recommend using zeitwerk? Cause with zeitwerk your code will be coupled to class constants etc. But I do not see an issue with something like this:

App["serializers.user"].to_json(user)
# instead of Serialzers::User.to_json(user)

@ alassek Thanks, good point!

I literally meant what I wrote, relying on both class and instance methods creates coupling between class constants and code that uses them and mixes that with instance methods, which adds more complexity to the way a system is structured. When you use dry-system, it’s recommended to rely on instance methods exclusively, this has proven to be a great architectural pattern that reduces complexity of the code base and makes it more flexible and coherent.

Zeitwerk is an autoloader, what does this have to do with coupling to class constants?

@solnic Sorry that was a misunderstanding. Reading your answer I thought it was related to alassek’s answer and thought that you don’t recommend registering class constants. Now I realized that you only responded to the initial question.

Zeitwerk is an autoloader, what does this have to do with coupling to class constants?

Because all class constants are available through Zeitwerk, it’s tempting to bypass the DI. That’s what I meant, but forget it.

thanks and best regards.

Ah thanks I see the misunderstanding now too. I don’t recommend registering class constants when they are also used to create instances, because that’s the type of inconsistency that we’re trying to avoid when using dry-system. Relying on instances exclusively works much better.

Ah gotcha, yes so to clarify - when you use Zeitwerk and refer to class constants explicitly and bypass the DI provided by dry-system then you’re going to introduce tight coupling between your code and class constants. When you use Zeitwerk setup via dry-system just to have it auto-load constants (which of course also means having it load files too) but you still use DI and don’t refer to class constants explicitly, then there’s no such coupling.

@wuarmin This is sound advice and I think you should follow this rule of thumb in most cases.

I sometimes reference class constants in my code, but @solnic is correct here that it introduces coupling so take great care when doing this.

I’ll give a couple examples of why I do this. I occasionally write singeton helper methods on module namespaces that aren’t tied to a specific implementation. This is far less dangerous because namespaces change less often than implementations.

Here’s a concrete example: while my application code uses ROM repositories as the primary interface to the data layer, in many cases I need to just open a console and poke around to investigate. This leads to a really common pattern.

db = App["persistence.db"]
db[:table_name].where(simple: "query").all

rom = App["persistence.rom"]
rom[:relation_name].where(simple: "query").to_a

I organize my data layer in the DB namespace, with ROM Relations living in DB::Relation. So I wrote a couple simple helper methods.

DB[:table_name].where(simple: "query").all
DB::Relation[:relation_name].where(simple: "query").to_a

Under the hood, my helper methods are still using the container system so I still have the benefits of swapping implementation if necessary.

This is also useful for simple Rake tasks that don’t merit being implemented as a full-blown class.

Another common reason is pattern-matching, if I just need to know an operation succeeded and returned the type I expect. This is also not a serious case of coupling because I’m not depending on an interface in these cases, just identity. And search-replacing a specific constant name is very easy.

1 Like