0

I have Parent-Search-Child system as below:

class Room

class Building {
    fun find(by: By) = Room()
}

sealed class By {

    abstract fun search(): Room

    class ById(id: String) : By() {
        override fun search(): Room = Room() // epic search method
    }

    class ByName(name: String) : By() {
        override fun search(): Room = Room() // epic search method
    }

    class Byurpose(purpose: String) : By() {
        override fun search(): Room = Room() // epic search method
    }

    companion object {
        fun id(id: String) = ById(id)
        fun name(name: String) = ByName(name)
        fun purpose(purpose: String) = Byurpose(purpose)
    }
}

Which can be used as follows:

val building = Building()
val room = building.find(By.name("Toilet"))

However, I am not very satisfied with the current syntax, which could be much less verbose in Kotlin. In addition, building.find can appear thousands times in the code. I could implement it differently, but actually I don't own Room, Building or By classes, so I can't. Thus this was my approach:

I implemented context class that stores Building reference, and use it internally as source for search methods:

class BuildingContext(private val building: Building) {
    fun String.findById() = building.find(By.id(this))
    fun String.findByName() = building.find(By.name(this))
    fun String.findByPurpose() = building.find(By.purpose(this))
}

It can be used as below:

with(BuildingContext(building)) {
    val room2 = "Toilet".findByName()
}

After that I noticed that I only use one search method in 99% cases, so (for sake of even shorter syntax!) I implemented following classes:

object AlwaysSearchById {
    fun String.find(building: Building) = building.find(By.id(this))
}

object AlwaysSearchByName {
    fun String.find(building: Building) = building.find(By.name(this))
}

object AlwaysSearchByPurpose {
    fun String.find(building: Building) = building.find(By.purpose(this))
}

Which can be used this way:

with(AlwaysSearchByName) {
    val room3 = "Toilet".find(building)
}

Unfortunately, building reference appears again. The ideal syntax would be "Toilet".find(). I could fix it re-implementing Always~ classes as follows:

class AlwaysSearchByNameV2(private val building: Building) {
    fun String.find() = building.find(By.name(this))
}

And it would be used as below:

with(AlwaysSearchByNameV2(building)) {
    val room = "Toilet".find()
}

But it some cases, I would like to access BuildingContext methods as well, so So I have to write:

with(BuildingContext(building)) {
    with(AlwaysSearchByNameV2(building)) {
        val toilet = "Toilet".find()
        val randomRoom = "123".findById()
    }
}

The question is - How I reduce multiple with clauses in this case?

In the example above there are only 2 with clauses, but it's only basic example. In real world there could be dozens of them, and writing with(with(with(with(with... would surely be a pain.

On the side note this doesn't work:

with(BuildingContext(building), AlwaysSearchByNameV2(building)) {
    val toilet = "Toilet".find()
    val randomRoom = "123".findById()
}

nor this

with(*arrayOf(BuildingContext(building), BuildingContext(building))) {
    val toilet = "Toilet".find()
    val randomRoom = "123".findById()
}
xinaiz
  • 7,744
  • 6
  • 34
  • 78

1 Answers1

1

You can write custom scoping functions instead of relying on with all the time. For example you can add an extension function that will run a block of code in scope of AlwaysSearchByNameV2 object:

inline fun BuildingContext.byName(f : AlwaysSearchByNameV2.() -> Unit) = AlwaysSearchByNameV2(building).apply(f)

And use it:

with(BuildingContext(building)) {  // this: BuildingContext
    byName {  // this: AlwaysSearchByNameV2
        val toilet = "Toilet".find()
        val randomRoom = "123".findById()  // can still refer to BuildingContext
    }
    // back to this: BuildingContext
}
Pawel
  • 15,548
  • 3
  • 36
  • 36