Domain Design, data- or function-centric?

There are two great articles by Scott Wlaschin on how functional programming can be used for the domain design of real-world applications by implementing a Tic-Tac-Toe game. He demonstrates how business rules can be modeled with types, how data hiding, encapsulation, logging and capability-based security can be achieved with functional programming and more.

What I found remarkable is that in the second article he completely re-engineered the first design. Even though to me the original implementation appeared to be very appealing.

I liked that the data-centric domain model was concise, totally clear and very close to the natural notion of the game. Eventually it couldn’t meet high security standards since there were ways for malicious users of the API to manipulate the data.

The second function-centric implementation introduced the concept of capability-based security. The design smells of the previous version could be resolved. But I argue that the API of the second version is not as intuitive as the first one anymore. Technically it is also quite simple. However, the recursive structure doesn’t come totally natural to me. Also an indicator that the second version is more complex is that logging becomes trickier.

Why not do both?

I was wondering if it is possible to combine the two approaches? In my opinion this could be achieved by implementing the function-centric version as a thin wrapper around the data-centric version.

Also by doing this the implementation better complies with separation of concerns.

Is it the concern of the domain module to represent the business rules or to ensure security?

Proof of concept: Secret dice game

Here is a small example as a proof of concept.

I took an even simpler game than Tic-Tac-Toe. The objective is to guess a number (from one to six) that was randomly generated as by rolling a dice. The score will be inversely proportional to the number of trials needed.

Domain Model

This is the data-centric domain model:

module GameDomain =

    type Dice = One | Two | Three | Four | Five | Six

    type Guess = Guess of Dice

    type ValidMoves = Guess list

    type GameState = {
        Trials: Guess list
        Secret:Dice }

    type Score = Score of int

    type MoveResult = 
        | Unsolved of ValidMoves
        | Solved of Dice * Score

    type GameApi = {
        NewGame: unit -> GameState * MoveResult
        MakeGuess: GameState -> Guess -> GameState * MoveResult }

The types nicely represent the domain and the use cases. Everything is very comprehensive and clear.

Implementation

The implementation is simple and straight forward:

module GameImplementation =
    open GameDomain

    let private rnd = Random()

    let private allpossibleGuesses = 
        [One; Two; Three; Four; Five; Six] |> List.map Guess

    let private newGame ()=
        match rnd.Next(1,7) with
        | 1 -> One
        | 2 -> Two
        | 3 -> Three
        | 4 -> Four
        | 5 -> Five
        | 6 -> Six
        |> fun dice -> { Trials = []; Secret = dice }, Unsolved allpossibleGuesses

    let private isSolved guess secret = guess = secret

    let private makeGuess gameState (Guess guess) =
        let trials = Guess guess :: gameState.Trials
        let score = 6 - List.length trials
        let findNextMoves trials = 
            allpossibleGuesses 
            |> List.filter (fun guess -> trials |> (not << List.exists ((=) guess)))
        let moveResult = 
            if isSolved guess gameState.Secret then
                (gameState.Secret, Score score) |> Solved
            else 
                findNextMoves trials |> Unsolved
        { gameState with Trials = trials }, moveResult

    let api = {
        NewGame = newGame
        MakeGuess = makeGuess }

Domain Model with capability-based security

Now let’s look at the function-centric domain model which uses capability-based security:

module GameDomainWithCapabilityBasedSecurity =
    open GameDomain

    type MoveCapability = unit -> CbsMoveResult

    and NextMoveInfo = {
        GuessToMake : Guess
        Capability : MoveCapability }

    and CbsMoveResult =
        | Unsolved of NextMoveInfo list
        | Solved of Dice * Score

    type CbsGameApi = { NewGame : MoveCapability }

Note that the GameState is hidden in this model. However, this post is not about the details of capability-base security. Please refer to this article for a more detailed explanation.

Implementation with capability-based security

The implementation of the function-centric domain model uses the original implementation and therefore is just a thin wrapper around it.

module GameImplementationWithCapabilityBasedSecurity =
    open GameDomain
    open GameDomainWithCapabilityBasedSecurity

    let rec makeMove api moveStatePair =
        let (newState, moveResult) =
            match moveStatePair with
            | Some (guess, gameState) -> api.MakeGuess gameState guess
            | None                    -> api.NewGame()

        let makeMoveInfo g = 
            { GuessToMake = g; Capability = fun () -> makeMove api (Some (g, newState)) }

        match moveResult with
        | MoveResult.Unsolved validMoves -> 
            validMoves |> List.map makeMoveInfo |> CbsMoveResult.Unsolved
        | MoveResult.Solved (secret, score) -> CbsMoveResult.Solved (secret, score)

    let resolveApi api = { NewGame = fun () -> makeMove api None }

The original API is passed as an argument to the functions of this implementation.

Logging

Logging is pretty easy now because the logger can be injected into the original API:

module Logger =
    open GameDomain

    let injectLogging api =
        let makeGuess gameState guess =
            printfn "[LOGINFO] %A" guess
            api.MakeGuess gameState guess

        { api with MakeGuess = makeGuess }

module ConsoleApplication = 

    let startGame() =
        let loggingApi = Logger.injectLogging GameImplementation.api
        let api = GameImplementationWithCapabilityBasedSecurity.resolveApi loggingApi
        ConsoleUi.startGame api 

The UI

The complete code including a console based UI is available on GitHub.

Here is a typical course of the game and the output on the console:

> ConsoleApplication.startGame();;

------------------------------

USOLVED: Make a guess
0) Guess One
1) Guess Two
2) Guess Three
3) Guess Four
4) Guess Five
5) Guess Six
Enter an int corresponding to a displayed move or q to quit:
2
[LOGINFO] Guess Three

------------------------------

USOLVED: Make a guess
0) Guess One
1) Guess Two
2) Guess Four
3) Guess Five
4) Guess Six
Enter an int corresponding to a displayed move or q to quit:
3
[LOGINFO] Guess Five

------------------------------

USOLVED: Make a guess
0) Guess One
1) Guess Two
2) Guess Four
3) Guess Six
Enter an int corresponding to a displayed move or q to quit:
0
[LOGINFO] Guess One

------------------------------

SOLVED: Score 3
SECRET: One

Would you like to play again (y/n)?

Conclusion

Understanding and implementing a data-centric design is easier.

If security is a concern then this implementation can be wrapped inside a function-centric implementation.

To me the approach of combining both designs feels more right because it follows the single-responsibility-principle and the principle of separation of concerns.

The complete code is available on GitHub.