代码要写成别人看不懂的样子(二)

前言

  上一期我们,浅尝辄止,稍微介绍了下代码如何写才能更高级,更有内涵,主要是需要配合继承,或是某些数据特性来说。这一节咱们放个大招,运用兵法来写代码,保证初学者看不懂,既便能看懂,也不知道为什么这样做,即便知道为什么这样做,没有系统学习过的人也做不好,也说不出个所以然来。

  这部兵法就是– 设计模式
在这里插入图片描述

何为设计模式?

  古有云:“何谓虽练无益?今一营之卒为炮手者常十也。不知兵法五兵迭用,当长以卫短,短以救长,一也。”

  (翻译成人话)

  意思就是训练要有意义,不能瞎练。类比到我们写代码,就是要按照一定的章法,什么时候改写什么,怎么写,要符合核心思想,这样写出来的代码,美观,漂亮,可维护性强,不易出错,而且关键的是,别人看不懂。

  那么核心思想是什么,就是 根据你的目的去筛选要使用的设计模式 ,比如说现在有一个需求,让你给一个天气预报的功能块下方增加一个 PM 指数的展示,当然你可以直接在天气预报的方法最下面增加一个展示 PM 值的小方法。

  不过这样就污染了原先的函数,导致函数功能不单一,第一不好维护,第二,复用不方便。当有别处需要使用天气功能,但是不想添加 PM 值的时候,该函数又得修改,判断是何处调用的,来确定要不要执行 PM 方法。

   那么该怎么解决这个问题,才是符合兵法的呢?

  我们可以给 Function 函数的原型上设置一个 after 方法,让这个方法在执行完函数后触发,这样把显示 PM 的方法放到 after 里面,不会对原函数造成污染,而且需要的时候,加上 after 函数,不需要的时候,我们就不使用 after ,也保证了原函数的复用性。

  具体做法如下:

//在执行函数 前 执行
Function.prototype.before = function (beforefn) {
	let _self = this;                         //保存原函数引用
	return function () {                      //返回包含了原函数和新函数的"代理函数"
		beforefn.apply(this, arguments);      //执行新函数,修正this
		return _self.apply(this, arguments);  //执行原函数
	}
};

//在执行函数 后 执行
Function.prototype.after = function (afterfn) {
	let _self = this;
	return function () {
		var ret = _self.apply(this, arguments);
		afterfn.apply(this, arguments);
		return ret;
	}
};

  上面在Function的原型上,增加了两个方法,一个 before ,一个 after before 会在函数执行前先执行, after 会在函数执行后立即执行。

  使用的时候只需要指定对应的函数即可,如下:

let func = function () {
    console.log('天气预报')}
func = func.before(function () {
	console.log('天气预报之前执行的函数');
}).after(function () {
	console.log('之后执行的PM方法');
});
func();//天气预报之前执行的函数
	   //天气预报
	   //之后执行的PM方法

  这回如果后面需要升级增加新功能,也都可以放在我们设置的这两个函数里,是不是感觉后面的路越来越轻松了。讲到这里,似乎有些跑题。这个方法和兵法有什么关系?

  别着急,上面这种,不破坏原函数,只是通过某种方法,对原函数进行功能增加的手段,就是设计模式中的 装饰者模式 ,是不是不知不觉的我们就融入灵魂了。

装饰者模式

  这个也叫装饰器模式,是一种比较常见的设计模式,举个例子,比如穿衣服,不同的衣服款式不同,颜色不同,所适合的场合也不同,无论增加什么样子的衣服,人还是不变的,他的高矮胖瘦不会随衣服变化,换句话说,衣服只是我们为了达到某种目的(比如为了吸引异性)的一种手段,这个手段只到起装饰作用,并不起到决定性作用。

  有一点需要注意,穿衣服,脱衣服的过程是简单易实现的,这一点很重要,如果不能很方便的穿脱服装,每次恨不得扒一层皮下来,那这个过程就失去了意义。(做衣服的过程可以稍微复杂,因为衣服只需要做一次,做好之后我们随用随穿)

适用环境

  这种思想比较适合在已经成型的项目上增加新的功能,项目中的各个函数,方法一般都要求高内聚,低耦合。模块之间各干各的,别掺和其他人的事。

总结

  通过上面的几部分的描述,简单总结一下,设计模式是前辈们在长期编码过程中总结出来的一套编码习惯,这些习惯可以让我们在编码过程中提高效率。(这里的效率不仅指编码过程中,同时还能提高项目的效率,使整体代码可维护,易修改,方便阅读和使用)。

  补充一点,设计模式是不受语言的限制的,语言的不同只是实现方式的不同,而思想永远是一致的。所以古人常说,兵法精髓,在于一个“”字。

  今后的文章,我会对各个设计模式进行逐一的介绍,我个人也在边学习边总结,文中有出错的地方,各位前辈多多指点。




完全没关系!你说“**我全都看不懂**”,那我们就从最最基础的开始,像教一个刚学编程的人一样,**一句一句、一步一步地解释这段代码到底在做什么**。 我们不跳步、不假设你知道任何图形学或数学知识。准备好,我们现在开始: --- ## 🌟 目标:画一个“圆柱体”(比如像一根水管、一个罐头) 我们要用 OpenGL(一种图形库)在屏幕上画出这个圆柱体。 但计算机不能直接理解“圆柱”这种东西,它只能画: - 点 - 线 - 三角形 所以我们必须把“圆柱”拆成很多小三角形拼起来 —— 就像乐高积木搭模型。 --- ## 🔧 第一步:准备材料(定义参数) ```cpp void Cylinder::initData(DataParam *param) { dataParam = *param; ``` ### ✅ 这句什么意思? - `DataParam *param` 是一个“装数据的盒子”的指针。 - 它里面可能有这些信息: ```cpp struct DataParam { float radius; // 圆柱半径(粗细) GLuint longSegments; // 把圆周切成几段(越多样子越圆) float height; // 高度(多长) }; ``` - `dataParam = *param;` 表示:“把这个盒子的内容复制到我自己的成员变量里保存下来。” 👉 所以现在我们知道要画一个多粗、多高、多精细的圆柱了。 --- ### 继续往下: ```cpp GLfloat radius = dataParam.radius; GLuint longSegments = dataParam.longSegments; GLfloat height = dataParam.height; ``` ✅ 把刚才复制过来的数据取出来,起个短名字方便后面用。 比如: - `radius` 就是半径(比如 1.0) - `height` 是高度(比如 2.0) - `longSegments` 是“把圆切几块”(比如 8 块 → 八边形) --- ## 📐 第步:计算角度(怎么画一个圆?) ```cpp float fTheta = (glm::pi<float>() * 2.0f) / longSegments; ``` ### ❓这是什么? 我们来慢慢讲。 ### 💡 想象你在一个操场上,想绕着中心走一圈,走出一个“圆形”。 你可以: 1. 每次走一小步 2. 走完后转一个小角度 3. 再走下一步…… 这样走很多次,就画出了一个近似的圆。 而每次转的角度是多少呢? > 一圈总共是 **360 度**,也就是 **2π 弧度**(程序员喜欢用弧度) 如果你把圆分成 `longSegments = 8` 段,那每段就是: $$ \frac{2\pi}{8} = \frac{\pi}{4} $$ 👉 所以这行代码的意思是: > “我要把一个完整的圆($2\pi$)平均分成 `longSegments` 份,每一份的角度差是 `fTheta`” 🎯 `fTheta` 就是你每画一个点时增加的角度。 --- ## 🧮 第三步:需要多少个顶点?(提前申请内存) ```cpp int numVertices = 2 * (longSegments + 1) + (longSegments + 2) + (longSegments + 2); ``` 这句话看起来复杂,其实就是在数:**一共要用多少个“点”来组成这个圆柱?** 我们把它拆开来看! ### 🔹 第一部分:侧面(筒身)→ `2 * (longSegments + 1)` - 我们要在上下两个圈上各放一堆点: - 下面一圈:`longSegments + 1` 个点(因为首尾重合,所以比段数多1) - 上面一圈:同样 `longSegments + 1` 个点 - 所以侧面一共需要:`2 * (L+1)` 个点 📌 举个例子:如果 `longSegments = 8`,那么每圈9个点,共 `2×9=18` 个点 --- ### 🔹 第部分:上底盖(顶上的圆)→ `longSegments + 2` - 画一个圆盖,要用 `GL_TRIANGLE_FAN`(扇形模式),需要: - 1 个中心点 - `longSegments + 1` 个边缘点(闭合) - 所以总共:`1 + (L+1) = L+2` 个点 --- ### 🔹 第三部分:下底盖(底下的圆)→ 又一个 `longSegments + 2` - 同理,下面也要画一个圆盖,也需要 `L+2` 个点 --- ### ✅ 加起来总数: ``` 侧面: 2*(L+1) 上盖: L+2 下盖: L+2 总点数 = 2L+2 + L+2 + L+2 = 4L + 6 ``` 代入 `L=8` → `4×8 + 6 = 38` 个点 所以我们要准备一个能装 **38 个点** 的数组。 --- ## 🗃️ 第四步:创建一个“点”的数组 ```cpp if (vertices) { delete[] vertices; } vertices = new TextureColorVertex[numVertices]; ``` ### ✅ 解释: - `vertices` 是一个数组,用来存所有的“顶点” - 每个顶点包含: - 坐标(x, y, z) - 颜色(r, g, b) - 纹理坐标(s, t) 👉 就像 Excel 表格的一行,记录一个点的所有信息。 #### 为什么先 `delete[]`? - 如果之前已经画过一次圆柱,`vertices` 已经分配过内存了 - 再次初始化前必须先释放旧内存,否则会**内存泄漏** #### `new TextureColorVertex[38]` 是什么? - 在电脑内存中开辟一块空间,可以放 38 个这样的“点” - 就像租了一个有 38 个格子的柜子,每个格子放一个点的信息 --- ## 🌀 第五步:生成侧面的点(最关键的一步) ```cpp for (int i = 0; i < (longSegments + 1); i++) { ``` 👉 循环 `L+1` 次(比如 9 次),每次生成一对点:一个在底下,一个在顶上。 --- ### 🟢 第一个点:底部的点 ```cpp vertices[2 * i].coordinate.x = radius * cosf(i * fTheta); vertices[2 * i].coordinate.y = -(height / 2.0f); vertices[2 * i].coordinate.z = radius * sinf(i * fTheta); ``` 我们来理解这三行。 #### 数学知识:如何用角度算出 x 和 z? 想象你在画一个圆: - 角度为 θ - 半径为 r - 那么: - $ x = r \cdot \cos(\theta) $ - $ z = r \cdot \sin(\theta) $ 这里的 `i * fTheta` 就是当前的角度。 例如: - i=0 → 角度=0 → x=r, z=0 → 正右方 - i=1 → 角度=π/4 → 斜上方 - ... - i=8 → 角度=2π → 回到起点 🎯 所以这一系列点就在 XY 平面上绕了一圈(其实是 XZ 平面,Y 是上下) Y 固定为 `-height / 2.0f` → 所有底边点都在“最下面” --- ### 🔵 第个点:顶部的点 ```cpp vertices[2 * i + 1].coordinate.x = radius * cosf(i * fTheta); vertices[2 * i + 1].coordinate.y = (height / 2.0f); vertices[2 * i + 1].coordinate.z = radius * sinf(i * fTheta); ``` 和上面几乎一样,只是 Y 是 `+height/2` → 所有点都在“最上面” --- ### 📦 存储位置是怎么安排的? | i | 底部点索引 | 顶部点索引 | |---|------------|------------| | 0 | 0 | 1 | | 1 | 2 | 3 | | 2 | 4 | 5 | | ... | ... | ... | 所以整个侧面变成了这样一排点: ``` [底0][顶0][底1][顶1][底2][顶2]... ``` OpenGL 用 `GL_TRIANGLE_STRIP` 把它们连成带状三角形,形成筒壁。 --- ### 🎨 设置纹理坐标(简单理解:贴图位置) ```cpp vertices[2 * i].texture.s = (longSegments - i) * (1.0f / longSegments); vertices[2 * i].texture.t = 0; ``` - `s` 和 `t` 类似于图片上的横纵坐标(0~1之间) - 这里 `s` 是 `(L - i)/L` → 随着 i 增大而减小 → **从右往左贴图** - `t=0` → 贴图最下面 - 顶点设 `t=1` → 贴图最上面 ⚠️ 缺点:纹理是倒着贴的!正常应该用 `i * (1.0/L)` --- ### 🖍️ 设置颜色 ```cpp vertices[2 * i].color.r = ... = 1.0f; ``` - RGB 都设为 1 → 白色 --- ## 🛑 到这里为止:完成了侧面 18 个点 接下来要“上底盖”和“下底盖” 所以我们需要知道:**下一个该哪个位置?** --- ## 📍 关键行来了! ```cpp int start = 2 * (longSegments + 1); ``` 👉 这句话的意思是: > “我已经了 `2*(L+1)` 个点(侧面),所以下一个可用的位置是第 `2*(L+1)` 号。” 比如 `L=8` → `2*9 = 18` → 所以下一个点从 `vertices[18]` 开始 🎯 完全就是一个“记账”操作:前面用了多少格子?现在该从哪开始? --- ## ✅ 总结:你现在明白了吗? | 代码 | 实际含义 | |------|----------| | `fTheta = 2π / L` | 每一步转多少角度 | | `numVertices = ...` | 总共要多少个点 | | `new TextureColorVertex[N]` | 开辟内存存点 | | `for(...)` | 一个个生成点 | | `x = r*cosθ`, `z = r*sinθ` | 用三角函数画圆 | | `start = 2*(L+1)` | 记录“接下来从哪个位置开始” | --- ## 🧩 最后一句话类比: 想象你要做一盘饺子: 1. 先看要做几个(`longSegments`) 2. 准备面粉和馅(`new` 内存) 3. 擀皮:一个个切好(生成侧面点) 4. 包完一圈后,开始做盖子(上底、下底) 5. `start` 就像是你对老婆说:“前面18个饺子包完了,现在该包盖子了,从第19个位置摆起。” ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值