-
-
Save yordis/9bac5d4db2f2962dd9f8122894a3fa53 to your computer and use it in GitHub Desktop.
TypeScript prototype of the Decider pattern. (F# version: https://github.com/akhansari/EsBankAccount)
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
import * as assert from "assert"; | |
/** Decider Pattern **/ | |
type Transaction = { | |
amount: number | |
date: Date | |
} | |
type Deposited = Transaction & { | |
kind: "deposited" | |
} | |
type Withdrawn = Transaction & { | |
kind: "withdrawn" | |
} | |
type Closed = { | |
kind: "closed" | |
closedOn: Date | |
} | |
export type Event_ = | |
| Deposited | |
| Withdrawn | |
| Closed | |
type State = { | |
Balance: number | |
IsClosed: boolean | |
} | |
export module State { | |
export const initial: Readonly<State> = { | |
Balance: 0, | |
IsClosed: false, | |
} | |
} | |
export const evolve = (state: Readonly<State>, event: Readonly<Event_>): Readonly<State> => { | |
switch (event.kind) { | |
case "deposited": | |
return { ...state, Balance: state.Balance + event.amount } | |
case "withdrawn": | |
return { ...state, Balance: state.Balance - event.amount } | |
case "closed": | |
return { ...state, IsClosed: true } | |
} | |
} | |
export type Command = | |
| { kind: "deposit", amount: number, date: Date } | |
| { kind: "withdraw", amount: number, date: Date } | |
| { kind: "close", date: Date } | |
export module Command { | |
export const deposit = (amount: number, date: Date): Command => | |
({ kind: "deposit", amount: amount, date: date }) | |
export const withdraw = (amount: number, date: Date): Command => | |
({ kind: "withdraw", amount: amount, date: date }) | |
export const close = (date: Date): Command => | |
({ kind: "close", date: date }) | |
} | |
export const decide = (command: Readonly<Command>) => (state: Readonly<State>): ReadonlyArray<Event_> => { | |
switch (command.kind) { | |
case "deposit": | |
return [ { kind: "deposited", amount: command.amount, date: command.date } ] | |
case "withdraw": | |
return [ { kind: "withdrawn", amount: command.amount, date: command.date } ] | |
case "close": | |
const events: Event_[] = [] | |
if (state.Balance > 0) { | |
events.push({ kind: "withdrawn", amount: state.Balance, date: command.date }) | |
} | |
events.push({ kind: "closed", closedOn: command.date }) | |
return events | |
} | |
} | |
/** Tests **/ | |
function deciderSpec<S, C, E>( | |
initialState: Readonly<S>, | |
evolve: (s: Readonly<S>, e: Readonly<E>) => Readonly<S>, | |
decide: (c: Readonly<C>) => (s: Readonly<S>) => ReadonlyArray<E>) { | |
const spec: { State: Readonly<S>, Outcome: ReadonlyArray<E> } = { | |
State: initialState, | |
Outcome: [], | |
} | |
return { | |
given(events: ReadonlyArray<E>) { | |
spec.State = events.reduce(evolve, spec.State) | |
return this | |
}, | |
when(command: Readonly<C>) { | |
const events = decide(command)(spec.State) | |
spec.State = events.reduce(evolve, spec.State) | |
spec.Outcome = events | |
return this | |
}, | |
then(expectedEvents: ReadonlyArray<E>) { | |
assert.deepStrictEqual(spec.Outcome, expectedEvents) | |
return this | |
}, | |
} | |
} | |
const test = (message: string, fn: Function): void => { | |
try { | |
fn() | |
console.log(" ✅ " + message) | |
} catch (e) { | |
console.error(" ❌ " + message) | |
console.error(e) | |
} | |
} | |
const anyDate = new Date(2000, 1, 1) | |
test("make a deposit", () => { | |
deciderSpec(State.initial, evolve, decide) | |
.when(Command.deposit(5, anyDate)) | |
.then([ { kind: "deposited", amount: 5, date: anyDate } ]) | |
}) | |
test("make a withdrawal", () => { | |
deciderSpec(State.initial, evolve, decide) | |
.when(Command.withdraw(5, anyDate)) | |
.then([ { kind: "withdrawn", amount: 5, date: anyDate } ]) | |
}) | |
test("close the account and withdraw the remaining amount", () => { | |
deciderSpec(State.initial, evolve, decide) | |
.given([ | |
{ kind: "deposited", amount: 50, date: anyDate }, | |
]) | |
.when(Command.deposit(50, anyDate)) | |
.when(Command.close(anyDate)) | |
.then([ | |
{ kind: "withdrawn", amount: 100, date: anyDate }, | |
{ kind: "closed", closedOn: anyDate }, | |
]) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment