大家好~本文提出了“依赖隔离”模式

系列文章详见:

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 = null

export 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接口,从而都隔离出去。

依赖隔离模式的通用类图

我们来看看依赖隔离模式的相关角色:

  • IDendepency(依赖接口)

    该角色对依赖的具体库的API进行了抽象
  • DependencyImplement(依赖实现)

    该角色是对IDendepency的实现
  • DependencyLibrary(依赖的具体库)

    该角色通常为第三方库,如Three.js引擎等
  • DependencyContainer(依赖容器)

    该角色封装了注入的依赖实现,为每个注入的依赖实现提供get/set函数
  • System(系统)

    该角色使用了一个或多个外部依赖,它只知道外部依赖的接口而不知道外部依赖的具体实现

各个角色之间的关系为:

  • 可以有多个依赖接口,如除了IRenderEngine以外,还可以IFile、IServer等依赖接口
  • 一个IDendepency可以有多个DependencyImplement,如对于IRenderEngine,除了有ThreeImplement,还可以有Babylon引擎的依赖实现BabylonImplement
  • 一个DependencyImplement一般只组合一个DependencyLibrary,但也可以组合多个DependencyLibrary,比如对于IRenderEngine,可以增加ThreeAndBabylonImplement,它同时组合Three.js和Babylon.js这两个DependencyLibrary,这样就使得编辑器可以同时使用两个引擎来渲染,达到最大化的渲染能力

下面我们来看看各个角色的抽象代码:

  • IDendepency抽象代码
type abstractType1 = any;
... export interface IDependency1 {
abstractAPI1(): abstractType1,
...
}
  • DependencyImplement抽象代码
import { IDependency1 } from "./IDependency1";
import {
api1,
...
} from "dependencylibrary1"; export let implement = (): IDependency1 => {
return {
abstractAPI1: () => {
...
return api1()
},
...
}
}
  • DependencyLibrary抽象代码
export let api1 = function () {
...
} ...
  • DependencyContainer抽象代码
import { IDependency1 } from "./IDependency1"

let _dependency1: IDependency1 = null
... export let getDependency1 = (): IDependency1 => {
return _dependency1;
} export let setDependency1 = (dependency1: IDependency1) {
_dependency1 = dependency1;
} ...
  • System抽象代码
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() ...
}
  • Client抽象代码
import { doSomethingNeedDependency1, injectDependencies } from "./System";
import { implement } from "./Dependency1Implement1"; injectDependencies(implement(), 其它依赖实现...) doSomethingNeedDependency1()

依赖隔离模式主要遵循下面的设计原则:

  • 依赖倒置原则

    系统依赖于外部依赖的抽象(IDendepency)而不是外部依赖的细节(DependencyImplement和DependencyLibrary),从而外部依赖的细节的变化不会影响系统
  • 开闭原则

    可以增加更多的IDendepency,从而隔离更多的外部依赖;或者对一个IDendepency增加更多的DependencyImplement,从而能够替换外部依赖的实现。这些都不会影响System,从而实现了对扩展开放

    可以升级外部依赖,这也只会影响DependencyImplement和DependencyLibrary,不会影响System,从而实现了对修改关闭

依赖隔离模式也应用了“依赖注入”、“控制反转”的思想

应用

优点

  • 提高系统的稳定性

    外部依赖的变化不会影响系统
  • 提高系统的扩展性

    可以任意修改外部依赖的实现而不影响系统
  • 提高系统的可维护性

    系统与外部依赖解耦,便于维护

缺点

使用场景

  • 需要替换外部依赖的实现,如替换编辑器使用的渲染引擎
  • 外部依赖经常变化,如编辑器使用的渲染引擎的版本频繁升级
  • 运行时外部依赖会变化

    对于这种情况,在运行时注入对应的DependencyImplement即可。如编辑器向用户提供了“切换渲染效果”的功能,使用户点击一个按钮后,就可以切换渲染引擎。为了实现该功能,只需在按钮的点击事件中注入对应的DependencyImplement到DependencyContainer中即可

注意事项

  • IDendepency要足够抽象,这样才能不至于在修改或增加DependencyImplement时修改IDendepency,导致影响System

    当然,在开发阶段难免考虑不足,如当一开始只有一个DependencyImplement时,IDendepency往往只会考虑这个DependencyImplement,导致在增加其它DependencyImplement时就需要修改IDendepency,使其变得更加抽象。

    我们可以在开发阶段修改IDendepency,而在上线后则确保IDendepency已经足够抽象和稳定,不需要再改动

扩展

如果基于依赖隔离模式这样设计一个架构:

  • 定义4个层,其中的应用服务层、领域服务层、领域模型层为上下层的关系,上层依赖下层;外部依赖层则属于独立的层,层中的外部依赖是按照依赖隔离模式设计,在运行时注入
  • 将系统的所有外部依赖都隔离出去,也就是为每个外部依赖创建一个IDendepency,其中DependencyImplement位于外部依赖层,IDendepency位于领域模型层中的Application Core

    其它三层不依赖外部依赖层,而是依赖领域模型层中的Application Core(具体就是依赖IDendepency)
  • 运用领域驱动设计DDD设计系统,将系统的核心逻辑建模为领域模型,放到领域模型层

那么这样的架构就是洋葱架构

洋葱架构如下图所示:

它的核心思想就是将变化最频繁的外部依赖层隔离出去,并使变化最少的领域模型层独立而不依赖其它层。

在传统的架构中,领域模型层会依赖外部依赖层(如在领域模型中调用后端服务等),但是现在却解耦了。这样的好处就是如果外部依赖层变化,不会影响其他层

最佳实践

如果只是开发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

相比之下工作量减少了很多

更多资料推荐

可以了解下依赖注入 控制反转 依赖倒置

洋葱架构的资料在这里:资料

六边形架构类似于洋葱架构,相关资料在这里

参考资料

《设计模式之禅》

3D编程模式:依赖隔离模式的更多相关文章

  1. 初涉JavaScript模式 (11) : 模块模式

    引子 这篇算是对第9篇中内容的发散和补充,当时我只是把模块模式中的一些内容简单的归为函数篇中去,在北川的提醒下,我才发觉这是非常不严谨的,于是我把这些内容拎出来,这就是这篇的由来. 什么是模块模式 在 ...

  2. 对象创建模式之模块模式(Module Pattern)

    模块模式可以提供软件架构,为不断增长的代码提供组织形式.JavaScript没有提供package的语言表示,但我们可以通过模块模式来分解并组织代码块,这些黑盒的代码块内的功能可以根据不断变化的软件需 ...

  3. JavaScript基础对象创建模式之模块模式(Module Pattern)(025)

    模块模式可以提供软件架构,为不断增长的代码提供组织形式.JavaScript没有提供package的语言表示,但我们可以通过模块模式来分解并组织 代码块,这些黑盒的代码块内的功能可以根据不断变化的软件 ...

  4. 理论+案例,带你掌握Angular依赖注入模式的应用

    摘要:介绍了Angular中依赖注入是如何查找依赖,如何配置提供商,如何用限定和过滤作用的装饰器拿到想要的实例,进一步通过N个案例分析如何结合依赖注入的知识点来解决开发编程中会遇到的问题. 本文分享自 ...

  5. Python编程中的反模式

    Python是时下最热门的编程语言之一了.简洁而富有表达力的语法,两三行代码往往就能解决十来行C代码才能解决的问题:丰富的标准库和第三方库,大大节约了开发时间,使它成为那些对性能没有严苛要求的开发任务 ...

  6. .NET CORE学习笔记系列(2)——依赖注入【3】依赖注入模式

    原文:https://www.cnblogs.com/artech/p/net-core-di-03.html IoC主要体现了这样一种设计思想:通过将一组通用流程的控制权从应用转移到框架中以实现对流 ...

  7. 设计模式---接口隔离模式之门面模式(Façade)

    前提:接口隔离模式 在组建构建过程中,某些接口之间直接的依赖常常会带来很多问题.甚至根本无法实现.采用添加一层间接接口(稳定的),来隔离本来相互紧密关联的接口是一种常见的解决方案. 典型模式: 门面模 ...

  8. C++设计模式 之 “接口隔离” 模式:Facade、Proxy、Mediator、Adapter

    “接口隔离”模式 在组建构建过程中,某些接口之间之间的依赖常常会带来很多问题.甚至根本无法实现.采用添加一层间接(稳定)接口,来隔离本来相互紧密关联的接口是一种常见的解决方案. 典型模式 #Facad ...

  9. Python Twisted系列教程2:异步编程初探与reactor模式

    作者:dave@http://krondo.com/slow-poetry-and-the-apocalypse/  译者:杨晓伟(采用意译) 这个系列是从这里开始的,欢迎你再次来到这里来.现在我们可 ...

随机推荐

  1. Linux上部署net6应用

    前言 ​ .net6都出来了,作为一名.net搬砖工却一直都在windows下部署应用,还未尝试过linux环境下部署应用.参考福禄网络研发团队的相关博客,学习一下如何将应用部署到linux系统. . ...

  2. 微信小程序,制作属于自己的Icon图标

    前言 最近在接手一个微信小程序,发现里面的图标都是使用的image组件,看起来非常别扭,加载也不太顺畅. 就想着看看微信有没有类似自带的图标库可以使用. 有是有,就是太少了,翻来翻去好像也就 8 种, ...

  3. 电机三环pid控制及调试经验

    一.伺服电机的双环pid 双环pid在正常底盘运动的控制中已经足够了,但是对于双轴云台的控制来说,双环pid的云台控制的响应速度是远远不够的,所以加入了电流环的控制. 两篇大佬的文章--这是我学习pi ...

  4. Linux curl遇到错误curl: (3) Illegal characters found in URL

    服务器上执行一个脚本,在linux新建的sh,把本地编辑器的内容粘贴到文件里. 结果执行的时候报错了. 问题就是 curl:(3)Illegal characters found in URL 看着一 ...

  5. 设置网站标题时找不到index.html问题解决

    都知道,修改网站标题在根目录index.html里修改.但是在vue3更新后,index.html就没有放这里了,放到了public中.去public中一眼就能看到.我也是去那里就找到了.

  6. npm 报错This is probably not a problem with npm. There is likely additional logging output above.

    报错This is probably not a problem with npm. There is likely additional logging output above. 安装了一个插件后 ...

  7. 【论文笔记】A Survey on Federated Learning: The Journey From Centralized to Distributed On-Site Learning and Beyond(综述)

    A Survey on Federated Learning: The Journey From Centralized to Distributed On-Site Learning and Bey ...

  8. MySQL left join 引发的惨案

    当我用这个进行更改值时,type未控制order表 其他数据被更改 还好备份数据表了(这里就体现了备份的重要性) UPDATE expense_order as a left join ( SELEC ...

  9. [AcWing 756] 蛇形矩阵

    点击查看代码 #include<iostream> using namespace std; const int N = 110; int n, m; int dx[] = {-1, 0, ...

  10. insert语句生成的存储过程

    问题: 1.如何配置数据库数据: 方式一:图形界面点击输入数据,导出成sql. 缺点:表多,数据多的时候非常繁琐,字段含义需要另外开窗口对照. 方式二:徒手写或者修改已有语句:insert table ...