Let's walk through an example of implementing the Factory pattern to retrieve difficulty settings for a particular game. What we want to achieve is something like this:
val settings = DifficultySettings.getSettings(GameDifficulty.HARD)
println("Difficulty Settings = ${settings.label}")
Our goal is to use our Factory method, in this case getSettings(), to retrieve an instance of some settings type and be able to work with that type without needing to understand how that type is actually instantiated. To implement this, perform the following steps:
- First, we will create an enum class to represent the available difficulties we want our client code to be aware of:
enum class GameDifficulty {
EASY, NORMAL, HARD
}
- Next, we will create a DifficultySettings class, which is what our client code will access to represent the individual settings for the game:
class DifficultySettings private constructor(
val label: String,
val lives: Int,
val enemySpeed: Float,
val enemyHealth: Float
)
Notice that we've made the constructor for this class private. This is because we don't want our client code to be able to instantiate instances of this class. We want to hide that work behind our Factory method.
- Now, we will add our Factory method to the DifficultySettings class using a companion object:
class DifficultySettings private constructor(
val label: String,
val lives: Int,
val enemySpeed: Float,
val enemyHealth: Float
) {
companion object Factory {
@JvmStatic
fun getSettings(difficulty: GameDifficulty):
DifficultySettings {
return when (difficulty) {
GameDifficulty.EASY -> DifficultySettings(
"EASY", 5, .5f, .5f)
GameDifficulty.NORMAL -> DifficultySettings(
"NORMAL", 3, .75f, .75f)
GameDifficulty.HARD -> DifficultySettings(
"HARD", 1, 1f, 1f)
}
}
}
}
We've named our companion object Factory to provide some additional semantic meaning to this companion object, and the use of @JvmStatic ensures that our new method will be readily available from both Kotlin and Java with the same syntax.
With this companion object in place, we can now retrieve instances of DifficultySettings through our Factory method, getSettings():
fun main() {
val settings = DifficultySettings.getSettings(GameDifficulty.HARD)
println("Difficulty Settings = ${settings.label}")
}
// outputs: Difficulty Settings = HARD
By leveraging a factory to retrieve our instance of DifficultySettings, we've hidden the initialization details from our calling code and insulated it from future changes in how DifficultySettings must be constructed. The Factory pattern is a great tool for encapsulating this type of instantiation and retrieval, and it is easy to implement in Kotlin.
In the next section, we'll see how Kotlin enables us to rethink the Builder pattern.