设计模式:以桥接模式和访问者模式为例,看设计模式在微信小游戏版本迭代中的应用(下)

简介: 设计模式:以桥接模式和访问者模式为例,看设计模式在微信小游戏版本迭代中的应用

使用桥接模式的意义在哪里?


现在我们思考一下,我们在碰撞检测这一块应用桥接模式,创建了许多新类,除了把项目变复杂了,到底有什么积极作用?我们将碰撞测试元素拆分为两个抽象对象(HitObjectRectangle和HitedObjectRectangle)的意义在哪里?


看一张结构图,如图11-4所示:



image.pngimage.png

image.png

图11-4待扩展的桥接模式示意图


HitObjectRectangle代表碰撞对象的碰撞检测数据对象,HitedObjectRectangle代表被碰撞对象的碰撞检测数据对象,后者有三个具体实现的子类:ScreenRectangle、LeftPanelRectangle和RightPanelRectangle,这三个子类代表三类被撞击的类型。如果游戏中出现一个四周需要被碰撞检测的对象,它的检测数据对象可以继承于ScreenRectangle;如果出现一个右侧需要碰撞检测的对象,它的检测数据对象可以继承于RightPanelRectangle,以此类推左侧出现的,它的数据对象可以继承于LeftPanelRectangle。而如果出现一个撞击对象,它的检测数据对象可以继承于BallRectangle。


目前我们这个小游戏项目太过简单,不足够显示桥接模式的作用。接下来我们做一个人为拓展,新增一个红色立方体代替小球,代码如代码清单11-14所示:


代码清单11-14创建立方体模块


1.  *// JS* *:* *src\views\cube.js*
1. import { Ball } from "ball.js"
2. import CubeRectangle from "hitTest/cube_rectangle.js"
3. 
4.  */** 红色立方块 */*
5. **class** Cube **extends** Ball {
6.   constructor() {
7.     **super**()
8.     **this**.rectangle = **new** CubeRectangle()
9.   }
10. 
11.    */** 渲染 */*
12.   render(context) {
13.     context.fillStyle = "red"
14.     context.beginPath()
15.     context.rect(**this**.rectangle.left, **this**.rectangle.top, **this**.rectangle.width, **this**.rectangle.height)
16.     context.fill()
17.   }
18. }
19. 
20. export default **new** Cube()


Cube类的代码与Ball是类似的,只有render代码略有不同,让它继承于Ball是最简单的实现方法。第9行,rectangle设置为CubeRectangle的实例,这个类尚不存在,稍后我们创建,它是BallRectangle的子类。


在cube.js文件中引入的Ball(第2行)现在还没有导出,我们需要修改一下ball.js文件,如下所示:


1.// JS:src/views/ball.js
2....
3.
4./** 小球 */
5.// class Ball {
6.export class Ball {
7.  ...
8.}
9....


第6行,使用export关键字添加了常规导出,其它不会修改。

现在看一下新增的CubeRectangle类,如下所示:


1.// JS:src\views\hitTest\ball_rectangle.js
2.import BallRectangle from "ball_rectangle.js"
3.
4./** 碰撞对象的具体实现部分,立方体的大小及运动数据对象 */
5.class CubeRectangle extends BallRectangle { }
6.
export default CubeRectangle


CubeRectangle是立方块的检测数据对象。CubeRectangle可以继承于HitObjectRectangle实现,但因为立方体与小球特征很像,所以让它继承于BallRectangle更容易实现。事实上它像一个“富二代”,只需要继承(第5行),什么也不用做。


接下来开始使用立方块。为了使测试代码简单,我们将game.js文件中的页面创建代码修改一下,如代码清单11-15所示:


代码清单11-15使用页面工厂创建页面对象


1.// JS:disc\第11章\11.1\11.1.2\game.js
2....
3.// import PageBuildDirector from "src/views/page_build_director.js" // 引入页面建造指挥者
4.import PageFactory from "src/views/page_factory.js" // 引入页面工厂
5.
6./** 游戏对象 */
7.class Game extends EventDispatcher {
8.  ...
9.
10.  /** 游戏换页 */
11.  turnToPage(pageName) {
12.    ...
13.    // this.#currentPage = PageBuildDirector.buildPage(pageName, { game: this, context: this.#context })
14.    this.#currentPage = PageFactory.createPage(pageName, this, this.#context)
15.    ...
16.  }
17.
18.  ...
19.}
20....


只有两处改动,第4行和第14行,继承使用PageBuildDirector不利于代码测试,使用PageFactory代码会更简单。这一步改动与本小节的桥接模式没有直接关系。


最后修改game_index_page.js文件,使用立方块,代码如下:


1.// JS:src\views\game_index_page.js
2....
3.// import ball from "ball.js" // 引入小球单例
4.import ball from "cube.js" // 引入立方块实例
5....


只有第4行引入地址变了,其他不会改变。


代码扩展完了,重新编译测试,游戏的运行效果如图11-5所示:


image.png

图11-5小球变成了红色方块


改动后,白色的小球变成了红色的方块。看到了吗?项目的可扩展性非常好,在应用了桥接模式以后,当我们把小球扩展为方块时,只需要少量的变动就可以做到了。


现在,将CubeRectangle纳入结构图,如图11-6所示:

image.png

图11-6扩展后的桥接模式示意图


第四层添加了一个CubeRectangle,我们的HitObjectRectangle修改了吗?没有。虽然在HitObjectRectangle的hitTest方法中,我们使用instanceof进行了类型判断,如下所示:


1.  */*** *与被撞对象的碰撞检测* **/*
1. hitTest(hitedObject) {
2.   **let** res = 0
3.   **if** (hitedObject **instanceof** LeftPanelRectangle) {
4.     ...
5.   } **else** **if** (hitedObject **instanceof** RightPanelRectangle) {
6.     ...
7.   } **else** **if** (hitedObject **instanceof** ScreenRectangle) {
8.     ...
9.   }
10.   **return** res
11. }
复制代码


但判断的是基本类型,在第四层添加子类型不会影响代码的执行。我们添加的CubeRectangle继承于BallRectangle,属于HitObjectRectangle一支,如果添加一个新类继承于HitedObjectRectangle的子类(即ScreenRectangle、LeftPanelRectangle和RightPanelRectangle),结果是一样的,代码不用修改仍然有效。HitObjectRectangle和HitedObjectRectangle作为抽象部分,是我们实现的桥接模式中的重要组成部分,它们帮助具体实现部分屏蔽了变化的复杂性。


注意:如果我们添加了新的碰撞检测类型,不同于ScreenRectangle、LeftPanelRectangle和RightPanelRectangle中的任何一个,代码应该如何拓展?这时候就需要修改HitObjectRectangle类的hitTest方法啦,需要添加else if分支。


注:桥接模式完成后的最终源码目录见:disc/第11章/11.1/11.1.2,读者可以自行下载随书源码以对照查看。


总结一下桥接模式的用法


最后总结一下,在桥接模式中,是两部分对象分别实现抽象部分与具体部分,然后这两部分对象相对独立自由的变化。在本小节示例中,我们主要应用桥接模式实现了碰撞检测。小球和立方块是撞击对象,左右挡板及屏幕是被撞击对象,通过相同的方式定义它们的大小、位置数据,然后以一种相对优雅的方式实现了碰撞检测。


对比重构前后的代码,我们不难发现,在应用桥接模式之前,我们的碰撞检测代码是与GameIndexPage、Ball、LeftPanel和RightPanel耦合在一起的,并且不方便进行新的碰撞对象扩展;在重构以后,我们碰撞检测的代码变成了只有top、bottom、left和right属性数值的对比,变得非常清晰了。


第11章所有面向对象重构中使用的设计模式,桥接模式是最复杂的一个,它所用的笔墨最多。在大型跨平台GUI软件中,桥接模式基本是必出现的。


另一个访问者模式的应用


在应用了桥接模式以后,你是不是对设计模式的作用已经了然于胸了呢?有意识地运用设计模式,是不是可以更大限度地应对需求变化的复杂性,从而保证版本迭代的稳定与快捷?


下面我们再看一个访问者模式的应用。以下内容属于《微信小游戏开发》前端篇第11章第32课,我们尝试在随书源码第11章/11.1/11.1.3的基础之上,尝试应用访问者模式,目的仍然是有针对性地锤炼学习者渐进性模块化重构和面向对象重构思维的能力。


当前的小游戏项目分析


目前我们在实现碰撞检测功能的时候,在HitObjectRectangle类中有一个很重要的方法,如代码清单11-20所示:


代码清单11-20撞击对象的hitTest方法


1.// JS:src\views\hitTest\hit_object_rectangle.js
2....
3.
4./** 碰撞对象的抽象部分,球与方块的注册点在中心,不在左上角 */
5.class HitObjectRectangle extends Rectangle {
6.  ...
7.
8.  /** 与被撞对象的碰撞检测 */
9.  hitTest(hitedObject) {
10.    let res = 0
11.    if (hitedObject instanceof LeftPanelRectangle) { // 碰撞到左挡板返回1
12.      ...
13.    } else if (hitedObject instanceof RightPanelRectangle) { // 碰撞到右挡板返回2
14.      ...
15.    } else if (hitedObject instanceof ScreenRectangle) {
16.      ...
17.    }
18.    return res
19.  }
20.}
21.
22.export default HitObjectRectangle
复制代码


正是hitTest这个方法实现了碰撞检测,它根据不同的被撞击的对象,分别做了不同的边界检测。

但是这个方法它有坏味,它内部有if else,并且这个if else是会随着被检测对象的类型增长而增加的。

怎么优化它呢?有没有优化办法?


我们可以使用访问者模式重构。在访问者模式中,可以根据不同的对象分别作不同的处理,这里多个被撞击的对象,恰好是定义中所说的不同的对象。


什么是访问者模式?


在应用访问者模式之前,老规矩,先来简单了解一下它的概念吧。


访问者模式是一种行为设计模式, 它能将算法与算法所作用的对象隔离开来。


换言之,访问者模式根据访问者不同,展示不同的行为或做不同的处理。使用访问者模式,一般意味着调用反转,本来是A调用B,结果该调用最终反赤来是通过B调用A完成的。


在这个模式中一般有两个方面,我们可以拿软件外包市场中的甲方乙方类比一下,甲方是发包方,乙方是接包方,本来需要甲方到乙方公司系统阐明需求,由乙方根据不同需求安排不同的项目进行开发;现在反过来了,甲方不动窝了,由乙方分别派不同的开发小组,到甲方公司内部,现场与甲方进行对接。


访问者模式的实现与应用


下面开始访问者模式的实践。我们先给LeftPanelRectangle、RightPanelRectangle和ScreenRectangle都添加一个相同的方法accept,第一个LeftPanelRectangle的改动是这样的:


1.  *// JS* *:* *src\views\hitTest\left_panel_rectangle.js*
1. ...
2. 
3.  */** 被碰撞对象左挡板的大小数据 */*
4. **class** LeftPanelRectangle **extends** HitedObjectRectangle {
5.   ...
6. 
7.   visit(hitObject) {
8.     **if** (hitObject.left < **this**.right && hitObject.top > **this**.top && hitObject.bottom < **this**.bottom) {
9.       **return** 1 << 0
10.     }
11.     **return** 0
12.   }
13. }
14. 
15. export default LeftPanelRectangle


第8行至第13行,在这个新增的visit方法中,代码是从原来HitObjectRectangle类中摘取一段并稍加修改完成的,这里碰撞检测只涉及两个对象的边界,没有if else,逻辑上简洁清晰多了。


第二个RightPanelRectangle类的改动是这样的:


1.  *// JS* *:* *src\views\hitTest\right_panel_rectangle.js*
1. ...
2. 
3.  */** 被碰撞对象右挡板的大小数据 */*
4. **class** RightPanelRectangle **extends** HitedObjectRectangle {
5.   ...
6. 
7.   visit(hitObject) {
8.     **if** (hitObject.right > **this**.left && hitObject.top > **this**.top && hitObject.bottom < **this**.bottom) {
9.       **return** 1 << 1
10.     }
11.     **return** 0
12.   }
13. }
14. 
export default RightPanelRectangle


第8行至第13行,这个visit方法的实现,与LeftPanelRectangle中visit方法的实现如出一辙。


第3个是ScreenRectangle类的改动如代码清单11-21所示:


代码清单11-21屏幕的被碰撞对象


1.// JS:src\views\hitTest\screen_rectangle.js
2....
3.
4./** 被碰撞对象屏幕的大小数据 */
5.class ScreenRectangle extends HitedObjectRectangle {
6.  ...
7.
8.  visit(hitObject) {
9.    let res = 0
10.    if (hitObject.right > this.right) { // 触达右边界返回4
11.      res = 1 << 2
12.    } else if (hitObject.left < this.left) { // 触达左边界返回8
13.      res = 1 << 3
14.    }
15.    if (hitObject.top < this.top) { // 触达上边界返回16
16.      res = 1 << 4
17.    } else if (hitObject.bottom > this.bottom) { // 触达下边界返回32
18.      res = 1 << 5
19.    }
20.    return res
21.  }
22.}
23.
24.export default ScreenRectangle

第8行至第21行,是新增的visit方法。所有返回值,与原来均是一样的,代码的逻辑结构也是一样的,只是从哪个对象上取值进行比较作了变化。


上面这3个类都是HitedObjectRectangle的子类,为了让基类的定义更加完整,我们也修改一下hited_object_rectangle.js文件,如下所示:


1.// JS:src\views\hitTest\hited_object_rectangle.js
2....
3.
4./** 被碰撞对象的抽象部分,屏幕及左右挡板的注册点默认在左上角 */
5.class HitedObjectRectangle extends Rectangle {
6.  ...
7.
8.  visit(hitObject) { }
9.}
10.
11.export default HitedObjectRectangle


仅是第8行添加了一个空方法visite,这个改动可以让所有HitedObjectRectangle对象都有一个默认的visite方法,在某些情况下可以避免代码出错。


最后我们再看一下HitObjectRectangle类的改动,这也是访问者模式中的核心部分,如代码清单11-22所示:


代码清单11-22在撞击对象中应用访问者模式


1.// JS:src\views\hitTest\hit_object_rectangle.js
2....
3.
4./** 碰撞对象的抽象部分,球与方块的注册点在中心,不在左上角 */
5.class HitObjectRectangle extends Rectangle {
6.  ...
7.
8.  /** 与被撞对象的碰撞检测 */
9.  hitTest(hitedObject) {
10.    // let res = 0
11.    // if (hitedObject instanceof LeftPanelRectangle) { // 碰撞到左挡板返回1
12.    //   if (this.left < hitedObject.right && this.top > hitedObject.top && this.bottom < hitedObject.bottom) {
13.    //     res = 1 << 0
14.    //   }
15.    // } else if (hitedObject instanceof RightPanelRectangle) { // 碰撞到右挡板返回2
16.    //   if (this.right > hitedObject.left && this.top > hitedObject.top && this.bottom < hitedObject.bottom) {
17.    //     res = 1 << 1
18.    //   }
19.    // } else if (hitedObject instanceof ScreenRectangle) {
20.    //   if (this.right > hitedObject.right) { // 触达右边界返回4
21.    //     res = 1 << 2
22.    //   } else if (this.left < hitedObject.left) { // 触达左边界返回8
23.    //     res = 1 << 3
24.    //   }
25.    //   if (this.top < hitedObject.top) { // 触达上边界返回16
26.    //     res = 1 << 4
27.    //   } else if (this.bottom > hitedObject.bottom) { // 触达下边界返回32
28.    //     res = 1 << 5
29.    //   }
30.    // }
31.    // return res
32.    return hitedObject.visit(this)
33.  }
34.}
35.
export default HitObjectRectangle


第10行至第31行,是hitTest方法中被注释掉的旧代码,看到了吗, 原来复杂的if else逻辑没有了,只留下简短的一句话(第32行) , 简单吧?这就是设计模式的力量,不仅现在简单,稍后如果我们要添加其他碰撞对象与被碰撞对象,这里也不需要变动,代码的可扩展性非常好。


我们在增加新的碰撞检测对象时,只需要创建新类就可以了,没有if else逻辑需要添加,也不会影响旧代码。第9行,这里的hitTest方法,相当于一般访问者模式中的accept方法。


当我们将访问者模式和桥接模式完成结合应用时,代码变得异常简洁清晰了,这才是好的面向对象设计该有的样子。


小游戏的运行效果与之前是一致的,如下所示。


image.png

image.png

注:访问者模式实践完成后,源码目录见:disc/第11章/11.2/11.2.1,读者可以自行下载随书源码与之对照。


总结一下访问者模式的用法


最后总结一下,访问者模式特别擅长将拥有多个if else逻辑或switch分支逻辑的代码,以一种反向调用的方式,转化为两类对象之间一对一的逻辑关系进行处理。这是一个应用十分普遍的设计模式,当遇到复杂的if else代码时,可以考虑使用该模式重构。


小结


以上就是桥接模式与访问者模式的应用举例了,这些设计模式是通用的,与业务无关,学完以后不可以应用于小游戏开发中,在其他前端项目中也可以用到,甚至在其他编程语言中也可以用到。设计模式本质上是一种组织软件功能、架构代码模块的面向对象思想,这种思想貌似让我们在开始写代码的时候多干了一些活,但干这些活的精力是值得投入的,它让我们可以把其他的活干得更快、更稳、更好。


只有走得稳,才可以走得更远、更快。不知道你看到这里,有没有理解设计模式在项目开发中的作用?有人可能会反驳说,项目着急上线根本不给我们仔细分析需求与架构的时间,怎么应用设计模式?


快速上线是没有问题的,时间就是产品的生命;但在第一版本上线之后,趁运营兄弟忙碌的时候,程序员要马上进行渐进式重构,你注意看一下,我在前面提到重构时,前面都加了“渐进式”三个字。在书中作者也是这样强调的,重构并不发生在项目之初,对设计模式的应用也是在基本功能尘埃落定之后进行的。


一个大都市,为了盖几座新楼、修几条新路,难道要把所有人都赶出城去吗?不会的,肯定是在尽量不影响居民生产生活的前提下同步进行的,这便是渐进式,这个道理在软件开发中同样适用。


只有走得稳,才可以走得更远、更快,而设计模式与渐进式面向对象重构思想可以帮助我们做到这一点。

目录
相关文章
|
2月前
|
前端开发 API UED
我写个HarmonyOS Next版本的微信聊天02
我写个HarmonyOS Next版本的微信聊天02
106 9
我写个HarmonyOS Next版本的微信聊天02
|
2月前
|
人工智能 安全 前端开发
我写个HarmonyOS Next版本的微信聊天01
我写个HarmonyOS Next版本的微信聊天01
62 1
我写个HarmonyOS Next版本的微信聊天01
|
9天前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践感悟####
本文作为一篇技术性文章,旨在深入探讨Python编程中设计模式的应用价值与实践心得。在快速迭代的软件开发领域,设计模式如同导航灯塔,指引开发者构建高效、可维护的软件架构。本文将通过具体案例,展现设计模式如何在实际项目中解决复杂问题,提升代码质量,并分享个人在实践过程中的体会与感悟。 ####
|
1月前
|
存储 缓存 开发框架
提高微信小程序的应用速度
【10月更文挑战第21天】提高微信小程序的应用速度需要从多个方面入手,综合运用各种优化手段。通过不断地优化和改进,能够显著提升小程序的性能,为用户带来更流畅、更高效的使用体验。
50 3
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
19 1
|
2月前
|
设计模式 PHP 开发者
PHP中的设计模式:桥接模式的解析与应用
在软件开发的浩瀚海洋中,设计模式如同灯塔一般,为开发者们指引方向。本文将深入探讨PHP中的一种重要设计模式——桥接模式。桥接模式巧妙地将抽象与实现分离,通过封装一个抽象的接口,使得实现和抽象可以独立变化。本文将阐述桥接模式的定义、结构、优缺点及其应用场景,并通过具体的PHP示例代码展示如何在实际项目中灵活运用这一设计模式。让我们一起走进桥接模式的世界,感受它的魅力所在。
|
2月前
|
设计模式 测试技术 持续交付
架构视角下的NHibernate:设计模式与企业级应用考量
【10月更文挑战第13天】随着软件开发向更复杂、更大规模的应用转变,数据访问层的设计变得尤为重要。NHibernate作为一个成熟的对象关系映射(ORM)框架,为企业级.NET应用程序提供了强大的支持。本文旨在为有一定经验的开发者提供一个全面的指南,介绍如何在架构层面有效地使用NHibernate,并结合领域驱动设计(DDD)原则来构建既强大又易于维护的数据层。
39 2
|
2月前
|
设计模式 算法 PHP
PHP中的设计模式:策略模式的深入解析与应用
【10月更文挑战第8天】 在软件开发的浩瀚宇宙中,设计模式如同星辰指引,照亮了代码设计与架构的航道。本文旨在深入探索PHP语境下策略模式(Strategy Pattern)的精髓,不仅剖析其内核原理,还将其融入实战演练,让理论在实践中生根发芽。策略模式,作为解决“如何优雅地封装算法族”的答案,以其独特的灵活性与扩展性,赋予PHP应用以动态变换行为的能力,而无需牵动既有的类结构。
31 2
|
2月前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践###
【10月更文挑战第18天】 本文深入探讨了Python编程中设计模式的应用与实践,通过简洁明了的语言和生动的实例,揭示了设计模式在提升代码可维护性、可扩展性和重用性方面的关键作用。文章首先概述了设计模式的基本概念和重要性,随后详细解析了几种常用的设计模式,如单例模式、工厂模式、观察者模式等,在Python中的具体实现方式,并通过对比分析,展示了设计模式如何优化代码结构,增强系统的灵活性和健壮性。此外,文章还提供了实用的建议和最佳实践,帮助读者在实际项目中有效运用设计模式。 ###
20 0
|
2月前
|
JSON 小程序 JavaScript
uni-app开发微信小程序的报错[渲染层错误]排查及解决
uni-app开发微信小程序的报错[渲染层错误]排查及解决
591 7