3
3
4
4
## 需求
5
5
6
- 编辑器使用Three.js引擎来创建一个场景
6
+ 编辑器使用Three.js引擎作为渲染引擎,来创建一个场景
7
7
8
8
9
9
## 实现思路
@@ -47,7 +47,7 @@ export let createScene = function () {
47
47
}
48
48
```
49
49
50
- Editor在createScene函数中调用Three.js,创建场景
50
+ createScene函数调用Three.js库创建场景
51
51
52
52
53
53
## 提出问题
@@ -64,7 +64,6 @@ Editor在createScene函数中调用Three.js,创建场景
64
64
## 概述解决方案?
65
65
66
66
编辑器改为引入Babylon.js库,并修改编辑器中与引擎相关的代码
67
- <!-- 直接将three.js引擎换成Babylon.js引擎,修改跟引擎相关的代码 -->
68
67
69
68
## 给出UML?
70
69
@@ -77,8 +76,8 @@ Babylon.js是Babylon.js库
77
76
78
77
## 结合UML图,描述如何具体地解决问题?
79
78
80
- 将Three.js换成Babylon.js
81
- 修改Editor的相关代码
79
+ - 将Three.js换成Babylon.js
80
+ - 修改Editor的相关代码
82
81
83
82
84
83
## 给出代码?
@@ -104,9 +103,9 @@ Editor改为引入Babylon.js库,并修改createScene函数中与引擎相关
104
103
105
104
## 提出问题
106
105
107
- 替换引擎需要修改编辑器中所有相关代码 ,这样的成本太高了
106
+ 现在需要修改编辑器中所有与引擎相关代码 ,这样的成本太高了
108
107
109
- 有没有办法能在不修改编辑器相关代码的情况下实现替换引擎呢 ?
108
+ 有没有办法能在不修改编辑器代码的情况下实现替换引擎呢 ?
110
109
111
110
112
111
# [ 给出使用模式的改进方案]
@@ -122,7 +121,7 @@ Editor改为引入Babylon.js库,并修改createScene函数中与引擎相关
122
121
TODO tu
123
122
124
123
125
- RenderEngine接口是对引擎API的抽象
124
+ RenderEngine接口是对渲染引擎的抽象
126
125
127
126
BabylonImplement是使用Babylon.js引擎对RenderEngine接口的实现
128
127
@@ -134,7 +133,7 @@ Three.js是Three.js库
134
133
135
134
Client通过依赖注入的方式注入RenderEngine实现,使Editor能够调用它来创建场景
136
135
137
- DependencyContainer是RenderEngine实现的容器,负责维护由Client注入的RenderEngine实现
136
+ DependencyContainer是注入的RenderEngine实现的容器,提供它的get/set函数
138
137
139
138
140
139
## 结合UML图,描述如何具体地解决问题?
@@ -216,7 +215,7 @@ export let setRenderEngine = (renderEngine: RenderEngine) {
216
215
```
217
216
218
217
219
- DependencyContainer提供了get/set函数来获得和设置当前的RenderEngine实现
218
+ DependencyContainer提供了get/set函数来获得和设置注入的RenderEngine实现
220
219
221
220
222
221
Editor
@@ -237,7 +236,7 @@ export let createScene = function () {
237
236
Editor增加了injectDependencies函数,实现了注入由Client传过来的RenderEngine实现
238
237
239
238
createScene函数通过DependencyContainer获得注入的RenderEngine实现,调用它来创建场景
240
- 这里它只知道RenderEngine接口类型 ,没有依赖具体的RenderEngine实现
239
+ 它只知道RenderEngine接口 ,没有依赖具体的RenderEngine实现
241
240
242
241
243
242
Client
@@ -269,13 +268,10 @@ Client注入了BabylonImplement
269
268
270
269
将外部依赖隔离后,系统变得更“纯”了,类似于函数式编程中的[ “纯函数”] ( 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 ) 的概念,消除了外部依赖带来了副作用
271
270
272
- 那么哪些依赖属于外部依赖呢 ?对于编辑器而言,引擎、UI组件库(如Ant Design)、后端服务、文件操作、日志等都属于外部依赖;
271
+ 哪些依赖属于外部依赖呢 ?对于编辑器而言,引擎、UI组件库(如Ant Design)、后端服务、文件操作、日志等都属于外部依赖;
273
272
对于引擎而言,各种子引擎(如物理引擎、动画引擎、例子引擎)、后端服务、文件操作等都属于外部依赖
274
273
275
- <!-- 对于网站而言,UI组件库(如Ant Design)、后端服务、数据库操作等都属于外部依赖。 -->
276
-
277
- <!-- 可以将每个外部依赖都抽象为对应的IDendency接口,从而都隔离出去。 -->
278
- 可以将每个外部依赖都抽象为接口,从而都隔离出去。
274
+ 可以将每个可能会变化的外部依赖都抽象为接口,从而都隔离出去。
279
275
280
276
281
277
@@ -290,24 +286,26 @@ TODO tu
290
286
291
287
我们来看看模式的相关角色:
292
288
293
- - Dendepency
294
- 该角色是依赖接口,对依赖的具体库的API进行了抽象
289
+ - Dependency
290
+ 该角色是依赖的接口
295
291
- DependencyImplement
296
- 该角色是对Dendepency的实现
292
+ 该角色是对Dependency的实现
297
293
- DependencyLibrary
298
294
该角色是一个库
295
+ - Client
296
+ 该角色是用户,通过依赖注入的方式注入DependencyImplement
299
297
- DependencyContainer
300
- 该角色是一个容器,负责维护由Client注入的DependencyImplement
298
+ 该角色是注入的DependencyImplement的容器,提供它的get/set函数
301
299
- System
302
- 该角色使用了一个或多个外部依赖,它只知道外部依赖的接口(Dendepency)而不知道外部依赖的具体实现(DendepencyImplement )
300
+ 该角色使用了一个或多个外部依赖,它只知道外部依赖的接口(Dependency)而不知道具体实现(DependencyImplement )
303
301
304
302
305
303
## 角色之间的关系?
306
304
307
- - 可以有多个Dendepency
305
+ - 可以有多个Dependency
308
306
如除了RenderEngine以外,还可以File、Server等
309
- - 一个Dendepency可以有多个DependencyImplement
310
- 如对于RenderEngine,除了有ThreeImplement ,还可以有BabylonImplement等
307
+ - 一个Dependency可以有多个DependencyImplement
308
+ 如RenderEngine除了有ThreeImplement ,还可以有BabylonImplement等
311
309
- 一个DependencyImplement一般只使用一个DependencyLibrary,但也可以使用多个DependencyLibrary
312
310
如对于RenderEngine,可以增加ThreeAndBabylonImplement,它同时使用Three.js和Babylon.js这两个DependencyLibrary。这样就使得编辑器可以同时使用两个引擎来渲染
313
311
@@ -316,7 +314,7 @@ TODO tu
316
314
317
315
下面我们来看看各个角色的抽象代码:
318
316
319
- - Dendepency的抽象代码
317
+ - Dependency的抽象代码
320
318
``` ts
321
319
type abstractType1 = any ;
322
320
...
@@ -379,6 +377,8 @@ export let doSomethingUseDependency1 = function () {
379
377
380
378
...
381
379
}
380
+
381
+ 更多doSomethingUseDependencyX 函数...
382
382
```
383
383
- Client的抽象代码
384
384
``` ts
@@ -394,10 +394,10 @@ doSomethingUseDependency1()
394
394
395
395
依赖隔离模式主要遵循下面的设计原则:
396
396
- 依赖倒置原则
397
- 系统依赖于外部依赖的抽象(Dendepency )而不是外部依赖的细节(DependencyImplement和DependencyLibrary),从而外部依赖的细节的变化不会影响系统
397
+ 系统依赖于外部依赖的抽象(Dependency )而不是外部依赖的细节(DependencyImplement和DependencyLibrary),从而外部依赖的细节的变化不会影响系统
398
398
- 开闭原则
399
- 可以增加更多的Dendepency ,从而隔离更多的外部依赖;或者对一个Dendepency增加更多的DependencyImplement ,从而能够替换外部依赖的实现。这些都不会影响System,从而实现了对扩展开放
400
- 如果需要升级外部依赖的版本 ,这也只会影响DependencyImplement和DependencyLibrary,不会影响System,从而实现了对修改关闭
399
+ 可以增加更多的Dependency ,从而隔离更多的外部依赖;或者对一个Dependency增加更多的DependencyImplement ,从而能够替换外部依赖的实现。这些都不会影响System,从而实现了对扩展开放
400
+ 如果需要修改已有的外部依赖(如升级版本) ,这也只会影响DependencyImplement和DependencyLibrary,不会影响System,从而实现了对修改关闭
401
401
402
402
依赖隔离模式也应用了“依赖注入”、“控制反转”的思想
403
403
@@ -410,7 +410,7 @@ doSomethingUseDependency1()
410
410
- 提高系统的稳定性
411
411
外部依赖的变化不会影响系统
412
412
- 提高系统的扩展性
413
- 可以任意修改外部依赖的实现而不影响系统
413
+ 可以任意替换外部依赖而不影响系统
414
414
- 提高系统的可维护性
415
415
系统与外部依赖解耦,便于维护
416
416
@@ -443,13 +443,13 @@ doSomethingUseDependency1()
443
443
444
444
## 注意事项
445
445
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已经足够抽象和稳定
449
449
450
450
- 有多少个Dependency接口,DependencyContainer就有多少个get/set函数
451
451
452
- - 最好一开始就使用依赖隔离模式将所有的可能会变化的外部依赖都隔离,这样可以避免到后期如果要修改外部依赖时需要修改系统所有相关代码的情况
452
+ - 最好一开始就使用依赖隔离模式将所有的可能会变化的外部依赖都隔离,这样可以避免到后期修改外部依赖时需要修改系统所有相关代码的情况
453
453
454
454
455
455
# 扩展
@@ -459,17 +459,20 @@ doSomethingUseDependency1()
459
459
460
460
如果基于依赖隔离模式这样设计一个架构:
461
461
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) -->
465
466
- 运用[ 领域驱动设计DDD] ( https://www.cnblogs.com/chaogex/p/12408802.html ) 设计系统,将系统的核心逻辑建模为领域模型,放到领域模型层
466
467
467
468
那么这样的架构就是洋葱架构
468
469
洋葱架构如下图所示:
469
470
![ image] ( https://img2022.cnblogs.com/blog/419321/202206/419321-20220609041114118-2037325753.webp )
470
471
471
- 它的核心思想就是将变化最频繁的外部依赖层隔离出去,并使变化最少的领域模型层独立而不依赖其它层。
472
- 在传统的架构中,领域模型层会依赖外部依赖层(如在领域模型中调用后端服务等),但是现在却解耦了。这样的好处就是如果外部依赖层变化,不会影响其他层
472
+ 洋葱架构与传统的三层架构的区别是颠倒了层之间的依赖关系:洋葱架构将三层架构中的最下层(外部依赖层)改为最上层;将三层架构中的倒数第二层(领域模型层)下降为最下层
473
+ 洋葱架构的核心思想就是将变化最频繁的外部依赖隔离出去,并使变化最少的领域模型层独立而不依赖其它层。
474
+ <!-- 在传统的架构中,领域模型层会依赖外部依赖层(如在领域模型中调用后端服务等),但是现在却解耦了 -->
475
+ 这样的好处是外部依赖层容易变化,但它的变化现在不会影响其他层
473
476
474
477
475
478
<!-- # 结合其它模式
@@ -505,7 +508,7 @@ doSomethingUseDependency1()
505
508
- 扩大使用场景
506
509
507
510
编辑器的外部依赖不只是引擎,也包括UI组件库等
508
- 如需要将旧的UI组件库(如React UI 组件库-Antd)替换为新的组件库,则可按照依赖隔离模式,提出UI这个Dependency接口,并加入作为接口的实现的OldUIImplement 、NewUIImplement,在这两个接口的实现中调用对应的UI组件库
511
+ 如需要将旧的UI组件库(如React UI 组件库-Antd)替换为新的组件库,则可按照依赖隔离模式,提出UI这个Dependency接口,并加入作为接口实现的OldUIImplement 、NewUIImplement,在这两个接口的实现中调用对应的UI组件库
509
512
510
513
511
514
除了编辑器外,引擎、网站等系统也可以使用依赖隔离模式
@@ -514,7 +517,7 @@ doSomethingUseDependency1()
514
517
515
518
516
519
517
- 有些外部依赖在运行时会变化,对于这种情况,在运行时注入对应的DependencyImplement即可
520
+ 有些外部依赖在运行时会变化,对于这种情况,使用依赖隔离模式后可以在运行时注入变化后的DependencyImplement
518
521
如编辑器向用户提供了“切换渲染效果”的功能:用户点击一个按钮后,就可以切换渲染引擎来渲染场景
519
522
为了实现该功能,只需在按钮的点击事件中注入对应的DependencyImplement到DependencyContainer中即可
520
523
@@ -523,17 +526,20 @@ doSomethingUseDependency1()
523
526
- 满足各种修改外部依赖的用户需求
524
527
525
528
我遇到过这种问题:3D应用开发完成后,交给3个外部用户使用。用了一段时间后,这3个用户提出了不同的修改外部依赖的要求:第一个用户想要升级3D应用依赖的渲染引擎A,第二个用户想要替换A为B,第三个用户想要同时使用B和升级后的A来渲染。
526
- 如果3D应用是直接调用外部依赖库的话,我们就需要去修改交付的3份代码中系统的相关代码,且每份代码都需要不同的修改(因为3个用户的需求不同),工作量很大;
529
+ 如果3D应用是直接调用外部依赖库的话,我们就需要将交付的代码修改为3个版本,分别满足3个用户的需求
530
+ 每个版本都需要修改系统中与外部依赖相关的所有代码,这样导致工作量很大
531
+
527
532
如果使用了依赖隔离模式进行了解耦,那么就只需要对3D应用做下面的修改:
528
533
1.修改AImplement和ALibrary(升级)
529
534
2.增加BImplement
530
535
3.增加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
+ 这样就能减少很多工作量
537
543
538
544
539
545
@@ -543,7 +549,5 @@ doSomethingUseDependency1()
543
549
544
550
关于洋葱架构,可以在网上搜索“洋葱架构”、“the-onion-architecture-part”
545
551
546
- 关于洋葱架构,可以在网上搜索“洋葱架构”、“the-onion-architecture-part”
547
-
548
552
六边形架构类似于洋葱架构,可以在网上搜索“六边形架构”
549
553
0 commit comments