We are taught that the less dependencies a class has the better. So having a single dependency seems legit? Yes? No?
Unfortunately even a single dependency can corrupt a design. If our class only needs to select certain records why do we make it depend on the whole database? Wouldn't it be cool if we could make the class depend only on things it really needs?
Even control how it works with the dependencies or revoke the access if the need arises? Yes! This would be a nice thing!
During the talk we will learn how thinking about capabilities of our software will allow us to design code that will only allow users to do what is possible in that particular moment and how capabilities can make testing our software much much easier.
2. “If you make information
available someone will make
use of it”
David Parnas
3.
4.
5.
6. class Mail {
fun send() {
// business logic ...
}
// or
fun receive() {
// business logic ...
}
}
fun main() {
val email = Mail()
email.send()
// vs
email.receive()
}
7. class Foo {
fun initialize() {
// should be called before using the class
}
fun release() {
// should be called after using the class
}
// some other important methods here...
}
fun main() {
val foo = Foo()
// although we've created Foo we cannot use it :(
// but nothing in this world prevents us from it
foo.initialize()
// now we can use it without a sudden runtime exception
// as soon as we release Foo we cannot use it :(
foo.release()
// but nothing in this world prevents us from it
}
8. class MaybeABetterFoo {
var initialized = false
private set
fun initialize(): Boolean {
if (!initialized) {
// do something
}
return initialized
}
fun release(): Boolean {
if (initialized) {
// do something
}
return initialized
}
// other important methods here ...
}
9. val foo = MaybeABetterFoo()
if (foo.initialized) {
// we are paranoid and check before we do anything
}
if (foo.initialize()) {
// did we manage to initialize it?
}
if (foo.release()) {
// did we manage to release it?
}
10. class MaybeABetterFoo {
// … //
fun canDoSth(): Boolean =
TODO("A very important logic here!")
fun doSth() {}
}
val foo = MaybeABetterFoo()
if (foo.initialize()) {
// if we managed to initialize it then do something else
if (foo.initialized) {
// we often need to double check if it was not released
if (foo.canDoSth()) {
// maybe there are still things that need to be checked
foo.doSth() // our code looks like an arrow >
}
}
}
11. class BarUninitialized {
fun initialize(): BarInitialized =
BarInitialized()
// other things that can be done with uninitialized Bar
}
class BarInitialized {
fun release(): BarUninitialized =
BarUninitialized()
// other things that can be done with initialized Bar
fun doSomething() {
}
}
13. class GameState {
// Returns all possible move capabilities for the given state
fun getAvailableCapabilities(): List<MoveCapabilities>
// Uses one of the capabilities for a given player
fun run(player: Player, capability: MoveCapabilities)
// Check the status of the game
fun getGameResult(): GameResult
}
sealed class MoveCapabilities {
data class PLAY_CARDS(val cards: List<Card>) : MoveCapabilities()
data class DRAW_CARDS(val amount: Int) : MoveCapabilities()
object END_TURN : MoveCapabilities()
object FORFEIT_GAME : MoveCapabilities()
}
14. Modify the state with a picked capability, get
new state with a new set of capabilities.
15. sealed class Capabilities(val move: () -> BetterGameState) {
class PLAY_CARDS(
val cards: List<Card>,
move: () -> BetterGameState
) : Capabilities(move)
class DRAW_CARDS(
val amount: Int,
move: () -> BetterGameState
) : Capabilities(move)
class END_TURN(
move: () -> BetterGameState
) : Capabilities(move)
class FORFEIT_GAME(
move: () -> BetterGameState
) : Capabilities(move)
}
16. class BetterGameState {
// Returns all possible move capabilities for the given state
fun getAvailableCapabilities(): List<Capabilities>
// Check the status of the game
fun getGameResult(): GameResult
fun getCurrentPlayer(): Player
}
17. fun main() {
val game = BetterGameState()
val capabilities = game.getAvailableCapabilities()
// we can present the options to the player
// let's assume the player picked one of the capabilities:
capabilities[0].move() // <-- running the capability
// player info is baked in, we cannot do an invalid move
}
20. // some database
class Database {
fun run(query: DbQuery): DbResult =
DbResult() // some dummy result
}
// represents a possible query in the database
class DbQuery
// represents a possible results when doing a query on the
database
class DbResult
21. class UserAcquisitionManager
(
private val query: (DbQuery) -> DbResult /*, other fields of course */
) {
// super important business logic that is using the database
}
22. fun log(tag: String, message: String) =
println("$tag:${generateTimestamp()}:$message")
fun generateTimestamp(): Long = 1L
fun <A, B> addLogging(tag: String, f: (A) -> B): (A) -> B = {
log(tag, "$it")
f(it)
}
23. fun main() {
val db = Database()
val queryWithLogging: (DbQuery) -> DbResult =
addLogging("UserAcquisitionManager", db::run)
val uam = UserAcquisitionManager(queryWithLogging)
}
24. interface Revoker {
fun revoke() // for the sake of the example this is good enough
}
fun <A, B> revocable(f: (A) -> B): Pair<(A) -> B, Revoker> {
val available = AtomicBoolean(true)
val revocableFunction: (A) -> B = {
if (available.get()) {
f(it)
}
throw IllegalStateException("Privileges were revoked!")
}
val revoker = object : Revoker {
override fun revoke() {
available.set(false)
}
}
return Pair(revocableFunction, revoker)
}
25. val db = Database()
val revocable: Pair<(query: DbQuery) -> DbResult, Revoker> =
revocable(db::run)
val revocableFunction: (query: DbQuery) -> DbResult =
revocable.first
val revoker: Revoker =
revocable.second
val uam = UserAcquisitionManager(revocableFunction)
// now to revoke at any moment
revoker.revoke()
26. // we could wrap everything with logging:
val loggedRevocableFunction: (query: DbQuery) -> DbResult =
addLogging("UserAcquisitionManager", revocableFunction)
// or
val revocableLogged: Pair<(query: DbQuery) -> DbResult, Revoker> =
revocable(addLogging("UserAcquisitionManager", db::run))
val revocableLoggedFunction: (query: DbQuery) -> DbResult =
revocable.first
val revokerLogged: Revoker =
revocable.second
27. // we can make things better with:
class UserAcquisitionManagerBetter
(
private val query: (DbQuery) -> (() -> DbResult)? /*, other fields */
) {
// super important business logic that is using the database
}
// or … even better:
class UserAcquisitionManagerEvenBetter
(
private val query: () -> DbResult /*, here other fields of course */
) {
// super important business logic that is using the database
}