Facade Design Pattern
Video Lecture
Overview
Sometimes you have a system that becomes quite complex over time as more features are added or modified. It may be useful to provide a simplified API over it. This is the Facade pattern.
The Facade pattern essentially is an alternative, reduced or simplified interface to a set of other interfaces, abstractions and implementations within a system that may be full of complexity and/or tightly coupled.
It can also be considered as a higher-level interface that shields the consumer from the unnecessary low-level complications of integrating into many subsystems.
Facade UML Diagram
Source Code
./src/facade/facade-concept.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 | // 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 ] }
SBCODE Editor
Facade Use Case
This is an example of a game engine API. The facade layer is creating one streamlined interface consisting of several methods from several larger API backend systems.
The client could connect directly to each subsystem's API and implement its authentication protocols, specific methods, etc. While it is possible, it would be quite a lot of consideration for each of the development teams, so the facade API unifies the common methods that becomes much less overwhelming for each new client developer to integrate into.
Example UML Diagram
Source Code
./src/facade/client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | // 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 | // 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 | // 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | // 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | // 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70 | // 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 ] ] }
SBCODE Editor
Summary
- Use when you want to provide a simple interface to a complex subsystem.
- You want to layer your subsystems into an abstraction that is easier to understand.
- Abstract Factory and Facade can be considered very similar. An Abstract Factory is about creating in interface over several creational classes of similar objects, whereas the Facade is more like an API layer over many creational, structural and/or behavioral patterns.
- The Mediator is similar to the Facade in the way that it abstracts existing classes. The Facade is not intended to modify, load balance or apply any extra logic. A subsystem does not need to consider that existence of the facade, it would still work without it.
- A Facade is a minimal interface that could also be implemented as a Singleton.
- A Facade is an optional layer that does not alter the subsystem. The subsystem does not need to know about the Facade, and could even be used by many other facades created for different audiences.