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.
// The Facade pattern conceptclassSubSystemClassA{// A hypothetically complicated classmethod():string{return'A'}}classSubSystemClassB{// A hypothetically complicated classmethod(value:string):string{returnvalue}}classSubSystemClassC{// A hypothetically complicated classmethod(value:{C:number[]}):{C:number[]}{returnvalue}}classFacade{// A simplified facade offering the services of subsystemssubSystemClassA():string{// Uses the subsystems methodreturnnewSubSystemClassA().method()}subSystemClassB(value:string):string{// Uses the subsystems methodreturnnewSubSystemClassB().method(value)}subSystemClassC(value:{C:number[]}):{C:number[]}{// Uses the subsystems methodreturnnewSubSystemClassC().method(value)}}// The Client// Calling potentially complicated subsystems directlyconsole.log(newSubSystemClassA().method())console.log(newSubSystemClassB().method('B'))console.log(newSubSystemClassC().method({C:[1,2,3]}))// or using the simplified facade insteadconstFACADE=newFacade()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]}
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.
// The Facade Example Use CaseimportGameAPIfrom'./game-api'functionsleep(ms:number){returnnewPromise((resolve)=>setTimeout(resolve,ms))}asyncfunctionfacadeExample(){constgameAPI=newGameAPI()constuser={user_name:'sean'}constuserId=gameAPI.registerUser(user)awaitsleep(500)gameAPI.submitEntry(userId,5)awaitsleep(500)console.log()console.log('---- GameState Snapshot ----')console.log(gameAPI.gameState())awaitsleep(1000)constHISTORY=gameAPI.getHistory()console.log()console.log('---- Reports History ----')Object.keys(HISTORY).forEach((key)=>{console.log(`${key} : ${HISTORY[key][0]} : ${HISTORY[key][1]}`)})awaitsleep(1000)console.log()console.log('---- User Balance ----')console.log(user.user_name+' : '+gameAPI.getBalance(userId))awaitsleep(1000)console.log()console.log('---- GameState Snapshot ----')console.log(gameAPI.gameState())}facadeExample()
// The Game API facadeimportReportsfrom'./reports'importWalletsfrom'./wallets'importUsersfrom'./users'importGameEngine,{GameState}from'./game-engine'exportdefaultclassGameAPI{#wallets:Wallets#reports:Reports#users:Users#gameEngine:GameEngineconstructor(){this.#wallets=newWallets()this.#reports=newReports()this.#users=newUsers()this.#gameEngine=newGameEngine()}getBalance(userId:string):number{// Get a players balancereturnthis.#wallets.getBalance(userId)}gameState():GameState{// Get the current game statereturnthis.#gameEngine.getGameState()}getHistory():{[id:string]:[number,string]}{// get the game historyreturnthis.#reports.getHistory()}changePwd(userId:string,password:string):boolean{// change users passwordreturnthis.#users.changePwd(userId,password)}submitEntry(userId:string,entry:number):boolean{// submit a betreturnthis.#gameEngine.submitEntry(userId,entry)}registerUser(value:{[id:string]:string}):string{// register a new user and returns the new idreturnthis.#users.registerUser(value)}}
// A Singleton Dictionary of UsersimportReportsfrom'./reports'importWalletsfrom'./wallets'exportdefaultclassUsers{staticinstance:Users#users:{[id:string]:{[id:string]:string}}={}#reports=newReports()#wallets=newWallets()constructor(){if(Users.instance){returnUsers.instance}Users.instance=this}registerUser(newUser:{[id:string]:string}):string{// register a userif(!(newUser['user_name']inthis.#users)){// generate really complicated unique user_id.// Using the existing user_name as the id for simplicityconstuserId=newUser['user_name']this.#users[userId]=newUserthis.#reports.logEvent(`new user '${userId}' created`)// create a wallet for the new userthis.#wallets.createWallet(userId)// give the user a sign up bonusthis.#reports.logEvent(`Give new user '${userId}' sign up bonus of 10`)this.#wallets.adjustBalance(userId,10)returnuserId}return''}editUser(userId:string,user:{[id:string]:string}):boolean{// do nothing. Not implemented yetconsole.log(userId)console.log(user)returnfalse}changePwd(userId:string,password:string):boolean{// do nothing. Not implemented yetconsole.log(userId)console.log(password)returnfalse}}
// A Singleton Dictionary of User WalletsimportReportsfrom'./reports'exportdefaultclassWallets{staticinstance:Wallets#wallets:{[id:string]:number}={}#reports=newReports()constructor(){if(Wallets.instance){returnWallets.instance}Wallets.instance=this}createWallet(userId:string):boolean{// A method to initialize a users walletif(!(userIdinthis.#wallets)){this.#wallets[userId]=0this.#reports.logEvent(`wallet for '${userId}' created and set to 0`)returntrue}returnfalse}getBalance(userId:string):number{// A method to check a users balancethis.#reports.logEvent(`Balance check for '${userId}' = ${this.#wallets[userId]}`)returnthis.#wallets[userId]}adjustBalance(userId:string,amount:number):number{// A method to adjust a user balance up or downthis.#wallets[userId]=this.#wallets[userId]+amountthis.#reports.logEvent(`Balance adjustment for '${userId}'. New balance = ${this.#wallets[userId]}`)returnthis.#wallets[userId]}}
./src/facade/reports.ts
1 2 3 4 5 6 7 8 9101112131415161718192021222324
// A Singleton Dictionary of Reported EventsexportdefaultclassReports{staticinstance:Reports#reports:{[id:string]:[number,string]}={}#rowId=0constructor(){if(Reports.instance){returnReports.instance}Reports.instance=this}getHistory():{[id:string]:[number,string]}{returnthis.#reports}logEvent(event:string):boolean{this.#reports[this.#rowId]=[Date.now(),event]this.#rowId=this.#rowId+1returntrue}}
// The Game EngineimportReportsfrom'./reports'importWalletsfrom'./wallets'exporttypeGameState={clock:numbergameOpen:booleanentries:[string,number][]}exportdefaultclassGameEngine{staticinstance:GameEngine#startTime=0#clock=0#entries:[string,number][]=[]#gameOpen=true#reports=newReports()#wallets=newWallets()constructor(){if(GameEngine.instance){returnGameEngine.instance}this.#startTime=Math.floor(Date.now()/1000)this.#clock=60GameEngine.instance=this}getGameState():GameState{// Get a snapshot of the current game stateconstnow=Math.floor(Date.now()/1000)lettimeRemaining=this.#startTime-now+this.#clockconsole.log('getGameState '+timeRemaining)if(timeRemaining<0){timeRemaining=0}this.#gameOpen=falsereturn{clock:timeRemaining,gameOpen:this.#gameOpen,entries:this.#entries,}asGameState}submitEntry(userId:string,entry:number):boolean{// Submit a new entry for the user in this gameconstnow=Math.floor(Date.now()/1000)consttime_remaining=this.#startTime-now+this.#clockif(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}'`)returntrue}this.#reports.logEvent(`Problem adjusting balance for '${userId}'`)returnfalse}this.#reports.logEvent(`User Balance for '${userId}' to low`)returnfalse}this.#reports.logEvent('Game Closed')returnfalse}}
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.