Skip to content

Instantly share code, notes, and snippets.

@DScheglov
Created March 24, 2025 15:03
Show Gist options
  • Save DScheglov/5654c4195eec3ff228793e81bccadfad to your computer and use it in GitHub Desktop.
Save DScheglov/5654c4195eec3ff228793e81bccadfad to your computer and use it in GitHub Desktop.
GoF State Pattern
/**
* TaxiOrder:
* - **Draft** (default) the order is created with empty data
* - Placed - the order is placed and System is searching for Driver
* - Accepted - the Driver accepted the Order, but the the trip is not yet started
* - InProgress - the trip is started but not finished
* - Completed - the trip is ended
* - Cancelled - the Order has been cancelled by Driver or by Customer
*/
type Person = {
id: string
name: string
rating: number
registeredAt: Date
}
type Driver = Person & {
licenseLevel: string
}
type Address = {
country: string
city: string
street: string
building: string
}
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E; message?: string }
const ok: {
(): Result<void, never>
<T>(value: T): Result<T, never>
} = <T,>(value?: T): Result<T, never> => ({
ok: true,
value: value as T,
})
const err = <E,>(error: E, message?: string): Result<never, E> => ({
ok: false,
error,
message,
})
interface ITaxiOrderContext {
setCustomer(customer: Person): void
setStart(start: Address): void
setFinish(finish: Address): void
setState(newState: ITaxiOrderState): void
setDriver(driver: Driver): void
setPassengersNumber(passengersNumber: number): void
setActualFinish(actualFinish: Address): void
}
interface ITaxiOrderState {
name: string
place(
customer: Person,
start: Address,
finish: Address,
): Result<void, 'ERR_ALREADY_PLACED' | 'ERR_HAS_BEEN_CANCELLED'>
accept(
driver: Driver,
): Result<
void,
'ERR_NOT_YET_PLACED' | 'ERR_ALREADY_ACCEPTED' | 'ERR_HAS_BEEN_CANCELLED'
>
startRide(
passengersNumber: number,
): Result<
void,
'ERR_NOT_YET_ACCEPTED' | 'ERR_HAS_BEEN_CANCELLED' | 'ERR_ALREADY_STARTED'
>
completeRide(
actualFinish: Address,
): Result<
void,
'ERR_NOT_YET_STARTED' | 'ERR_HAS_BEEN_CANCELLED' | 'ERR_ALREADY_COMPLETED'
>
cancel(
by: 'customer' | 'driver',
): Result<
void,
| 'ERR_ALREADY_STARTED'
| 'ERR_ALREADY_COMPLETED'
| 'ERR_HAS_BEEN_CANCELLED'
| 'ERR_INVALID_CANCELER'
>
}
class TaxiOrderDraftState implements ITaxiOrderState {
name = 'Draft'
#taxiOrder: ITaxiOrderContext
constructor(taxiOrder: ITaxiOrderContext) {
this.#taxiOrder = taxiOrder
}
place(customer: Person, start: Address, finish: Address) {
this.#taxiOrder.setCustomer(customer)
this.#taxiOrder.setStart(start)
this.#taxiOrder.setFinish(finish)
this.#taxiOrder.setState(new TaxiOrderPlacedState(this.#taxiOrder))
return ok()
}
accept() {
return err('ERR_NOT_YET_PLACED' as const, 'The Order is not yet place')
}
startRide() {
return err('ERR_NOT_YET_ACCEPTED' as const, 'The order is not yet placed.')
}
completeRide() {
return err('ERR_NOT_YET_STARTED' as const, 'The order is not yet placed.')
}
cancel() {
this.#taxiOrder.setState(new TaxiOrderCancelledState(null))
return ok()
}
}
class TaxiOrderPlacedState implements ITaxiOrderState {
name = 'Placed'
#taxiOrder: ITaxiOrderContext
constructor(taxiOrder: ITaxiOrderContext) {
this.#taxiOrder = taxiOrder
}
place() {
return err('ERR_ALREADY_PLACED' as const, 'The order is alrady placed')
}
accept(driver: Driver) {
this.#taxiOrder.setDriver(driver)
this.#taxiOrder.setState(new TaxiOrderAcceptedState(this.#taxiOrder))
return ok()
}
startRide() {
return err(
'ERR_NOT_YET_ACCEPTED' as const,
'The order is not yet accepted by any driver.',
)
}
completeRide() {
return err(
'ERR_NOT_YET_STARTED' as const,
'The order is not yet accepted by any driver.',
)
}
cancel(by: 'customer' | 'driver') {
if (by === 'driver')
return err(
'ERR_INVALID_CANCELER' as const,
'The Order cannot be cancelled by driver: no driver has accepted the order yet',
)
this.#taxiOrder.setState(new TaxiOrderCancelledState(by))
return ok()
}
}
class TaxiOrderAcceptedState implements ITaxiOrderState {
name = 'Accepted'
#taxiOrder: ITaxiOrderContext
constructor(taxiOrder: ITaxiOrderContext) {
this.#taxiOrder = taxiOrder
}
place() {
return err('ERR_ALREADY_PLACED' as const, 'The order is alrady accepted')
}
accept() {
return err(
'ERR_ALREADY_ACCEPTED' as const,
'Order has already been accepted.',
)
}
startRide(passengersNumber: number) {
this.#taxiOrder.setPassengersNumber(passengersNumber)
this.#taxiOrder.setState(new TaxiOrderInProgressState(this.#taxiOrder))
return ok()
}
completeRide() {
return err('ERR_NOT_YET_STARTED' as const, 'Ride has not yet started.')
}
cancel(by: 'customer' | 'driver') {
this.#taxiOrder.setState(new TaxiOrderCancelledState(by))
return ok()
}
}
class TaxiOrderInProgressState implements ITaxiOrderState {
name = 'InProgress'
#taxiOrder: ITaxiOrderContext
constructor(taxiOrder: ITaxiOrderContext) {
this.#taxiOrder = taxiOrder
}
place() {
return err(
'ERR_ALREADY_PLACED' as const,
'The ride has already been started',
)
}
accept() {
return err(
'ERR_ALREADY_ACCEPTED' as const,
'The ride has already been started.',
)
}
startRide() {
return err(
'ERR_ALREADY_STARTED' as const,
'Order has already been started.',
)
}
completeRide(actualFinish: Address) {
this.#taxiOrder.setActualFinish(actualFinish)
this.#taxiOrder.setState(new TaxiOrderCompletedState())
return ok()
}
cancel() {
return err(
'ERR_ALREADY_STARTED' as const,
'Order has already been started. Complite ride with currect location',
)
}
}
class TaxiOrderCancelledState implements ITaxiOrderState {
name = 'Cancelled'
by: string | null
constructor(by: string | null) {
this.by = by
}
place() {
return err(
'ERR_HAS_BEEN_CANCELLED' as const,
'Order has already been cancelled.',
)
}
accept() {
return err(
'ERR_HAS_BEEN_CANCELLED' as const,
'Order has already been cancelled.',
)
}
startRide() {
return err(
'ERR_HAS_BEEN_CANCELLED' as const,
'Order has already been cancelled.',
)
}
completeRide() {
return err(
'ERR_HAS_BEEN_CANCELLED' as const,
'Order has already been cancelled.',
)
}
cancel() {
return err(
'ERR_HAS_BEEN_CANCELLED' as const,
'Order has already been cancelled.',
)
}
}
class TaxiOrderCompletedState implements ITaxiOrderState {
name = 'Completed'
place() {
return err(
'ERR_ALREADY_PLACED' as const,
'Order has already been completed.',
)
}
accept() {
return err(
'ERR_ALREADY_ACCEPTED' as const,
'Order has already been completed.',
)
}
startRide() {
return err(
'ERR_ALREADY_STARTED' as const,
'Order has already been completed.',
)
}
completeRide() {
return err(
'ERR_ALREADY_COMPLETED' as const,
'Order has already been completed.',
)
}
cancel() {
return err(
'ERR_ALREADY_COMPLETED' as const,
'Order has already been completed.',
)
}
}
class TaxiOrder {
#customer: Person | null = null
#driver: Driver | null = null
#start: Address | null = null
#finish: Address | null = null
#actualFinish: Address | null = null
#passengersNumber: number | null = null
#state!: ITaxiOrderState
#history: Array<{ date: Date; state: ITaxiOrderState }> = []
constructor() {
this.#ctx.setState(new TaxiOrderDraftState(this.#ctx))
}
#ctx: ITaxiOrderContext = {
setState: (state: ITaxiOrderState) => {
this.#state = state
this.#history.push({ date: new Date(), state })
},
setCustomer: (customer: Person) => {
this.#customer = customer
},
setStart: (start: Address) => {
this.#start = start
},
setFinish: (finish: Address) => {
this.#finish = finish
},
setDriver: (driver: Driver) => {
this.#driver = driver
},
setPassengersNumber: (passengersNumber: number) => {
this.#passengersNumber = passengersNumber
},
setActualFinish: (actualFinish: Address) => {
this.#actualFinish = actualFinish
},
}
place(customer: Person, start: Address, finish: Address) {
return this.#state.place(customer, start, finish)
}
accept(driver: Driver) {
return this.#state.accept(driver)
}
startRide(passengersNumber: number) {
return this.#state.startRide(passengersNumber)
}
completeRide(actualFinish: Address) {
return this.#state.completeRide(actualFinish)
}
cancel(by: 'customer' | 'driver') {
return this.#state.cancel(by)
}
toJSON() {
return {
state: this.#state.name,
customer: this.#customer,
driver: this.#driver,
start: this.#start,
finish: this.#finish,
actualFinish: this.#actualFinish,
passengersNumber: this.#passengersNumber,
history: this.#history,
}
}
}
const customer: Person = {
id: 'p1234',
name: 'Jhon Doe',
rating: 5,
registeredAt: new Date('2020-12-01'),
}
const driver: Driver = {
id: 'd4321',
name: 'Mark Twen',
rating: 5,
registeredAt: new Date('2024-01-01'),
licenseLevel: 'C',
}
const order = new TaxiOrder()
console.log(order.toJSON())
console.log(
'Place:',
order.place(
customer,
{
country: 'ua',
city: 'Kyiv',
street: 'blv. Lesi Ukrainki',
building: '12',
},
{ country: 'ua', city: 'Kyiv', street: 'Tsitadelna', building: '7' },
),
)
console.log(order.toJSON())
console.log('Cancel (by driver)', order.cancel('driver'))
console.log('Accept', order.accept(driver))
console.log(order.toJSON())
console.log('Start', order.startRide(2))
console.log('Cancel', order.cancel('customer'))
console.log(order.toJSON())
console.log(
'Complete',
order.completeRide({
country: 'ua',
city: 'Kyiv',
street: 'Tsitadelna',
building: '6',
}),
)
console.log(order.toJSON())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment