Fabian Zeindl
Fabian Zeindl

Reputation: 5988

Union types / extension interfaces

I have several data class with fields, which are used in forms and need them to have a method return true if any of the fields has been filled.

I don't want to rewrite this for all the classes, so I'm doing it like this at the moment:

data class Order(var consumer: String, var pdfs: List<URI>): Form  {

    override val isEmpty(): Boolean
         get() = checkEmpty(consumer, pdfs)
}

data class SomethingElse(var str: String, var set: Set<String>): Form  {

    override val isEmpty(): Boolean
         get() = checkEmpty(str, set)
}


interface Form {
    val isEmpty: Boolean

    fun <T> checkEmpty(vararg fields: T): Boolean {
        for (f in fields) {
            when (f) {
                is Collection<*> -> if (!f.isEmpty()) return false
                is CharSequence -> if (!f.isBlank()) return false
            }
        }
        return true;
    }
}

This is obviously not very pretty nor type-safe.

What's a more idiomatic way of doing this, without abstracting every property into some kind of Field-type?

Clarification: What I'm looking for is a way to get exhaustive when, for example by providing all the allowed types (String, Int, List, Set) and a function for each to tell if they're empty. Like an "extension-interface" with a method isEmptyFormField.

Upvotes: 3

Views: 103

Answers (3)

mfulton26
mfulton26

Reputation: 31254

If all you are doing is checking for isEmpty/isBlank/isZero/etc. then you probably don't need a generic checkEmpty function, etc.:

data class Order(var consumer: String, var pdfs: List<URI>) : Form {
    override val isEmpty: Boolean
        get() = consumer.isEmpty() && pdfs.isEmpty()
}

data class SomethingElse(var str: String, var set: Set<String>) : Form {
    override val isEmpty: Boolean
        get() = str.isEmpty() && set.isEmpty()
}

interface Form {
    val isEmpty: Boolean
}

However, if you are actually do something a bit more complex then based on your added clarification I believe that "abstracting every property into some kind of Field-type" is exactly what you want just don't make the Field instances part of each data class but instead create a list of them when needed:

data class Order(var consumer: String, var pdfs: List<URI>) : Form {
    override val fields: List<Field<*>>
        get() = listOf(consumer.toField(), pdfs.toField())
}

data class SomethingElse(var str: String, var set: Set<String>) : Form {
    override val fields: List<Field<*>>
        get() = listOf(str.toField(), set.toField())
}

interface Form {
    val isEmpty: Boolean
        get() = fields.all(Field<*>::isEmpty)

    val fields: List<Field<*>>
}

fun String.toField(): Field<String> = StringField(this)
fun <C : Collection<*>> C.toField(): Field<C> = CollectionField(this)

interface Field<out T> {
    val value: T
    val isEmpty: Boolean
}

data class StringField(override val value: String) : Field<String> {
    override val isEmpty: Boolean
        get() = value.isEmpty()
}

data class CollectionField<out C : Collection<*>>(override val value: C) : Field<C> {
    override val isEmpty: Boolean
        get() = value.isEmpty()
}

This gives you type-safety without changing your data class components, etc. and allows you to "get exhaustive when".

Upvotes: 2

mfulton26
mfulton26

Reputation: 31254

You can use null to mean "unspecified":

data class Order(var consumer: String?, var pdfs: List<URI>?) : Form {
    override val isEmpty: Boolean
        get() = checkEmpty(consumer, pdfs)
}

data class SomethingElse(var str: String?, var set: Set<String>?) : Form {
    override val isEmpty: Boolean
        get() = checkEmpty(str, set)
}

interface Form {
    val isEmpty: Boolean
    fun <T> checkEmpty(vararg fields: T): Boolean = fields.all { field -> field == null }
}

The idea here is the same as that of an Optional<T> in Java but without the extra object, etc.

You now have to worry about null safety but if your fields are meant to have a concept of absent/empty then this seems appropriate (UsingAndAvoidingNullExplained · google/guava Wiki).

Upvotes: 1

rafal
rafal

Reputation: 3250

It's kinda hacky but should work. Every data class creates set of method per each constructor parameters. They're called componentN() (where N is number starting from 1 indicating constructor parameter).

You can put such methods in your interface and make data class implicitly implement them. See example below:

data class Order(var consumer: String, var pdfs: List) : Form

data class SomethingElse(var str: String, var set: Set) : Form

interface Form {
    val isEmpty: Boolean
        get() = checkEmpty(component1(), component2())

    fun  checkEmpty(vararg fields: T): Boolean {
        for (f in fields) {
            when (f) {
                is Collection -> if (!f.isEmpty()) return false
                is CharSequence -> if (!f.isBlank()) return false
            }
        }
        return true;
    }

    fun component1(): Any? = null
    fun component2(): Any? = null
}

You can also add fun component3(): Any? = null etc... to handle cases with more that 2 fields in data class (e.g. NullObject pattern or handling nulls directly in your checkEmpty() method.

As I said, it's kinda hacky but maybe will work for you.

Upvotes: 3

Related Questions