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 additionalprivate
field to demonstrate that the above solution works with data fields too.interface Accessor
- This is the core idea. We have apublic 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 viarunBlock()
you provided.It's an
inner class
so its objects hold a reference to the appropriateClassA
instance (thus thethis@ClassA
parts).It has the same fields as your
ClassA
, but madepublic
.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 theprivate
fields. Notice that the receiver is anAccessor
, notAccessorImpl
. It couldn't be the latter, because it'sprivate
(for the aforementioned reasons).AccessorImpl().block()
- We need to create anAccessor
for theblock
to call on. We need one that will be able to interact withClassA
'sprivate
fields. Thus we createAccessorImpl
and callblock()
on it. Keep in mind thatAccessorImpl
, due to being aninner class
, knows that it's bound to the object on whichrunBlock()
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.