Reputation: 8292
I'm using kind of dynamic form system coming from the backend. To be able to map my form I have a visitor pattern with generics, I have it working in Java but I can't make it to work in Kotlin.
I have this interface:
internal interface FormFieldAccessor<T> {
fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<T>
fun setValueToBuilder(builder: Builder, value: T)
fun accept(visitor: FormFieldVisitor)
fun getValue(personalInfo: PersonalInfo): T
}
Then I have my list of accessors like this:
val accessors = mutableMapOf<String, FormFieldAccessor<*>>()
accessors[FIRST_NAME] = object : FormFieldAccessor<String> {
override fun getValue(personalInfo: PersonalInfo): String {
return personalInfo.surname
}
override fun accept(visitor: FormFieldVisitor) {
visitor.visitString(this)
}
override fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<String> {
//not relevant
}
override fun setValueToBuilder(builder: Builder, value: String) {
builder.withSurname(value)
}
}
//more other accessors with different type like Int or Boolean
And want to use it like this:
accessors[FIRST_NAME]!!.setValueToBuilder(builder, field.value )
But this is not working and give me:
Out-projected type 'FormFieldAccessor<*>' prohibits the use of 'public abstract fun setValueToBuilder(builder: Builder, value: T): Unit defined in FormFieldAccessor'
If you have an idea of what I'm doing wrong would be cool :)
EDIT: here is a smaller gist of the structure I have https://gist.github.com/jaumard/1fd1ccc9db0374cb5d08f047414a6bc8
I don't want to loose the type by using Any, feel frustrated compare to Java as it's really easy to implement. I understand the problem with the star projection now but is there anything else than this to achieve the same as in java ?
Upvotes: 3
Views: 2584
Reputation: 559
In this case you can add a wrapper method that takes Any as a value and checks its type
internal interface FormFieldAccessor<T> {
fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<T>
fun _setValueToBuilder(builder: Builder, value: T)
fun setValueToBuilder(builder: Builder, value: Any){
val value = value as? T ?: return
_setValueToBuilder(builder, value)
}
fun accept(visitor: FormFieldVisitor)
fun getValue(personalInfo: PersonalInfo): T
}
Upvotes: 0
Reputation: 170795
@s1m0nw1's answer gives you the reason for the problem and the simple fix. However, with your setup there may also be another possibility.
Add
fun setValueFromForm(builder: Builder, fieldDefinition: FormFieldDefinition) {
setValueToBuilder(builder, getFormField(fieldDefinition).value)
}
to FormFieldAccessor<T>
. Because its signature doesn't involve T
, it can be safely called on a FormFieldAccessor<*>
.
And if you also store FormFieldDefinition
s in a map, you can now call it
accessors[FIRST_NAME]!!.setValueToBuilder(builder, fieldDefinitions[FIRST_NAME]!!)
Further improvements would depend on details of your system.
EDIT:
I'm reasonably certain even without seeing the Java code that it uses raw types (i.e. FormFieldAccessor
instead of FormFieldAccessor<Something>
or FormFieldAccessor<?>
). Something like
Map<String, FormFieldAccessor> accessors = new HashMap<>();
...
accessors.get(FIRST_NAME).setValueToBuilder(builder, field.value);
But this is also unsafe, the compiler just ignores the problems instead of telling you about them. The value of accessors.get(FIRST_NAME)
can still actually be e.g. an FormFieldAccessor<Boolean>
, in which case setValueToBuilder
will fail with a ClassCastException
. You can see this by accidentally passing the wrong name to get
or storing the wrong accessor in the map: it won't stop anything from compiling.
Instead, better Java code would use Map<String, FormFieldAccessor<?>>
and then require a cast after get
just as Kotlin code does.
Raw types exist primarily to allow very old pre-Java-5 code to still compile. Kotlin doesn't have this consideration, so it doesn't support raw types and your only option there is to do what you should do in Java.
Upvotes: 0
Reputation: 81989
Using star-projection indicates that you know nothing about the actual type, as the documentation tells:
Sometimes you want to say that you know nothing about the type argument, but still want to use it in a safe way. The safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type would be a subtype of that projection.
[...]
For
Foo<out T : TUpper>
, whereT
is a covariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
. It means that when theT
is unknown you can safely read values ofTUpper
fromFoo<*>
.
What you can do is casting to the appropriate type:
(accessors[FIRST_NAME] as FormFieldAccessor<String>).setValueToBuilder(builder, field.value)
Yet, these types of casts are error-prone and a safer way would be the following;
object FormFieldProvider {
private val accessors = mutableMapOf<String, FormFieldAccessor<*>>()
fun <T : Any> addAccessor(key: String, fieldValidator: FormFieldAccessor<T>) {
accessors[key] = fieldValidator
}
@Suppress("UNCHECKED_CAST")
operator fun <T : Any> get(key: String): FormFieldAccessor<T> =
accessors[key] as? FormFieldAccessor<T>
?: throw IllegalArgumentException(
"No accessor found for $key")
}
The access to the star-projected map got wrapped in an object and accessing the values is safe with this solution.
You can use it like this:
FormFieldProvider.addAccessor(FIRST_NAME, object : FormFieldAccessor<String> {
//...
})
FormFieldProvider.get<String>(FIRST_NAME).setValueToBuilder(...)
Upvotes: 1