Ruby semantics for accepting an object or its identifier as an argument

I try to work on the principle of least surprise here ...

Say you have a method that takes two objects. The method requires that these be instances of objects, but in the place where you initialize the class, you can only have link identifiers. For example, this would be common in a router / controller in a web service. The setup might look something like this:

post "/:foo_id/add_bar/:bar_id" do AddFooToBar.call(...) end 

There are many different ways to solve this problem. For me, the most β€œidomatic” here is something like this:

 def AddFooToBar.call(foo:nil,foo_id:nil,bar:nil,bar_id:nil) @foo = foo || Foo[foo_id] @bar = bar || Bar[bar_id] ... end 

Then, when you call the method, you can call it the following:

 AddFooToBar.call(foo: a_foo, bar: a_bar) AddFooToBar.call(foo_id: 1, bar_id: 2) 

This creates a fairly intuitive interface, but the implementation is a bit detailed, especially if there are more than two objects, and their names are longer than foo and bar .

Instead, you can use an old vintage hash ...

 def AddFooToBar.call(input={}) @foo = input[:foo] || Foo[ input[:foo_id] ] @bar = input[:bar] || Bar[ input[:bar_id ] end 

Now the method signature is super simple, but it loses much clarity compared to what you get using keyword arguments.

Instead, you can use only one key, especially if both inputs are required:

 def AddFooToBar.call(foo:,bar:) @foo = foo.is_a?(Foo) ? foo : Foo[foo] @bar = bar.is_a?(Bar) ? bar : Bar[bar] end 

The method signature is simple, although it’s a little strange to pass only the identifier using the same argument name to which you passed the instance of the object. A search in a method definition is also a little uglier and less readable.

You could simply decide not to do this at all, and require the caller to initialize the instances before transferring them.

 post "/:foo_id/add_bar/:bar_id" do foo = Foo[ params[:foo_id] ] bar = Bar[ params[:bar_id] ] AddFooToBar.call(foo: foo, bar: bar) end 

This is quite obvious, but it means that every place calling the method should know how to initialize the required objects first, and not be able to encapsulate this behavior in a method that needs objects.

Finally, you can invert and allow only object identifiers to be passed so that objects are viewed in the method. This can lead to a double search, although in case you have pre-existing instances that you want to transfer. This is also harder to verify, since you cannot just enter a layout.

It seems to me that this is a fairly common problem in Ruby, especially when creating web services, but I could not find many letters about this. So my questions are:

  • Which of the above approaches (or something else) do you expect as more ordinary Ruby? (Pols)

  • Are there any other problems or problems around one of the above approaches that I did not list that should influence what works best, or the experience that you had that led you to choose one option over others?

Thanks!

+7
ruby
source share
1 answer

I would go with the assumption that objects or identifiers are immodest. However, I would not do it like you:

 def AddFooToBar.call(foo:,bar:) @foo = foo.is_a?(Foo) ? foo : Foo[foo] @bar = bar.is_a?(Bar) ? bar : Bar[foo] end 

Actually, I don’t understand why you have Bar[foo] and not Bar[bar] . But besides this, I would put conditions built into the [] method:

 def Foo.[] arg case arg when Foo then arg else ...what_you_originally_had... end end 

Then I would like the method in question to be defined as follows:

 def AddFooToBar.call foo:, bar: @foo, @bar = Foo[foo], Bar[bar] end 
+1
source share

All Articles