Scenario: Given a class with some internal state and methods to manipulate this state, I want to limit the exposed methods that are available to potential clients/users of my API.
TLDR: In the following, I present a simple example describing the problem and four alternatives I have considered. Answers should either point out problems with the alternatives or even better let me know if there is a way to address my problem in a different way. However, I might also have tunnel vision. In that case, please explain why limiting the exposed methods at compile-time is not really needed (What is the runtime alternative? Exceptions or Result-Monads?). I used Kotlin examples, but any language is fine.
Consider the following example, where I require users to first call changeState
before then only calling incrementState
. You could also consider the system of first being in a state "Changeable", then transitioning into state "Incrementable" after changeState
was called.
class Stateful {
private var state: Int = 0
fun changeState(value: Int) {
state = value
}
fun incrementState() {
state++
}
}
One approach would be to only explose the interfaces that are available in the current state, and then transition to a new state with a different interface (similar as stateful builders would do it):
interface S1 {
fun changeState(value: Int): S2
}
interface S2 {
fun incrementState(): S2
}
class Stateful: S1, S2 {
private var state: Int = 0
override fun changeState(value: Int): S2 {
state = value
return this
}
override fun incrementState(): S2 {
state++
return this
}
}
Here I see the following problems:
- How do I enforce that clients do not reuse the old instance
s1
after the transition, but use the returned interfaces2
? The interface allows for immutability, i.e. I can have fresh instances (new Stateful object) instead of returningthis
and not care about the old instance. - Without SELF types, is returning
this
a problem here?
A second approach would be to encapsulate the behavior in distinct classes:
class Change(var state: Int) { // could also implement S1
fun changeState(value: Int): Increment {
state = value
return Increment(state)
}
}
class Increment(var state: Int) { // could also implement S2
fun incrementState(): Increment {
state++
return Increment(state)
}
}
The problem I see here is that in a real-world implementation, manipulating the state within a single class is easier and no copying/transfer of the state is required. An internal State-Holder-object could make passing the state easier, but would make it mutable again.
I have also considered two "patterned" approaches:
A) StateMachine
enum class S { Change, Increment }
sealed interface E
data class OnChange(val state: Int): E
object OnIncrement: E
interface SM {
var state: Int // internal
var currentState: S
fun transition(event: E)
fun isValid(event: E): Boolean
}
What I don't like about the state machine (apart from probably pattern matching + delegating a lot inside the transition rather than simply letting the client call the correct method) is that I can not restrict the available transitions (events) at compile-time.
B) State pattern
Problem: My available methods are not unifiable under a common interface.
Here is how clients would use the respective approaches:
// Approach 1
val s1: S1 = ...
var s2: S2 = s1.changeState(1000)
s2 = s2.incrementState()
// Approach 2
val c: Change = ...
var i:Increment = c.changeState(1000)
i = i.incrementState()
// Approach 3
val sm: SM = ...
// Problem here is that we need to assert which transitions are valid
sm.transition(OnChange(1000))
sm.transition(OnIncrement)
How do I best design my API so that using my class is less errorprone to use at compile-time, since the correct subset of methods is available depending on the current state of the system?
Is this something that is desirable at all?