I might be asking too much, but Groovy seems super flexible, so here goes...
I would like a method in a class to be defined like so:
class Foo {
Boolean y = SomeOtherClass.DEFAULT_Y
Boolean z = SomeOtherClass.DEFAULT_Z
void bar(String x = SomeOtherClass.DEFAULT_X,
Integer y = this.y, Boolean z = this.z) {
// ...
}
}
And to be able to provide only certain arguments like so:
def f = new Foo(y: 16)
f.bar(z: true) // <-- This line throws groovy.lang.MissingMethodException!
I am trying to provide an API that is both flexible and type safe, which is the problem. The given code is not flexible in that I would have to pass in (and know as the user of the API) the default value for x
in order to call the method. Here are some challenges for the solution I want:
void bar(Map)
signatures unless the keys can somehow be made type safe. I realize with this I could do the type checking in the method body, but I'm trying to avoid that level of redundancy as I have many of this "kind" of method to write.I could use a class for each method signature--something like:
class BarArgs {
String x = SomeOtherClass.DEFAULT_X
String y
String z
}
And define it like:
void bar(BarArgs barArgs) {
// ...
}
And call it using my desired way using the map constructor: f.bar(z: true)
, but my problem lies in the object's default on y
. There's no way to handle that (that I know of) without having to specify it when calling the method as in: f.bar(y: f.y, z: true)
. This is fine for my little sample, but I'm looking at 20-30 optional parameters on some methods.
Any suggestions (or questions if needed) are welcome! Thank you for taking a look.
Interesting question. I've interpreted your requirements like this
I was not sure about number 5 since it is not explicitly specified, but it looked like that was what you wanted.
As far as I know, there is nothing built-in in groovy to support all this, but there are several ways to make it work in a "simple-to-use" manner.
One way that comes to mind is to create specialized argument classes, but only use maps as the arguments in the methods. With a simple super-class or trait to verify and set the properties, it is a one-liner to get the actual arguments for each method.
Here is a trait and some examples that can be used as a starting point:
trait DefaultArgs {
void setArgs(Map args, DefaultArgs defaultArgs) {
if (defaultArgs) {
setArgs(defaultArgs.toArgsMap())
}
setArgs(args)
}
void setArgs(Map args) {
MetaClass thisMetaClass = getMetaClass()
args.each { name, value ->
assert name instanceof String
MetaProperty metaProperty = thisMetaClass.getMetaProperty(name)
assert name && metaProperty != null
if (value != null) {
assert metaProperty.type.isAssignableFrom(value.class)
}
thisMetaClass.setProperty(this, name, value)
}
}
Map toArgsMap() {
def properties = getProperties()
properties.remove('class')
return properties
}
}
With this trait is it easy to create specialized argument classes.
@ToString(includePackage = false, includeNames = true)
class FooArgs implements DefaultArgs {
String a = 'a'
Boolean b = true
Integer i = 42
FooArgs(Map args = [:], DefaultArgs defaultArgs = null) {
setArgs(args, defaultArgs)
}
}
@ToString(includePackage = false, includeNames = true, includeSuper = true)
class BarArgs extends FooArgs {
Long l = 10
BarArgs(Map args = [:], FooArgs defaultArgs = null) {
setArgs(args, defaultArgs)
}
}
And a class that uses these arguments:
class Foo {
FooArgs defaultArgs
Foo(Map args = [:]) {
defaultArgs = new FooArgs(args)
}
void foo(Map args = [:]) {
FooArgs fooArgs = new FooArgs(args, defaultArgs)
println fooArgs
}
void bar(Map args = [:]) {
BarArgs barArgs = new BarArgs(args, defaultArgs)
println barArgs
}
}
Finally, a simple test script; output of method invocations in comments
def foo = new Foo()
foo.foo() // FooArgs(a:a, b:true, i:42)
foo.foo(a:'A') // FooArgs(a:A, b:true, i:42)
foo.bar() // BarArgs(l:10, super:FooArgs(a:a, b:true, i:42))
foo.bar(i:1000, a:'H') // BarArgs(l:10, super:FooArgs(a:H, b:true, i:1000))
foo.bar(l:50L) // BarArgs(l:50, super:FooArgs(a:a, b:true, i:42))
def foo2 = new Foo(i:16)
foo2.foo() // FooArgs(a:a, b:true, i:16)
foo2.foo(a:'A') // FooArgs(a:A, b:true, i:16)
foo2.bar() // BarArgs(l:10, super:FooArgs(a:a, b:true, i:16))
foo2.bar(i:1000, a:'H') // BarArgs(l:10, super:FooArgs(a:H, b:true, i:1000))
foo2.bar(l:50L) // BarArgs(l:50, super:FooArgs(a:a, b:true, i:16))
def verifyError(Class thrownClass, Closure closure) {
try {
closure()
assert "Expected thrown: $thrownClass" && false
} catch (Throwable e) {
assert e.class == thrownClass
}
}
// Test exceptions on wrong type
verifyError(PowerAssertionError) { foo.foo(a:5) }
verifyError(PowerAssertionError) { foo.foo(b:'true') }
verifyError(PowerAssertionError) { foo.bar(i:10L) } // long instead of integer
verifyError(PowerAssertionError) { foo.bar(l:10) } // integer instead of long
// Test exceptions on missing properties
verifyError(PowerAssertionError) { foo.foo(nonExisting: 'hello') }
verifyError(PowerAssertionError) { foo.bar(nonExisting: 'hello') }
verifyError(PowerAssertionError) { foo.foo(l: 50L) } // 'l' does not exist on foo
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