By Diego Liascovich
Full-Stack Developer | Microservices | Angular | Node.js
π What is the State Design Pattern?
The State Pattern is a behavioral design pattern that lets an object alter its behavior when its internal state changes. The object will appear to change its class.
Rather than using conditionals, each state is encapsulated in a separate class. The object delegates behavior to the current state class.
π§± Key Components
- Context: Holds a reference to the current state and delegates operations.
- State Interface: Defines the common interface for all states.
- Concrete States: Represent different states with unique behavior.
π‘ Real Case: Billing Invoice Lifecycle
A billing invoice can go through different states:
Created
Approved
Paid
Canceled
Each state defines what actions are valid. For example:
- A
Created
invoice can be approved or canceled. - An
Approved
invoice can be paid or canceled. - A
Paid
invoice cannot be modified. - A
Canceled
invoice is read-only.
π οΈ TypeScript Implementation
1. State Interface
// BillingState.ts
export interface BillingState {
approve(): void;
pay(): void;
cancel(): void;
}
2. Concrete States
// CreatedState.ts
import { BillingState } from './BillingState';
import { BillingContext } from './BillingContext';
import { ApprovedState } from './ApprovedState';
import { CanceledState } from './CanceledState';
export class CreatedState implements BillingState {
constructor(private context: BillingContext) {}
approve(): void {
console.log("Invoice approved.");
this.context.setState(new ApprovedState(this.context));
}
pay(): void {
console.log("Cannot pay a created invoice.");
}
cancel(): void {
console.log("Invoice canceled.");
this.context.setState(new CanceledState(this.context));
}
}
// ApprovedState.ts
import { BillingState } from './BillingState';
import { BillingContext } from './BillingContext';
import { PaidState } from './PaidState';
import { CanceledState } from './CanceledState';
export class ApprovedState implements BillingState {
constructor(private context: BillingContext) {}
approve(): void {
console.log("Already approved.");
}
pay(): void {
console.log("Invoice paid.");
this.context.setState(new PaidState(this.context));
}
cancel(): void {
console.log("Invoice canceled.");
this.context.setState(new CanceledState(this.context));
}
}
// PaidState.ts
import { BillingState } from './BillingState';
export class PaidState implements BillingState {
approve(): void {
console.log("Invoice already paid. Cannot approve.");
}
pay(): void {
console.log("Invoice already paid.");
}
cancel(): void {
console.log("Invoice already paid. Cannot cancel.");
}
}
// CanceledState.ts
import { BillingState } from './BillingState';
export class CanceledState implements BillingState {
approve(): void {
console.log("Invoice is canceled. Cannot approve.");
}
pay(): void {
console.log("Invoice is canceled. Cannot pay.");
}
cancel(): void {
console.log("Invoice already canceled.");
}
}
3. Context Class
// BillingContext.ts
import { BillingState } from './BillingState';
import { CreatedState } from './CreatedState';
export class BillingContext {
private state: BillingState;
constructor() {
this.state = new CreatedState(this); // initial state
}
setState(state: BillingState) {
this.state = state;
}
approve() {
this.state.approve();
}
pay() {
this.state.pay();
}
cancel() {
this.state.cancel();
}
}
β Demo Usage
const invoice = new BillingContext();
invoice.approve(); // "Invoice approved."
invoice.pay(); // "Invoice paid."
invoice.cancel(); // "Invoice already paid. Cannot cancel."
π― Benefits of Using the State Pattern for Billing
- Enforces business rules via state logic.
- Avoids complex conditionals.
- Each state encapsulates its valid transitions and behavior.
- Easy to extend with new states (e.g., "Refunded", "Disputed").
Top comments (0)