Home > Software design >  How can I access private members in Kotlin when passing a block from one class to another
How can I access private members in Kotlin when passing a block from one class to another

Time:03-05

I've got a class called ClassA, that I'd like to be completely private except for one method that can take in a block of code and execute it. I would like the block of code I pass into this method to be able to call private members of ClassA. This is a heavily simplified representation of what I have now:

class ClassA() {  
    private fun printFoo(session: SessionInfo){
        System.out.println("Foo")
    }

    fun runBlock(block: ClassA.() -> Unit) {
        this.block()
    }
}

class ClassB(var classA: ClassA) {    
    fun doThing() {
            classA.runBlock { this.printFoo() }
    }
}

The above code doesn't compile because printFoo is only private access in ClassA. If I change it's access to public, this code works fine. Is there a way I can reference this printFoo in the block I pass in from ClassB without having to make the method public?

CodePudding user response:

There are two ways of approaching this: using reflection or a proxy accessor.

Using reflection

Reflection will pretty much enable you to do anything you like. You can always force through the hermetization with it:

class HasPrivateMembers(private val foo: String)

fun main() {
    val x = HasPrivateMembers("bar")
//    println(x.foo) // doesn't work due to private specifier
    val foo = x.javaClass.getDeclaredField("foo").run {
        isAccessible = true
        get(x) as String
    }.also {
        println(it)
    }
}

I wouldn't take this approach though. It feels a little bit hacky and it should feel so. If you were to endorse this approach, you wouldn't be designing an architectural access to your fields. You would be basically indicating that there should be no access to your ields, but if one wishes to interact with them directly anyway (even violate the invariants, if any present), they are free to use the tool that can do that.

This leads us to the second approach.

Using proxy accessor object

The idea is to create a private inner class that will be used to delegate the calls to the API that it shares with ClassA:

class ClassA {
    private var privateField = "this is private"

    private fun printFoo(){
        println("Foo")
    }

    interface Accessor {
        fun printFoo()
        var privateField: String
    }

    private inner class AccessorImpl : Accessor {
        override fun printFoo() {
            [email protected]()
        }

        override var privateField: String
            get() = [email protected]
            set(value) {
                [email protected] = value
            }
    }

    fun runBlock(block: Accessor.() -> Unit) {
        AccessorImpl().block()
    }
}

class ClassB(var classA: ClassA) {
    fun doThing() {
        classA.runBlock {
            printFoo()
            privateField = "but I just changed it"
        }
    }
}

There are a lot of changes going on here, so let me explain every single one of them for the snippet to make sense:

  • private var privateField = "this is private" - I added an additional private field to demonstrate that the above solution works with data fields too.

  • interface Accessor - This is the core idea. We have a public interface which serves as a glue between the outside world and the private implementation that delegates the access of private fields.

  • private inner class AccessorImpl - This is the implementation of the core idea.

    • It's private, so no one will be able to exploit it and the only way one can use it is via runBlock() you provided.

    • It's an inner class so its objects hold a reference to the appropriate ClassA instance (thus the this@ClassA parts).

    • It has the same fields as your ClassA, but made public.

    • All it does it to delegate every single operation to the outer class' object.

  • fun runBlock(block: Accessor.() -> Unit) - This is the public API for accessing all the private fields. Notice that the receiver is an Accessor, not AccessorImpl. It couldn't be the latter, because it's private (for the aforementioned reasons).

    • AccessorImpl().block() - We need to create an Accessor for the block to call on. We need one that will be able to interact with ClassA's private fields. Thus we create AccessorImpl and call block() on it. Keep in mind that AccessorImpl, due to being an inner class, knows that it's bound to the object on which runBlock() has been called.

While I strongly prefer the second solution to the first one, it's good to keep in mind that hermetization shouldn't be violated in normal circumstances. Every time you change private API of ClassA (that... does sound weird, but that's what we're dealing with here), you will have to change both Accessor and AccessorImpl to reflect that. There is no easy workaround around it and frankly I believe that to be a good thing. You should be extra cautious when dealing with such architecture.

  • Related