大家好~本文提出了“依赖隔离”模式,我们定义依赖隔离模式为:隔离系统的外部依赖,使得外部依赖的变化不会影响系统 大家好~本文提出了“依赖隔离”模式
系列文章详见:
3D编程模式:开篇
本文相关代码在这里:
相关代码
编辑器使用了Three.js引擎作为渲染引擎,来创建一个默认的3D场景
编辑器相关代码Editor:
import { Scene, ...} from "three";export let createScene = function () { let scene = new Scene(); ...}客户相关代码Client:
import { createScene } from "./Editor";createScene()
现在需要升级Three.js引擎的版本,或者替换为其它的渲染引擎,我们会发现非常困难,因为需要修改编辑器中所有调用了该引擎的代码(如需要修改createScene函数),代码一多就不好修改了
有没有办法能在不需要修改编辑器相关代码的情况下,就升级引擎版本或者替换引擎呢?
只要解除编辑器和具体的引擎的依赖,并把引擎升级或替换的逻辑隔离出去就可以!新设计的类图如下所示:
IRenderEngine接口对渲染引擎的API进行抽象
IRenderEngine
//scene为抽象类型,这里用any类型表示type scene = any;export interface IRenderEngine { createScene(): scene ...}ThreeImplement用Three.js引擎实现IRenderEngine接口
ThreeImplement
import { IRenderEngine } from "./IRenderEngine";import { Scene,...} from "three";export let implement = (): IRenderEngine => { return { createScene: () => { return new Scene(); }, ... }}DependencyContainer负责维护由Client注入的IRenderEngine的实现
DependencyContainer:
import {IRenderEngine} from "./IRenderEngine"let _renderEngine: IRenderEngine = nullexport let getRenderEngine = (): IRenderEngine => { return _renderEngine;}export let setRenderEngine = (renderEngine: IRenderEngine) { _renderEngine = renderEngine;}Editor增加injectDependencies函数,用于通过DependencyContainer注入由Client传过来的IRenderEngine的实现;另外还通过DependencyContainer获得注入的IRenderEngine的实现,调用它来创建场景
Editor:
import { getRenderEngine, setRenderEngine } from "./DependencyContainer";import { IRenderEngine } from "./IRenderEngine";export let injectDependencies = function (threeImplement: IRenderEngine) { setRenderEngine(threeImplement);};export let createScene = function () { let { createScene, ...} = getRenderEngine() let scene = createScene() ...}Client选择要注入的IRenderEngine的实现
Client:
import { createScene, injectDependencies } from "./Editor";import { implement } from "./ThreeImplement";injectDependencies(implement())createScene()经过这样的设计后,就把编辑器和渲染引擎隔离开了
现在如果要升级Three.js引擎版本,只需要修改ThreeImplement;如果要替换为Babylon引擎,只需要增加BabylonImplement,修改Client为注入BabylonImplement。它们都不再影响Editor的代码了
隔离系统的外部依赖
我们定义依赖隔离模式为:
隔离系统的外部依赖,使得外部依赖的变化不会影响系统
将外部依赖隔离后,系统变得更“纯”了,类似于函数式编程中的“纯函数”的概念,消除了外部依赖带来了副作用
那么哪些依赖属于外部依赖呢?对于编辑器而言,渲染引擎、后端服务、文件操作、日志等都属于外部依赖;对于网站而言,UI组件库(如Ant Design)、后端服务、数据库操作等都属于外部依赖。可以将每个外部依赖都抽象为对应的IDendency接口,从而都隔离出去。

我们来看看依赖隔离模式的相关角色:
各个角色之间的关系为:
下面我们来看看各个角色的抽象代码:
type abstractType1 = any;...export interface IDependency1 { abstractAPI1(): abstractType1, ...}import { IDependency1 } from "./IDependency1";import { api1,...} from "dependencylibrary1";export let implement = (): IDependency1 => { return { abstractAPI1: () => { ... return api1() }, ... }}export let api1 = function () { ...}...import { IDependency1 } from "./IDependency1"let _dependency1: IDependency1 = null...export let getDependency1 = (): IDependency1 => { return _dependency1;}export let setDependency1 = (dependency1: IDependency1) { _dependency1 = dependency1;}...import { getDependency1, setDependency1 } from "./DependencyContainer";import { IDependency1 } from "./IDependency1";export let injectDependencies = function (dependency1Implement1: IDependency1, ...) { setDependency1(dependency1Implement1) 注入其它依赖实现...};export let doSomethingNeedDependency1 = function () { let { abstractAPI1, ...} = getDependency1() let abstractType1 = abstractAPI1() ...}import { doSomethingNeedDependency1, injectDependencies } from "./System";import { implement } from "./Dependency1Implement1";injectDependencies(implement(), 其它依赖实现...)doSomethingNeedDependency1()依赖隔离模式主要遵循下面的设计原则:
依赖隔离模式也应用了“依赖注入”、“控制反转”的思想
优点
缺点
无
使用场景
注意事项
如果基于依赖隔离模式这样设计一个架构:
那么这样的架构就是洋葱架构
洋葱架构如下图所示:
它的核心思想就是将变化最频繁的外部依赖层隔离出去,并使变化最少的领域模型层独立而不依赖其它层。
在传统的架构中,领域模型层会依赖外部依赖层(如在领域模型中调用后端服务等),但是现在却解耦了。这样的好处就是如果外部依赖层变化,不会影响其他层
如果只是开发Demo或者短期使用的系统,可以在系统中直接调用外部依赖库,这样开发得最快;
如果要开发长期维护的系统(如编辑器),则最好一开始就使用依赖隔离模式将所有的外部依赖都隔离,或者升级为洋葱架构。这样可以避免到后期如果要修改外部依赖时需要修改系统所有相关代码的情况。
我遇到过这种问题:3D应用开发完成后,交给3个外部用户使用。用了一段时间后,这3个用户提出了不同的修改外部依赖的要求:第一个用户想要升级3D应用依赖的渲染引擎A,第二个用户想要替换A为B,第三个用户想要同时使用B和升级后的A来渲染。
如果3D应用是直接调用外部依赖库的话,我们就需要去修改交付的3份代码中系统的相关代码,且每份代码都需要不同的修改(因为3个用户的需求不同),工作量很大;
如果使用了依赖隔离模式进行了解耦,那么就只需要对3D应用做下面的修改:
1.修改AImplement和ALibrary(升级)
2.增加BImplement
3.增加BLibrary
4.增加ABImplement
对交付给用户的代码做下面的修改:
1.更新第一个用户交付代码的AImplement和ALibrary
2.为第二个用户交付代码增加BImplement、BLibrary;修改Client代码,注入BImplement
3.为第三个用户交付代码增加ABImplement、BLibrary;修改Client代码,注入ABImplement
相比之下工作量减少了很多
可以了解下依赖注入 控制反转 依赖倒置
洋葱架构的资料在这里:资料
六边形架构类似于洋葱架构,相关资料在这里
《设计模式之禅》
欢迎来到Wonder~
扫码加入我的QQ群:

扫码加入免费知识星球-YYC的Web3D旅程:
