A short presentation about driving our API design towards capabilities that will allow more control over what our code is really responsible for. This talk was given on a rapid talks (15 min format) session in my company.
2. Scott Wlaschin – F# for Fun and Profit
Designing with Capabilities
Domain Modeling Made Functional
Functional Design Patterns
F# for Fun and Profit
(much more …)
3. Functional Architecture– the pits
of success
Types + Properties = Software
Type Driven Development
Ploeh blog
(much more …)
Mark Seemann – Ploeh blog
4.
5.
6.
7. val mail = Mail()
// which one?
mail.send()
// or
mail.receive()
8. class Foo {
fun initialize()
fun release()
// some other important methods here...
}
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
9. Let's play a game of cards:
• We can start a gameif it is not running
yet
• We can only do valid moves
• We can forfeit if the gameis running
• We can win a game
• We can lose a game
• We cannot win if someone else already
won
• We cannot make a move after the game
is finished
10. // our domain model:
class Deck
class Card
class Hand
// our main "engine":
class Game {
fun start()
fun finish()
fun play(card: Card)
}
12. Game
Game Game
We can only ask for a list of things that can be done (capabilities)
We can only use the availablecapabilityto change the state of our game
13.
14.
15. // models database response like Success/Failure:
class DbResponse
// contains query results or failure status:
class DbQueryResult
// something stored in a database:
class Element
// describes a query (some SQL magic here):
class Query
class Database {
fun insert(element: Element): DbResponse
fun select(query: Query): DbQueryResult
fun delete(element: Element): DbResponse
}
16. class UserAquisitionManager(
private val db: Database /*, here other fields ofcourse */
) {
// super important business logic that is using the database
}
Quiz:
1) Does UserAquisitionManager realyneed access to the whole database?
2) Does it need to be able to add any element?
3) Does it need to be able to ready any element?
4) Does it need to be able to delete any element?
5) What can go wrong if leave it like this?
17.
18. class UserAquisitionManager(
private val select: (Query) -> Database /*, here other fields
ofcourse */
) {
// super important business logic that is using the database
}
With this one simple change:
1) We are sure it is only query related!
2) No unwanteddeletes or inserts!
3) Easy testing (no in memory database reallyneeded)!
4) We won't be tempted to do evil hacks!
But is it really ok now?
19. val databaseSelect: (Query) -> DbResult = {
// function that is wrapping the access (select) to the database
}
val aquisitionManager = UserAquisitionManager(
databaseSelect /*, some other data*/
)
But I had logging in my Databse! :(
20. fun <A, B> addLogging(tag: String, f: (A) -> B): (A) -> B = {
if (Log.D) {
Log.d(tag, "${generateTimestamp()} : $it")
}
return f(it)
}
val databaseSelect: (Query) -> DbResult = {
// function that is wrapping the access (select) to the database
}
val loggedDatabaseSelect: (Query) -> DbResult =
addLogging("UserAquisitionManager", databaseSelect)
val aquisitionManager = UserAquisitionManager(
loggedDatabaseSelect /*, some other data*/
)
Logging is too easy! Try revoking the rights ]:->
21. interface Revoker {
fun revoke()
}
fun <A, B> revokable(f: (A) -> B): Pair<(A) -> B, Revoker> {
val available = AtomicBoolean(true)
val revokableFunction: (A) -> B = {
if (available.get()) {
return f(it)
}
throw IllegalStateException("Privilages were revoked")
}
val revoker = object : Revoker {
override fun revoke() {
available.set(false)
}
}
Pair(revokableFunction, revoker)
}
22. val databaseSelect: (Query) -> DbResult = {
// function that is wrapping the access (select) to the database
}
val (databaseSelectRevoker, revokableDatabaseSelect) =
revokable(databaseSelect)
val aquisitionManager = UserAquisitionManager(
revokableDatabaseSelect/*, some other data*/
)
// We can revoke at any time:
databaseSelectRevoker.revoke()
But we lost logging again! :(
23. val loggedRevokableDatabaseSelect: (Query) -> DbResult =
addLogging("UserAquisitionManager", revokableDatabaseSelect)
val aquisitionManager = UserAquisitionManager(
loggedDatabaseSelect /*, some other data*/
)
Ok. Nice and all! But is this really safe?