Created
February 19, 2016 11:14
-
-
Save battermann/cc72769a686fcd753d79 to your computer and use it in GitHub Desktop.
Data-centric vs Function-centric Domain Design
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open System | |
// ----------------------------------------------------------- | |
// Domain | |
// ----------------------------------------------------------- | |
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 } | |
// ----------------------------------------------------------- | |
// Implementation | |
// ----------------------------------------------------------- | |
module GameImplementation = | |
open GameDomain | |
open System.Linq | |
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 } | |
// ----------------------------------------------------------- | |
// Game Domain Model with 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 } | |
// ----------------------------------------------------------- | |
// Game Implementation with Capability Based Security | |
// (uses the API implementation without security) | |
// ----------------------------------------------------------- | |
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 } | |
// ----------------------------------------------------------- | |
// UI | |
// ----------------------------------------------------------- | |
(* | |
This UI implementation was taken from Scoot Wlaschin from here https://gist.github.com/swlaschin/7a5233a91912e66ac1e4 | |
It was minimally adjusted to get it working in this context | |
*) | |
module ConsoleUi = | |
open GameDomainWithCapabilityBasedSecurity | |
/// Track the UI state | |
type UserAction<'a> = | |
| ContinuePlay of 'a | |
| ExitGame | |
/// Print each available move on the console | |
let displayNextMoves nextMoves = | |
nextMoves | |
|> List.iteri (fun i moveInfo -> | |
printfn "%i) %A" i moveInfo.GuessToMake) | |
/// Get the move corresponding to the | |
/// index selected by the user | |
let getCapability selectedIndex nextMoves = | |
if selectedIndex < List.length nextMoves then | |
let move = List.nth nextMoves selectedIndex | |
Some move.Capability | |
else | |
None | |
/// Given that the user has not quit, attempt to parse | |
/// the input text into a index and then find the move | |
/// corresponding to that index | |
let processMoveIndex inputStr availableMoves processInputAgain = | |
match Int32.TryParse inputStr with | |
// TryParse will output a tuple (parsed?,int) | |
| true,inputIndex -> | |
// parsed ok, now try to find the corresponding move | |
match getCapability inputIndex availableMoves with | |
| Some capability -> | |
// corresponding move found, so make a move | |
let moveResult = capability() | |
ContinuePlay moveResult // return it | |
| None -> | |
// no corresponding move found | |
printfn "...No move found for inputIndex %i. Try again" inputIndex | |
// try again | |
processInputAgain() | |
| false, _ -> | |
// int was not parsed | |
printfn "...Please enter an int corresponding to a displayed move." | |
// try again | |
processInputAgain() | |
/// Ask the user for input. Process the string entered as | |
/// a move index or a "quit" command | |
let rec processInput availableCapabilities = | |
// helper that calls this function again with exactly | |
// the same parameters | |
let processInputAgain() = | |
processInput availableCapabilities | |
printfn "Enter an int corresponding to a displayed move or q to quit:" | |
let inputStr = Console.ReadLine() | |
if inputStr = "q" then | |
ExitGame | |
else | |
processMoveIndex inputStr availableCapabilities processInputAgain | |
/// After each game is finished, | |
/// ask whether to play again. | |
let rec askToPlayAgain api = | |
printfn "Would you like to play again (y/n)?" | |
match Console.ReadLine() with | |
| "y" -> | |
ContinuePlay (api.NewGame()) | |
| "n" -> | |
ExitGame | |
| _ -> askToPlayAgain api | |
/// The main game loop, repeated | |
/// for each user input | |
let rec gameLoop api userAction = | |
printfn "\n------------------------------\n" // a separator between moves | |
match userAction with | |
| ExitGame -> | |
printfn "Exiting game." | |
| ContinuePlay moveResult -> | |
// handle each case of the result | |
match moveResult with | |
| Unsolved nextMoves -> | |
printfn "USOLVED: Make a guess" | |
displayNextMoves nextMoves | |
let newResult = processInput nextMoves | |
gameLoop api newResult | |
| Solved (dice, score) -> | |
printfn "SOLVED: %A" score | |
printfn "SECRET: %A" dice | |
printfn "" | |
let nextUserAction = askToPlayAgain api | |
gameLoop api nextUserAction | |
/// start the game with the given API | |
let startGame api = | |
let userAction = ContinuePlay (api.NewGame()) | |
gameLoop api userAction | |
// ----------------------------------------------------------- | |
// Logging | |
// ----------------------------------------------------------- | |
module Logger = | |
open GameDomain | |
let injectLogging api = | |
let makeGuess gameState guess = | |
printfn "[LOGINFO] %A" guess | |
api.MakeGuess gameState guess | |
{ api with MakeGuess = makeGuess } | |
// ----------------------------------------------------------- | |
// Console Application | |
// ----------------------------------------------------------- | |
module ConsoleApplication = | |
let startGame() = | |
let loggingApi = Logger.injectLogging GameImplementation.api | |
let api = GameImplementationWithCapabilityBasedSecurity.resolveApi loggingApi | |
ConsoleUi.startGame api | |
// ConsoleApplication.startGame() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
coming from this blog post:
Domain Design | data- or function-centric?
this is exactly what i had in mind for my next apps with extra-thin wrappers, for tight security, cross-platform portability, and native responsiveness