Skip to content

Facade Design Pattern

Overview

...Refer to Book or Videos for extra content.

Facade UML Diagram

Facade Design Pattern

Source Code

./src/facade/facade-concept.ts

// The Facade pattern concept

class SubSystemClassA {
    // A hypothetically complicated class
    method(): string {
        return 'A'
    }
}

class SubSystemClassB {
    // A hypothetically complicated class
    method(value: string): string {
        return value
    }
}

class SubSystemClassC {
    // A hypothetically complicated class
    method(value: { C: number[] }): { C: number[] } {
        return value
    }
}

class Facade {
    // A simplified facade offering the services of subsystems
    subSystemClassA(): string {
        // Uses the subsystems method
        return new SubSystemClassA().method()
    }

    subSystemClassB(value: string): string {
        // Uses the subsystems method
        return new SubSystemClassB().method(value)
    }

    subSystemClassC(value: { C: number[] }): { C: number[] } {
        // Uses the subsystems method
        return new SubSystemClassC().method(value)
    }
}

// The Client
// Calling potentially complicated subsystems directly
console.log(new SubSystemClassA().method())
console.log(new SubSystemClassB().method('B'))
console.log(new SubSystemClassC().method({ C: [1, 2, 3] }))

// or using the simplified facade instead
const FACADE = new Facade()
console.log(FACADE.subSystemClassA())
console.log(FACADE.subSystemClassB('B'))
console.log(FACADE.subSystemClassC({ C: [1, 2, 3] }))

Output

node ./dist/facade/facade-concept.js
A
B
{ C: [ 1, 2, 3 ] }
A
B
{ C: [ 1, 2, 3 ] }

Facade Use Case

...Refer to Book or Videos for extra content.

Example UML Diagram

Facade Example UML Diagram

Source Code

./src/facade/client.ts

// The Facade Example Use Case

import GameAPI from './game-api'

function sleep(ms: number) {
    return new Promise((resolve) => setTimeout(resolve, ms))
}

async function facadeExample() {
    const gameAPI = new GameAPI()

    const user = { user_name: 'sean' }
    const userId = gameAPI.registerUser(user)

    await sleep(500)

    gameAPI.submitEntry(userId, 5)

    await sleep(500)

    console.log()
    console.log('---- GameState Snapshot ----')
    console.log(gameAPI.gameState())

    await sleep(1000)

    const HISTORY = gameAPI.getHistory()

    console.log()
    console.log('---- Reports History ----')
    Object.keys(HISTORY).forEach((key) => {
        console.log(`${key} : ${HISTORY[key][0]} : ${HISTORY[key][1]}`)
    })

    await sleep(1000)

    console.log()
    console.log('---- User Balance ----')
    console.log(user.user_name + ' : ' + gameAPI.getBalance(userId))

    await sleep(1000)

    console.log()
    console.log('---- GameState Snapshot ----')
    console.log(gameAPI.gameState())
}
facadeExample()

./src/facade/game-api.ts

// The Game API facade

import Reports from './reports'
import Wallets from './wallets'
import Users from './users'
import GameEngine, { GameState } from './game-engine'

export default class GameAPI {
    #wallets: Wallets
    #reports: Reports
    #users: Users
    #gameEngine: GameEngine

    constructor() {
        this.#wallets = new Wallets()
        this.#reports = new Reports()
        this.#users = new Users()
        this.#gameEngine = new GameEngine()
    }

    getBalance(userId: string): number {
        // Get a players balance
        return this.#wallets.getBalance(userId)
    }

    gameState(): GameState {
        // Get the current game state
        return this.#gameEngine.getGameState()
    }

    getHistory(): { [id: string]: [number, string] } {
        // get the game history
        return this.#reports.getHistory()
    }

    changePwd(userId: string, password: string): boolean {
        // change users password
        return this.#users.changePwd(userId, password)
    }

    submitEntry(userId: string, entry: number): boolean {
        // submit a bet
        return this.#gameEngine.submitEntry(userId, entry)
    }

    registerUser(value: { [id: string]: string }): string {
        // register a new user and returns the new id
        return this.#users.registerUser(value)
    }
}

./src/facade/users.ts

// A Singleton Dictionary of Users

import Reports from './reports'
import Wallets from './wallets'

export default class Users {
    static instance: Users
    #users: { [id: string]: { [id: string]: string } } = {}
    #reports = new Reports()
    #wallets = new Wallets()

    constructor() {
        if (Users.instance) {
            return Users.instance
        }
        Users.instance = this
    }

    registerUser(newUser: { [id: string]: string }): string {
        // register a user
        if (!(newUser['user_name'] in this.#users)) {
            // generate really complicated unique user_id.
            // Using the existing user_name as the id for simplicity
            const userId = newUser['user_name']
            this.#users[userId] = newUser
            this.#reports.logEvent(`new user '${userId}' created`)
            // create a wallet for the new user
            this.#wallets.createWallet(userId)
            // give the user a sign up bonus
            this.#reports.logEvent(
                `Give new user '${userId}' sign up bonus of 10`
            )
            this.#wallets.adjustBalance(userId, 10)
            return userId
        }
        return ''
    }

    editUser(userId: string, user: { [id: string]: string }): boolean {
        // do nothing. Not implemented yet
        console.log(userId)
        console.log(user)
        return false
    }

    changePwd(userId: string, password: string): boolean {
        // do nothing. Not implemented yet
        console.log(userId)
        console.log(password)
        return false
    }
}

./src/facade/wallets.ts

// A Singleton Dictionary of User Wallets

import Reports from './reports'

export default class Wallets {
    static instance: Wallets
    #wallets: { [id: string]: number } = {}
    #reports = new Reports()

    constructor() {
        if (Wallets.instance) {
            return Wallets.instance
        }
        Wallets.instance = this
    }

    createWallet(userId: string): boolean {
        // A method to initialize a users wallet
        if (!(userId in this.#wallets)) {
            this.#wallets[userId] = 0
            this.#reports.logEvent(
                `wallet for '${userId}' created and set to 0`
            )
            return true
        }
        return false
    }

    getBalance(userId: string): number {
        // A method to check a users balance
        this.#reports.logEvent(
            `Balance check for '${userId}' = ${this.#wallets[userId]}`
        )
        return this.#wallets[userId]
    }

    adjustBalance(userId: string, amount: number): number {
        // A method to adjust a user balance up or down
        this.#wallets[userId] = this.#wallets[userId] + amount
        this.#reports.logEvent(
            `Balance adjustment for '${userId}'. New balance = ${
                this.#wallets[userId]
            }`
        )
        return this.#wallets[userId]
    }
}

./src/facade/reports.ts

// A Singleton Dictionary of Reported Events

export default class Reports {
    static instance: Reports
    #reports: { [id: string]: [number, string] } = {}
    #rowId = 0

    constructor() {
        if (Reports.instance) {
            return Reports.instance
        }
        Reports.instance = this
    }

    getHistory(): { [id: string]: [number, string] } {
        return this.#reports
    }

    logEvent(event: string): boolean {
        this.#reports[this.#rowId] = [Date.now(), event]
        this.#rowId = this.#rowId + 1
        return true
    }
}

./src/facade/game-engine.ts

// The Game Engine

import Reports from './reports'
import Wallets from './wallets'

export type GameState = {
    clock: number
    gameOpen: boolean
    entries: [string, number][]
}

export default class GameEngine {
    static instance: GameEngine
    #startTime = 0
    #clock = 0
    #entries: [string, number][] = []
    #gameOpen = true
    #reports = new Reports()
    #wallets = new Wallets()

    constructor() {
        if (GameEngine.instance) {
            return GameEngine.instance
        }
        this.#startTime = Math.floor(Date.now() / 1000)
        this.#clock = 60
        GameEngine.instance = this
    }

    getGameState(): GameState {
        // Get a snapshot of the current game state
        const now = Math.floor(Date.now() / 1000)
        let timeRemaining = this.#startTime - now + this.#clock
        console.log('getGameState ' + timeRemaining)
        if (timeRemaining < 0) {
            timeRemaining = 0
        }
        this.#gameOpen = false
        return {
            clock: timeRemaining,
            gameOpen: this.#gameOpen,
            entries: this.#entries,
        } as GameState
    }

    submitEntry(userId: string, entry: number): boolean {
        // Submit a new entry for the user in this game
        const now = Math.floor(Date.now() / 1000)
        const time_remaining = this.#startTime - now + this.#clock
        if (time_remaining > 0) {
            if (this.#wallets.getBalance(userId) > 1) {
                if (this.#wallets.adjustBalance(userId, -1)) {
                    this.#entries.push([userId, entry])
                    this.#reports.logEvent(
                        `New entry '${entry}' submitted by '${userId}'`
                    )
                    return true
                }
                this.#reports.logEvent(
                    `Problem adjusting balance for '${userId}'`
                )
                return false
            }
            this.#reports.logEvent(`User Balance for '${userId}' to low`)
            return false
        }
        this.#reports.logEvent('Game Closed')
        return false
    }
}

Output

node ./dist/facade/client.js

---- GameState Snapshot ----
getGameState 59
{ clock: 59, gameOpen: false, entries: [ [ 'sean', 5 ] ] }

---- Reports History ----
0 : 1619260983800 : new user 'sean' created
1 : 1619260983800 : wallet for 'sean' created and set to 0
2 : 1619260983800 : Give new user 'sean' sign up bonus of 10
3 : 1619260983800 : Balance adjustment for 'sean'. New balance = 10
4 : 1619260984312 : Balance check for 'sean' = 10
5 : 1619260984312 : Balance adjustment for 'sean'. New balance = 9
6 : 1619260984312 : New entry '5' submitted by 'sean'

---- User Balance ----
sean : 9

---- GameState Snapshot ----
getGameState 56
{ clock: 56, gameOpen: false, entries: [ [ 'sean', 5 ] ] }

unknown v any

Summary

...Refer to Book or Videos for extra content.