Intro
I’m practicing Domain Modeling Made Functional and have implemented the Profile domain context of Nikeza.Mobile.
Here’s the birds-eye view of the workflows I constructed:
Workflow
The workflows that I wrote reflect operations that rely on I/O on the edges and domain logic at the core. All workflows take a command and emit domain events as defined upfront via function type declarations. These function types are RegistrationWorkflow, SessionWorkflow, and EditWorkflow.
module Workflows open Commands open Events open Logic type private RegistrationWorkflow = RegistrationCommand -> RegistrationEvent list type private SessionWorkflow = SessionCommand -> SessionEvent list type private EditWorkflow = EditCommand -> ProfileEvent list let handleRegistration : RegistrationWorkflow = fun command -> command |> function | RegistrationCommand.Validate form -> form |> Registration.validate |> ResultOf.Registration.Validate |> Registration.handle | RegistrationCommand.Submit form -> form |> IO.trySubmit |> ResultOf.Registration.Submit |> Registration.handle let handleSession : SessionWorkflow = fun command -> command |> function | SessionCommand.Login credentials -> credentials |> IO.tryLogin |> ResultOf.Login |> Session.handle | SessionCommand.Logout -> IO.tryLogout() |> ResultOf.Logout |> Session.handle let handleEdit : EditWorkflow = fun command -> command |> function | EditCommand.Validate profile -> profile |> Edit.validate |> ResultOf.Editor.Validate |> Edit.handle | EditCommand.Save profile -> profile |> IO.trySave |> ResultOf.Editor.Save |> Edit.handle
Commands
Commands are partitioned via responsibilities. Hence, I have command types for supporting registration, user-session, and editing.
The result of command execution is reflected by the ResultOf module that’s defined under the command types. I used similar naming conventions to enhance readability specifically for the Workflows module.
module Commands open Nikeza.DataTransfer open Registration type RegistrationCommand = | Validate of Registration.UnvalidatedForm | Submit of Registration.ValidatedForm type SessionCommand = | Login of Credentials | Logout type EditCommand = | Validate of EditedProfile | Save of ValidatedProfile module ResultOf = type Editor = | Validate of Result<ValidatedProfile, EditedProfile> | Save of Result<Profile, ValidatedProfile> type Session = | Login of Result<Provider, Credentials> | Logout of Result<unit, unit> type Registration = | Submit of Result<Nikeza.DataTransfer.Profile, Registration.ValidatedForm> | Validate of Result<ValidatedForm, UnvalidatedForm>
Events
Domain events are emitted from a workflow. These events are organized under different domain responsibilities (i.e. Registration, Session, Edit).
module Events open Nikeza.Common open Nikeza.DataTransfer type RegistrationEvent = | FormValidated of Registration.ValidatedForm | FormNotValidated of Registration.UnvalidatedForm | RegistrationSucceeded of Profile | RegistrationFailed of Registration.ValidatedForm | LoginRequested of ProfileId type SessionEvent = | LoggedIn of Provider | LoginFailed of Credentials | LoggedOut | LogoutFailed type ProfileEvent = | ProfileValidated of ValidatedProfile | ProfileNotValidated of EditedProfile | ProfileRequested of ProfileId | ProfileSaved of Nikeza.DataTransfer.Profile | ProfileSaveFailed of ValidatedProfile | Subscribed of ProviderId | Unsubscribed of ProviderId
I/O
The IO module is responsible for executing impure functions that have a dependency on the outside world. Thus, I use a Result type to reflect the status of the I/O operations.
module internal IO open Registration open Nikeza.DataTransfer type private TrySubmit = ValidatedForm -> Result<Profile, ValidatedForm> type private TryLogin = Credentials -> Result<Provider, Credentials> type private TryLogout = unit -> Result<unit, unit> type private TrySave = ValidatedProfile -> Result<Profile, ValidatedProfile> let trySubmit : TrySubmit = fun form -> Error form let tryLogout : TryLogout = fun () -> Error () let tryLogin : TryLogin = fun credentials -> Error credentials let trySave : TrySave = fun profile -> Error profile
Logic.editor.fs
Here’s the domain logic for editing a user’s profile:
module internal Logic.Edit open Nikeza.DataTransfer open Events open Commands type private Validate = EditedProfile -> Result<ValidatedProfile, EditedProfile> type private Handle = ResultOf.Editor -> ProfileEvent list let validate : Validate = fun edit -> let validEmail = not <| System.String.IsNullOrEmpty(edit.Profile.Email) let validFirstName = not <| System.String.IsNullOrEmpty(edit.Profile.FirstName) let validLastName = not <| System.String.IsNullOrEmpty(edit.Profile.LastName) if validEmail && validFirstName && validLastName then Ok { Profile= edit.Profile } else Error { Profile= edit.Profile } let handle : Handle = fun response -> response |> function | ResultOf.Editor.Validate result -> result |> function | Ok profile -> [ProfileValidated profile] | Error profile -> [ProfileNotValidated profile] | ResultOf.Editor.Save result -> result |> function | Ok profile -> [ProfileSaved profile] | Error profile -> [ProfileSaveFailed profile]
Logic.session.fs
Here’s the domain logic for managing a user’s session:
module internal Logic.Session open Commands open Events type private HandleLogin = ResultOf.Session -> SessionEvent list let handle : HandleLogin = fun result -> result |> function | ResultOf.Session.Login result -> result |> function | Ok info -> [LoggedIn info] | Error info -> [LoginFailed info] | ResultOf.Session.Logout result -> result |> function | Ok _ -> [LoggedOut] | Error _ -> [LogoutFailed]
Logic.registration.fs
Here’s the domain logic for managing a user’s registration:
module internal Logic.Registration open Nikeza.Common open Events open Registration open Commands type private Registration = ResultOf.Registration -> RegistrationEvent list let handle : Registration = fun resultOf -> resultOf |> function | ResultOf.Registration.Submit result -> result |> function | Ok profile -> [RegistrationSucceeded profile] | Error form -> [RegistrationFailed form] | ResultOf.Registration.Validate result -> result |> function | Ok form -> [FormValidated form] | Error form -> [FormNotValidated form] let validate (unvalidatedForm:UnvalidatedForm) : Result<ValidatedForm, UnvalidatedForm> = let isValidEmail email = false let form = unvalidatedForm.Form if not (form.Email |> isValidEmail) then Error unvalidatedForm elif form.Password <> form.Confirm then Error unvalidatedForm else Ok { Form= form } let isValid (credentials:LogInRequest) = let validEmail = not <| System.String.IsNullOrEmpty(credentials.Email) let validPassword = not <| System.String.IsNullOrEmpty(credentials.Password) validEmail && validPassword