Created
March 24, 2025 15:03
-
-
Save DScheglov/5654c4195eec3ff228793e81bccadfad to your computer and use it in GitHub Desktop.
GoF State Pattern
This file contains hidden or 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
/** | |
* 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