I'm trying to work on the principle of least surprise here...
Let's say you've got a method that accepts two objects. The method needs these to be object instances, but in the place where you initialize the class you may only have reference IDs. This would be common in a router / controller in a web service, for example. The setup might look something like this:
post "/:foo_id/add_bar/:bar_id" do
AddFooToBar.call(...)
end
There are many different ways that this could be solved. To 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 could call it like:
AddFooToBar.call(foo: a_foo, bar: a_bar)
AddFooToBar.call(foo_id: 1, bar_id: 2)
This creates a pretty clear interface, but the implementation is a little verbose, particularly if there are more than 2 objects and their names are longer than foo
and bar
.
You could use a good old fashioned hash instead...
def AddFooToBar.call(input={})
@foo = input[:foo] || Foo[ input[:foo_id] ]
@bar = input[:bar] || Bar[ input[:bar_id ]
end
The method signature is super simple now, but it loses a lot of clarity compared to what you get using keyword arguments.
You could just use a single key instead, 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, though it's a little weird to pass just an ID using the same argument name you'd pass an object instance to. The lookup in the method definition is also a little uglier and less easy to read.
You could just decide not to internalize this at all and require the caller to initialize instances before passing them in.
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 clear, but it means that every place that calls the method needs to know how to initialize the required objects first, rather than having the option to encapsulate that behavior in the method that needs the objects.
Lastly, you could do the inverse, and only allow object ids to be passed in, ensuring the objects will be looked up in the method. This may cause double lookups though, in case you sometimes have instances already existing that you want to pass in. It's also harder to test since you can't just inject a mock.
I feel like this is a pretty common issue in Ruby, particularly when building web services, but I haven't been able to find much writing about it. So my questions are:
Which of the above approaches (or something else) would you expect as more conventional Ruby? (POLS)
Are there any other gotchas or concerns around one of the approaches above that I didn't list which should influence which one works best, or experiences you've had that led you to choose one option over the others?
Thanks!
To pass an object as an argument we write the object name as the argument while calling the function the same way we do it for other variables. Syntax: function_name(object_name); Example: In this Example there is a class which has an integer variable 'a' and a function 'add' which takes an object as argument.
We can explicitly accept a block in a method by adding it as an argument using an ampersand parameter (usually called &block ). Since the block is now explicit, we can use the #call method directly on the resulting object instead of relying on yield .
The == operator, also known as equality or double equal, will return true if both objects are equal and false if they are not. The != operator, also known as inequality, is the opposite of ==. It will return true if both objects are not equal and false if they are equal.
I would go with allowing either the objects or the ids indistinctively. However, I would not do like you did:
def AddFooToBar.call(foo:,bar:)
@foo = foo.is_a?(Foo) ? foo : Foo[foo]
@bar = bar.is_a?(Bar) ? bar : Bar[foo]
end
In fact, I do not understand why you have Bar[foo]
and not Bar[bar]
. But besides this, I would put the conditions built-in within the []
method:
def Foo.[] arg
case arg
when Foo then arg
else ...what_you_originally_had...
end
end
Then, I would have the method in question to be defined like:
def AddFooToBar.call foo:, bar:
@foo, @bar = Foo[foo], Bar[bar]
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With