33
44## 需求
55
6- 编辑器使用Three.js引擎来创建一个场景
6+ 编辑器使用Three.js引擎作为渲染引擎,来创建一个场景
77
88
99## 实现思路
@@ -47,7 +47,7 @@ export let createScene = function () {
4747}
4848```
4949
50- Editor在createScene函数中调用Three.js,创建场景
50+ createScene函数调用Three.js库创建场景
5151
5252
5353## 提出问题
@@ -64,7 +64,6 @@ Editor在createScene函数中调用Three.js,创建场景
6464## 概述解决方案?
6565
6666编辑器改为引入Babylon.js库,并修改编辑器中与引擎相关的代码
67- <!-- 直接将three.js引擎换成Babylon.js引擎,修改跟引擎相关的代码 -->
6867
6968## 给出UML?
7069
@@ -77,8 +76,8 @@ Babylon.js是Babylon.js库
7776
7877## 结合UML图,描述如何具体地解决问题?
7978
80- 将Three.js换成Babylon.js
81- 修改Editor的相关代码
79+ - 将Three.js换成Babylon.js
80+ - 修改Editor的相关代码
8281
8382
8483## 给出代码?
@@ -104,9 +103,9 @@ Editor改为引入Babylon.js库,并修改createScene函数中与引擎相关
104103
105104## 提出问题
106105
107- 替换引擎需要修改编辑器中所有相关代码 ,这样的成本太高了
106+ 现在需要修改编辑器中所有与引擎相关代码 ,这样的成本太高了
108107
109- 有没有办法能在不修改编辑器相关代码的情况下实现替换引擎呢 ?
108+ 有没有办法能在不修改编辑器代码的情况下实现替换引擎呢 ?
110109
111110
112111# [ 给出使用模式的改进方案]
@@ -122,7 +121,7 @@ Editor改为引入Babylon.js库,并修改createScene函数中与引擎相关
122121TODO tu
123122
124123
125- RenderEngine接口是对引擎API的抽象
124+ RenderEngine接口是对渲染引擎的抽象
126125
127126BabylonImplement是使用Babylon.js引擎对RenderEngine接口的实现
128127
@@ -134,7 +133,7 @@ Three.js是Three.js库
134133
135134Client通过依赖注入的方式注入RenderEngine实现,使Editor能够调用它来创建场景
136135
137- DependencyContainer是RenderEngine实现的容器,负责维护由Client注入的RenderEngine实现
136+ DependencyContainer是注入的RenderEngine实现的容器,提供它的get/set函数
138137
139138
140139## 结合UML图,描述如何具体地解决问题?
@@ -216,7 +215,7 @@ export let setRenderEngine = (renderEngine: RenderEngine) {
216215```
217216
218217
219- DependencyContainer提供了get/set函数来获得和设置当前的RenderEngine实现
218+ DependencyContainer提供了get/set函数来获得和设置注入的RenderEngine实现
220219
221220
222221Editor
@@ -237,7 +236,7 @@ export let createScene = function () {
237236Editor增加了injectDependencies函数,实现了注入由Client传过来的RenderEngine实现
238237
239238createScene函数通过DependencyContainer获得注入的RenderEngine实现,调用它来创建场景
240- 这里它只知道RenderEngine接口类型 ,没有依赖具体的RenderEngine实现
239+ 它只知道RenderEngine接口 ,没有依赖具体的RenderEngine实现
241240
242241
243242Client
@@ -269,13 +268,10 @@ Client注入了BabylonImplement
269268
270269将外部依赖隔离后,系统变得更“纯”了,类似于函数式编程中的[ “纯函数”] ( https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch3.html#%E8%BF%BD%E6%B1%82%E2%80%9C%E7%BA%AF%E2%80%9D%E7%9A%84%E7%90%86%E7%94%B1 ) 的概念,消除了外部依赖带来了副作用
271270
272- 那么哪些依赖属于外部依赖呢 ?对于编辑器而言,引擎、UI组件库(如Ant Design)、后端服务、文件操作、日志等都属于外部依赖;
271+ 哪些依赖属于外部依赖呢 ?对于编辑器而言,引擎、UI组件库(如Ant Design)、后端服务、文件操作、日志等都属于外部依赖;
273272对于引擎而言,各种子引擎(如物理引擎、动画引擎、例子引擎)、后端服务、文件操作等都属于外部依赖
274273
275- <!-- 对于网站而言,UI组件库(如Ant Design)、后端服务、数据库操作等都属于外部依赖。 -->
276-
277- <!-- 可以将每个外部依赖都抽象为对应的IDendency接口,从而都隔离出去。 -->
278- 可以将每个外部依赖都抽象为接口,从而都隔离出去。
274+ 可以将每个可能会变化的外部依赖都抽象为接口,从而都隔离出去。
279275
280276
281277
@@ -290,24 +286,26 @@ TODO tu
290286
291287我们来看看模式的相关角色:
292288
293- - Dendepency
294- 该角色是依赖接口,对依赖的具体库的API进行了抽象
289+ - Dependency
290+ 该角色是依赖的接口
295291- DependencyImplement
296- 该角色是对Dendepency的实现
292+ 该角色是对Dependency的实现
297293- DependencyLibrary
298294该角色是一个库
295+ - Client
296+ 该角色是用户,通过依赖注入的方式注入DependencyImplement
299297- DependencyContainer
300- 该角色是一个容器,负责维护由Client注入的DependencyImplement
298+ 该角色是注入的DependencyImplement的容器,提供它的get/set函数
301299- System
302- 该角色使用了一个或多个外部依赖,它只知道外部依赖的接口(Dendepency)而不知道外部依赖的具体实现(DendepencyImplement )
300+ 该角色使用了一个或多个外部依赖,它只知道外部依赖的接口(Dependency)而不知道具体实现(DependencyImplement )
303301
304302
305303## 角色之间的关系?
306304
307- - 可以有多个Dendepency
305+ - 可以有多个Dependency
308306如除了RenderEngine以外,还可以File、Server等
309- - 一个Dendepency可以有多个DependencyImplement
310- 如对于RenderEngine,除了有ThreeImplement ,还可以有BabylonImplement等
307+ - 一个Dependency可以有多个DependencyImplement
308+ 如RenderEngine除了有ThreeImplement ,还可以有BabylonImplement等
311309- 一个DependencyImplement一般只使用一个DependencyLibrary,但也可以使用多个DependencyLibrary
312310如对于RenderEngine,可以增加ThreeAndBabylonImplement,它同时使用Three.js和Babylon.js这两个DependencyLibrary。这样就使得编辑器可以同时使用两个引擎来渲染
313311
@@ -316,7 +314,7 @@ TODO tu
316314
317315下面我们来看看各个角色的抽象代码:
318316
319- - Dendepency的抽象代码
317+ - Dependency的抽象代码
320318``` ts
321319type abstractType1 = any ;
322320...
@@ -379,6 +377,8 @@ export let doSomethingUseDependency1 = function () {
379377
380378 ...
381379}
380+
381+ 更多doSomethingUseDependencyX 函数...
382382```
383383- Client的抽象代码
384384``` ts
@@ -394,10 +394,10 @@ doSomethingUseDependency1()
394394
395395依赖隔离模式主要遵循下面的设计原则:
396396- 依赖倒置原则
397- 系统依赖于外部依赖的抽象(Dendepency )而不是外部依赖的细节(DependencyImplement和DependencyLibrary),从而外部依赖的细节的变化不会影响系统
397+ 系统依赖于外部依赖的抽象(Dependency )而不是外部依赖的细节(DependencyImplement和DependencyLibrary),从而外部依赖的细节的变化不会影响系统
398398- 开闭原则
399- 可以增加更多的Dendepency ,从而隔离更多的外部依赖;或者对一个Dendepency增加更多的DependencyImplement ,从而能够替换外部依赖的实现。这些都不会影响System,从而实现了对扩展开放
400- 如果需要升级外部依赖的版本 ,这也只会影响DependencyImplement和DependencyLibrary,不会影响System,从而实现了对修改关闭
399+ 可以增加更多的Dependency ,从而隔离更多的外部依赖;或者对一个Dependency增加更多的DependencyImplement ,从而能够替换外部依赖的实现。这些都不会影响System,从而实现了对扩展开放
400+ 如果需要修改已有的外部依赖(如升级版本) ,这也只会影响DependencyImplement和DependencyLibrary,不会影响System,从而实现了对修改关闭
401401
402402依赖隔离模式也应用了“依赖注入”、“控制反转”的思想
403403
@@ -410,7 +410,7 @@ doSomethingUseDependency1()
410410- 提高系统的稳定性
411411外部依赖的变化不会影响系统
412412- 提高系统的扩展性
413- 可以任意修改外部依赖的实现而不影响系统
413+ 可以任意替换外部依赖而不影响系统
414414- 提高系统的可维护性
415415系统与外部依赖解耦,便于维护
416416
@@ -443,13 +443,13 @@ doSomethingUseDependency1()
443443
444444## 注意事项
445445
446- - Dendepency要足够抽象,才不至于在修改或增加DependencyImplement时需要修改Dendepency ,从而影响System
447- 当然,在开发阶段难免考虑不足,如当一开始只有一个DependencyImplement时,Dendepency往往只会考虑这个DependencyImplement,导致在增加其它DependencyImplement时就需要修改Dendepency ,使其更加抽象,这样才能容纳更多的DependencyImplement带来的变化
448- 因此,我们可以允许在开发阶段修改Dendepency,但是在发布前则确保Dendepency已经足够抽象和稳定,不需要再改动
446+ - Dependency要足够抽象,才不至于在修改或增加DependencyImplement时需要修改Dependency ,从而影响System
447+ 当然,在开发阶段难免考虑不足,如当一开始只有一个DependencyImplement时,Dependency往往只会考虑这个DependencyImplement,导致在增加其它DependencyImplement时就需要修改Dependency ,使其更加抽象,这样才能容纳更多的DependencyImplement带来的变化
448+ 因此,我们可以允许在开发阶段修改Dependency,但是在发布前则确保Dependency已经足够抽象和稳定
449449
450450- 有多少个Dependency接口,DependencyContainer就有多少个get/set函数
451451
452- - 最好一开始就使用依赖隔离模式将所有的可能会变化的外部依赖都隔离,这样可以避免到后期如果要修改外部依赖时需要修改系统所有相关代码的情况
452+ - 最好一开始就使用依赖隔离模式将所有的可能会变化的外部依赖都隔离,这样可以避免到后期修改外部依赖时需要修改系统所有相关代码的情况
453453
454454
455455# 扩展
@@ -459,17 +459,20 @@ doSomethingUseDependency1()
459459
460460如果基于依赖隔离模式这样设计一个架构:
461461
462- - 定义4个层,其中的应用服务层、领域服务层、领域模型层为上下层的关系,上层依赖下层;外部依赖层则属于独立的层,层中的外部依赖是按照依赖隔离模式设计,在运行时注入
463- - 将系统的所有外部依赖都隔离出去,也就是为每个外部依赖创建一个IDendepency,其中DependencyImplement位于外部依赖层,IDendepency位于领域模型层中的Application Core
464- 其它三层不依赖外部依赖层,而是依赖领域模型层中的Application Core(具体就是依赖IDendepency)
462+ - 定义4个层:外部依赖层、应用服务层、领域服务层、领域模型层,其中前者是后者的上层,上层依赖下层
463+ - 外部依赖层则属于独立的层,该层中的外部依赖是按照依赖隔离模式设计的,在运行时由用户注入
464+ - 将系统的所有外部依赖都隔离出去,也就是为每个外部依赖创建一个Dependency,其中DependencyImplement位于外部依赖层,Dependency位于领域模型层中的Application Core
465+ <!-- 其它三层不依赖外部依赖层,而是依赖领域模型层中的Application Core(具体就是依赖Dependency) -->
465466- 运用[ 领域驱动设计DDD] ( https://www.cnblogs.com/chaogex/p/12408802.html ) 设计系统,将系统的核心逻辑建模为领域模型,放到领域模型层
466467
467468那么这样的架构就是洋葱架构
468469洋葱架构如下图所示:
469470![ image] ( https://img2022.cnblogs.com/blog/419321/202206/419321-20220609041114118-2037325753.webp )
470471
471- 它的核心思想就是将变化最频繁的外部依赖层隔离出去,并使变化最少的领域模型层独立而不依赖其它层。
472- 在传统的架构中,领域模型层会依赖外部依赖层(如在领域模型中调用后端服务等),但是现在却解耦了。这样的好处就是如果外部依赖层变化,不会影响其他层
472+ 洋葱架构与传统的三层架构的区别是颠倒了层之间的依赖关系:洋葱架构将三层架构中的最下层(外部依赖层)改为最上层;将三层架构中的倒数第二层(领域模型层)下降为最下层
473+ 洋葱架构的核心思想就是将变化最频繁的外部依赖隔离出去,并使变化最少的领域模型层独立而不依赖其它层。
474+ <!-- 在传统的架构中,领域模型层会依赖外部依赖层(如在领域模型中调用后端服务等),但是现在却解耦了 -->
475+ 这样的好处是外部依赖层容易变化,但它的变化现在不会影响其他层
473476
474477
475478<!-- # 结合其它模式
@@ -505,7 +508,7 @@ doSomethingUseDependency1()
505508- 扩大使用场景
506509
507510编辑器的外部依赖不只是引擎,也包括UI组件库等
508- 如需要将旧的UI组件库(如React UI 组件库-Antd)替换为新的组件库,则可按照依赖隔离模式,提出UI这个Dependency接口,并加入作为接口的实现的OldUIImplement 、NewUIImplement,在这两个接口的实现中调用对应的UI组件库
511+ 如需要将旧的UI组件库(如React UI 组件库-Antd)替换为新的组件库,则可按照依赖隔离模式,提出UI这个Dependency接口,并加入作为接口实现的OldUIImplement 、NewUIImplement,在这两个接口的实现中调用对应的UI组件库
509512
510513
511514除了编辑器外,引擎、网站等系统也可以使用依赖隔离模式
@@ -514,7 +517,7 @@ doSomethingUseDependency1()
514517
515518
516519
517- 有些外部依赖在运行时会变化,对于这种情况,在运行时注入对应的DependencyImplement即可
520+ 有些外部依赖在运行时会变化,对于这种情况,使用依赖隔离模式后可以在运行时注入变化后的DependencyImplement
518521如编辑器向用户提供了“切换渲染效果”的功能:用户点击一个按钮后,就可以切换渲染引擎来渲染场景
519522为了实现该功能,只需在按钮的点击事件中注入对应的DependencyImplement到DependencyContainer中即可
520523
@@ -523,17 +526,20 @@ doSomethingUseDependency1()
523526- 满足各种修改外部依赖的用户需求
524527
525528我遇到过这种问题:3D应用开发完成后,交给3个外部用户使用。用了一段时间后,这3个用户提出了不同的修改外部依赖的要求:第一个用户想要升级3D应用依赖的渲染引擎A,第二个用户想要替换A为B,第三个用户想要同时使用B和升级后的A来渲染。
526- 如果3D应用是直接调用外部依赖库的话,我们就需要去修改交付的3份代码中系统的相关代码,且每份代码都需要不同的修改(因为3个用户的需求不同),工作量很大;
529+ 如果3D应用是直接调用外部依赖库的话,我们就需要将交付的代码修改为3个版本,分别满足3个用户的需求
530+ 每个版本都需要修改系统中与外部依赖相关的所有代码,这样导致工作量很大
531+
527532如果使用了依赖隔离模式进行了解耦,那么就只需要对3D应用做下面的修改:
5285331.修改AImplement和ALibrary(升级)
5295342.增加BImplement
5305353.增加BLibrary
531- 4.增加ABImplement
532- 对交付给用户的代码做下面的修改:
533- 1.更新第一个用户交付代码的AImplement和ALibrary
534- 2.为第二个用户交付代码增加BImplement、BLibrary;修改Client代码,注入BImplement
535- 3.为第三个用户交付代码增加ABImplement、BLibrary;修改Client代码,注入ABImplement
536- 相比之下工作量减少了很多
536+
537+ 交付的代码只有1个版本,只是在Client中分别对这3个用户注入不同的DependencyImplement:
538+ 1.Client为第一个用户注入AImplement
539+ 2.Client为第二个用户注入BImplement
540+ 2.Client为第三个用户注入ABImplement
541+
542+ 这样就能减少很多工作量
537543
538544
539545
@@ -543,7 +549,5 @@ doSomethingUseDependency1()
543549
544550关于洋葱架构,可以在网上搜索“洋葱架构”、“the-onion-architecture-part”
545551
546- 关于洋葱架构,可以在网上搜索“洋葱架构”、“the-onion-architecture-part”
547-
548552六边形架构类似于洋葱架构,可以在网上搜索“六边形架构”
549553
0 commit comments