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

系列文章详见:

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. 使用js实现复选框的全选、取消功能

    id为all的想设置全选的那个框的id,name为checkname[]的是每个小复选框: 第一种: <script> function checkAll() { var all=docu ...

  2. 基于STM32单片机的简单红外循迹的实现

    初步接触STM32,采用两路红外传感器实现小车循迹,稍显简略,如有不好的地方,欢迎大家指点改正

  3. IDEA小技巧:Markdown里的命令行可以直接运行了

    作为一名开发者,相信大部分人都喜欢用Markdown来写文章和写文档. 如果你经常用开源项目或者自己维护开源项目,肯定对于项目下的README文件也相当熟悉了吧,通常我们会在这里介绍项目的功能.如何使 ...

  4. nodejs使用jquery风格环境安装

    BEGIN; 1.npm install jQuery 注意:是jQuery,不是jquery! 2.npm install jsdom 注意:直接执行会安装错误,必须先指定安装版本! 解决:修改pa ...

  5. Java 8的18个常用日期处理

    Java 8的18个常用日期处理 一.简介 伴随 lambda表达式.streams 以及一系列小优化,Java 8 推出了全新的日期时间API. Java处理日期.日历和时间的不足之处:将 java ...

  6. Antd Modal 可拖拽移动

    一 目标: 实现antd Modal 弹窗或者其他弹窗的点击标题进行拖拽的效果 二 准备及思录: 1.使用antd  Modal 组件,要想改变位置需要改变Modal style 的left 和top ...

  7. Bugku CTF练习题---杂项---隐写3

    Bugku CTF练习题---杂项---隐写3 flag:flag{He1l0_d4_ba1} 解题步骤: 1.观察题目,下载附件 2.打开图片,发现是一张大白,仔细观察一下总感觉少了点东西,这张图好 ...

  8. Gitlab-runner+Docker自动部署SpringBoot项目

    本文基于Gitlab CI/CD及Docker快速实现项目的自动部署. 注意:本文较长,浏览需要12分钟左右. 1.环境要求 以下服务器的操作系统均为Centos7 服务器A:Gitlab 服务器B: ...

  9. sql索引优化思路

    [开发]SQL优化思路(以oracle为例) powered by wanglifeng https://www.cnblogs.com/wanglifeng717 单表查询的优化思路 单表查询是最简 ...

  10. 劳动节快乐!手写个核心价值观编码工具 - Python实现

    前言 今天是五一劳动节,祝各位无产阶级劳动者节日快乐! 然后来整活分享一些有趣的东西~ 这个小工具是我大学时做着玩的,对于各位接班人来说,12个词的核心价值观这东西,大家都非常熟悉了,这工具可以实现将 ...