3D編程模式:依賴隔離模式
編輯器使用引擎創建場景
需求
編輯器需要使用引擎來創建一個場景
實現思路
編輯器引入Three.js引擎,調用它來創建場景
給出UML
領域模型

總體來看,領域模型分為用戶、編輯器、引擎這三個部分
我們看下用戶這個部分:
Client是用戶
我們看下編輯器這個部分:
Editor是編輯器
我們看下引擎這個部分:
Three.js是Three.js引擎
給出代碼
我們依次看下每個模塊的代碼,它們包括:
- Client的代碼
- Editor的代碼
Client的代碼
Client
Editor.createScene()
Client調用Editor來創建場景
Editor的代碼
Editor
import {
Scene,
...
} from "three";
export let createScene = function () {
let scene = new Scene();
...
}
Editor引入Three.js引擎,并在createScene函數中調用它來創建場景
提出問題
- 如果要將Three.js引擎替換為Babylon.js引擎,需要哪些修改?
編輯器替換引擎
概述解決方案
- Editor改為引入Babylon.js引擎,并修改Editor中與引擎相關的代碼
給出UML
領域模型

領域模型跟之前一樣,只是將Three.js替換為Babylon.js
結合UML圖,描述如何具體地解決問題
需要進行下面的修改:
- 將Three.js換成Babylon.js
- 修改Editor的相關代碼
給出代碼
Client代碼跟之前一樣,故省略
我們看下Editor的代碼
Editor的代碼
Editor
import {
Scene,
Engine,
...
} from "babylonjs";
export let createScene = function () {
let scene = new Scene(new Engine())
...
}
Editor改為引入Babylon.js引擎,并修改createScene函數中與引擎相關的代碼,改為調用Babylon.js來創建場景
提出問題
- 替換引擎的成本太高
替換引擎需要修改Editor中所有與引擎相關代碼,成本太高了。有沒有辦法能在不修改Editor代碼的情況下實現替換引擎呢?
使用依賴隔離模式來改進
概述解決方案
- 解除依賴
只要解除Editor和引擎的依賴,把替換引擎的邏輯隔離出去就可以實現
給出UML
領域模型

總體來看,領域模型分為用戶、編輯器、引擎、容器這四個部分
我們看下用戶這個部分:
Client是用戶
我們看下編輯器這個部分:
Editor是編輯器
我們看下引擎這個部分:
Engine接口是對引擎的抽象,抽象出了Editor需要的接口
BabylonImplement是使用Babylon.js引擎對Engine接口的實現
ThreeImplement是使用Three.js引擎對Engine接口的實現
Babylon.js是Babylon.js引擎
Three.js是Three.js引擎
我們看下容器這個部分:
DependencyContainer是保存注入的Engine接口實現的容器,提供操作它的get和set函數
我們來看下依賴關系:
Client通過依賴注入的方式注入Engine接口的一個實現(BabylonImplement或者ThreeImplement),從而使Editor能夠調用它來創建場景
結合UML圖,描述如何具體地解決問題
- 替換Three.js為Babylon.js引擎現在不再影響Editor了,只需要增加BabylonImplement,并讓Client從注入ThreeImplement改為注入BabylonImplement即可
因為Editor只依賴Engine接口,所以Engine接口的實現的變化不會影響Editor
給出代碼
我們依次看下各個部分的代碼,它們包括:
- 用戶的代碼
- 編輯器的代碼
- 容器的代碼
- 引擎的代碼
用戶的代碼
Client
Editor.injectDependencies(BabylonImplement.implement())
Editor.createScene()
Client首先注入了BabylonImplement;然后創建了場景
編輯器的代碼
Editor
export let injectDependencies = function (implement: Engine) {
DependencyContainer.setEngine(implement);
};
export let createScene = function () {
let { createScene, ...}: Engine = DependencyContainer.getEngine()
let scene = createScene()
...
}
Editor增加了injectDependencies函數,它將Client注入的Engine接口實現保存到DependencyContainer中
createScene函數通過DependencyContainer獲得注入的Engine接口實現,它的類型是Engine接口,用來創建場景
值得注意的是:
Editor只依賴Engine接口,沒有依賴Engine接口的實現
容器的代碼
DependencyContainer
let _engine: Engine = null
export let getEngine = (): Engine => {
return _engine;
}
export let setEngine = (engine: Engine) {
_engine = engine;
}
DependencyContainer使用_engine這個閉包變量來保存注入的Engine接口實現(當然也可以保存到一個state中;或者將DependencyContainer改為一個類,從而保存到它的私有成員中),并提供了get和set函數來獲得和保存注入的Engine接口實現
引擎的代碼
Engine
//scene為抽象類型
//這里用any類型表示抽象類型
type scene = any
export interface Engine {
createScene(): scene
...
}
Engine接口抽象出了Editor需要的接口,其中包括createScene函數
ThreeImplement
import {
Scene,
...
} from "three";
export let implement = (): Engine => {
return {
createScene: () => {
return new Scene();
},
...
}
}
ThreeImplement的implement函數使用了Three.js引擎,返回了Engine接口的實現
BabylonImplement
import {
Scene,
Engine,
...
} from "babylonjs";
export let implement = (): Engine => {
return {
createScene: () => {
return new Scene(new Engine())
},
...
}
}
BabylonImplement的implement函數使用了Babylon.js引擎,返回了Engine接口的實現
定義
一句話定義
隔離系統的外部依賴,使得外部依賴的變化不會影響系統
補充說明
將外部依賴隔離后,系統變得更“純”了,類似于函數式編程中“純函數”的概念
哪些依賴屬于外部依賴呢?依賴的各種第三方庫、外部環境等都屬于外部依賴。具體來說:
- 對于編輯器而言,引擎、UI組件庫(如Ant Design)、后端、文件操作、日志等都屬于外部依賴;
- 對于引擎而言,各種子引擎(如物理引擎、動畫引擎、粒子引擎)、后端、文件操作、日志等都屬于外部依賴
可以將每個可能會變化的外部依賴都抽象為接口,從而使用依賴隔離模式將其隔離出去
通用UML
領域模型

總體來看,領域模型分為用戶、系統、外部依賴、容器這四個部分
我們看下用戶這個部分:
- Client
該角色是用戶,通過依賴注入的方式注入DependencyImplement
我們看下系統這個部分:
- System
該角色使用了外部依賴,它只知道外部依賴的接口(Dependency)而不知道具體實現(DependencyImplement)
我們看下外部依賴這個部分:
- Dependency
該角色是外部依賴的接口 - DependencyImplement
該角色是Dependency的實現 - DependencyLibrary
該角色是一個庫
我們看下容器這個部分:
- DependencyContainer
該角色是保存注入的DependencyImplement的容器,提供操作它的get和set函數
角色之間的關系
-
可以有多個Dependency
如除了Engine以外,還可以有File、Server等Dependency,其中每個Dependency對應一個外部依賴 -
一個Dependency可以有多個DependencyImplement來實現
如Engine的實現除了有ThreeImplement,還可以有BabylonImplement等實現 -
Client可以依賴注入多個Dependency接口的實現。其中,對于一個Dependency接口而言,Client只依賴注入實現它的一個DependencyImplement
-
因為System可以使用多個Dependency接口,所以它們是一對多的關系
-
一個DependencyImplement一般只使用一個DependencyLibrary,但也可以使用多個DependencyLibrary
如可以增加實現Engine接口的ThreeAndBabylonImplement,它同時使用Three.js和Babylon.js這兩個DependencyLibrary來創建場景 -
只有一個DependencyContainer容器,它保存了所有注入的DependencyImplement,為每個DependencyImplement都提供了get和set函數
角色的抽象代碼
下面我們來看看各個角色的抽象代碼:
我們依次看下各個部分的抽象代碼,它們包括:
- 用戶的抽象代碼
- 系統的抽象代碼
- 容器的抽象代碼
- 外部依賴的抽象代碼
用戶的抽象代碼
Client
System.injectDependencies(Dependency1Implement1.implement(), 其它DependencyImplement...)
System.doSomethingUseDependency1()
系統的抽象代碼
System
export let injectDependencies = function (dependency1Implement1: Dependency1, ...) {
DependencyContainer.setDependency1(dependency1Implement1)
注入其它DependencyImplement...
};
export let doSomethingUseDependency1 = function () {
let { abstractOperate1, ...}: Dependency1 = DependencyContainer.getDependency1()
let value1: abstractType1 = abstractOperate1()
...
}
更多doSomethingUseDependencyX函數...
容器的抽象代碼
DependencyContainer
let _dependency1: Dependency1 = null
更多的_dependencyX...
export let getDependency1 = (): Dependency1 => {
return _dependency1;
}
export let setDependency1 = (dependency1: Dependency1) {
_dependency1 = dependency1;
}
更多的get和set函數(如getDependency2、setDependency2)...
外部依賴的抽象代碼
Dependency1
type abstractType1 = any;
...
export interface Dependency1 {
abstractOperate1(): abstractType1,
...
}
有多個Dependency,這里只給出一個Dependency的抽象代碼
Dependency1Implement1
import {
api1,
...
} from "dependencylibrary1";
export let implement = (): Dependency1 => {
return {
abstractOperate1: () => {
使用api1...
},
...
}
}
DependencyLibrary1
export let api1 = function () {
...
}
...
有多個DependencyImplement和多個DependencyLibrary,這里只給出一個DependencyImplement和一個DependencyLibrary的抽象代碼
遵循的設計原則在UML中的體現
依賴隔離模式主要遵循下面的設計原則:
- 依賴倒置原則
系統依賴于外部依賴的抽象(Dependency)而不是外部依賴的細節(DependencyImplement和DependencyLibrary),從而外部依賴的細節的變化不會影響系統 - 開閉原則
要隔離更多的外部依賴,只需要增加對應的Dependency、DependencyImplement和DependencyLibrary,以及DependencyContainer增加對應的閉包變量和get、set函數即可,無需修改System;要替換外部依賴的實現,只需要對它的Dependency增加更多的DependencyImplement,然后Client改為注入新的DependencyImplement即可,無需修改System;要修改已有的外部依賴(如升級版本),只需要修改DependencyImplement和DependencyLibrary即可,無需修改System
依賴隔離模式也應用了“依賴注入”、“控制反轉”的思想
應用
優點
- 提高系統的穩定性
外部依賴的變化不會影響系統 - 提高系統的擴展性
可以任意替換外部依賴而不影響系統 - 提高系統的可維護性
系統與外部依賴解耦,便于維護
缺點
無
使用場景
場景描述
系統的外部依賴經常變化
具體案例
-
編輯器使用的引擎、UI庫等第三方庫需要替換
-
編輯器使用的引擎、UI庫等第三方庫的版本需要升級
如需要升級編輯器使用的Three.js引擎的版本,只需要升級作為DependencyLibrary的Three.js,并修改ThreeImplement,使其使用升級后的Three.js即可 -
需要增加編輯器使用的引擎、UI庫等第三方庫
如需要讓編輯器在已使用Three.js引擎的基礎上增加使用Babylon.js引擎,則只需要加入ThreeAndBabylonImplement,讓它同時使用Three.js和Babylon.js這兩個DependencyLibrary
注意事項
- Dependency要足夠抽象,才不至于在修改或增加DependencyImplement時需要修改Dependency,從而影響System
當然,在開發階段難免考慮不足,如一開始只有一個DependencyImplement時,Dependency往往只會考慮這個DependencyImplement。這導致在增加實現該Dependency的其它DependencyImplement時發現需要修改Dependency,使其更加抽象,這樣才能容納因增加更多的DependencyImplement而帶來的變化
因此,我們可以允許在開發階段修改Dependency,但是在發布前則確保Dependency已經足夠抽象和穩定
- 最好一開始就使用依賴隔離模式,將所有的可能會變化的外部依賴都隔離,這樣可以避免后期使用依賴隔離模式時導致系統要改動的地方太多的情況
擴展
升級為洋蔥架構
如果基于依賴隔離模式設計一個這樣的架構:
- 劃分4個層:外部依賴層、應用服務層、領域服務層、領域模型層,其中前者為上層,前者依賴后者
- 將系統的所有外部依賴都使用依賴隔離模式來隔離出去,為每個外部依賴抽象一個Dependency接口。將DependencyImplement放到最上層的外部依賴層,將Dependency放到最下層的領域模型層。
這是因為DependencyImplement容易變化,所以將其放到最上層,這樣它的變化不會影響其它層;而Dependency非常穩定不易變化,且被領域模型依賴,所以將其放到領域模型層
- 運用領域驅動設計來設計系統,將系統的核心邏輯建模為領域模型,將其放到領域模型層
那么這樣的架構就是洋蔥架構。洋蔥架構如下圖所示:

洋蔥架構與傳統的三層架構的區別是顛倒了層之間的依賴關系:洋蔥架構將三層架構中的最下層(外部依賴層)改為最上層;將三層架構中的倒數第二層(領域模型層)下降為最下層
洋蔥架構的核心思想就是:
- 將變化最頻繁的外部依賴放在最上層從而不影響其它層
- 將領域模型放在最下層,使其不受其它層的影響。雖然它的變化會影響其它層,但是它通常比較穩定,不容易變化
最佳實踐
哪些場景不需要使用模式
-
系統的外部依賴比較穩定,不易變化
-
開發Demo或者開發短期使用的系統
對于上面的場景,可以在系統中直接使用外部依賴而不需要使用依賴隔離模式將其隔離,從而能最快地開發系統
給出具體的實踐案例
擴大使用場景
編輯器的外部依賴不只是引擎,也包括UI組件庫等。如需要將舊的UI組件庫(如React UI 組件庫-Antd)替換為新的組件庫,則可使用依賴隔離模式,提出UI這個Dependency接口,并加入作為UI接口實現的OldUIImplement、NewUIImplement,它們調用對應的UI組件庫;然后讓Client從注入OldUIImplement改為注入NewUIImplement
除了編輯器外,引擎、網站等系統也可以使用依賴隔離模式。如需要替換引擎的物理引擎、將網站的后端從阿里云換成騰訊云等場景,都可以使用依賴隔離模式
外部依賴在運行時會變化
有些外部依賴在運行時會變化,對于這種情況,使用依賴隔離模式后可以在運行時注入變化后的DependencyImplement,從而切換外部依賴。如編輯器向用戶提供了“切換渲染效果”的功能,它的需求是:用戶點擊一個按鈕后,切換不同的引擎來渲染場景
因為引擎的接口是同一個Dependency,所以為了實現該功能,只需在按鈕的點擊事件中注入實現該Dependency的對應的DependencyImplement到DependencyContainer中即可
滿足各種修改外部依賴的用戶需求
我遇到過這種問題:3D應用開發完成后,交給3個外部用戶使用。用了一段時間后,這3個用戶提出了不同的修改外部依賴的要求:第一個用戶想要升級3D應用依賴的引擎A,第二個用戶想要替換引擎A為引擎B,第三個用戶想要同時使用引擎B和升級后的引擎A。
如果3D應用沒有使用依賴隔離模式,而是使用調用引擎這個外部依賴的話,我們就需要將交付的代碼修改為3個版本,分別滿足3個用戶的需求。交付的3個版本的代碼如下:
- 使用了升級A的3D應用代碼
- 使用了B的3D應用代碼
- 使用了B和升級A的3D應用代碼
因為每個版本都需要修改3D應用中與引擎相關的代碼,所以導致工作量很大。如果使用了依賴隔離模式進行解耦,那么就只需要對3D應用做下面的修改:
- 修改AImplement和ALibrary,實現升級
- 增加BLibrary
- 增加BImplement,它使用BLibrary
- 增加ABImplement,它使用BLibrary和升級后的ALibrary
- DependencyContainer增加保存B、AB的閉包變量和對應的get、set函數
這樣的話,交付的代碼就只有1個版本,從而減少了很多工作量。只是該版本的代碼需要在Client中分別為這3個用戶注入不同的DependencyImplement,具體如下:
- Client為第一個用戶注入升級后的AImplement
- Client為第二個用戶注入BImplement
- Client為第三個用戶注入ABImplement
更多資料推薦
可以在網上搜索“依賴注入 控制反轉 依賴倒置”來找到依賴注入思想的資料
可以在網上搜索“洋蔥架構”、“the-onion-architecture-part”來找到洋蔥架構的資料
六邊形架構類似于洋蔥架構,可以在網上搜索“六邊形架構”來找到它的資料
大家好~本文提出了“依賴隔離”模式,我們定義依賴隔離模式為:隔離系統的外部依賴,使得外部依賴的變化不會影響系統
浙公網安備 33010602011771號