当前位置: 首页 > news >正文

锐评 Nodejs 设计模式 - 创建与结构型

本系列文章的思想,都融入了 让 Java 再次伟大 这个全新设计的脚手架产品中,欢迎大家使用。

单例模式与模块系统

Node 的单例模式既特殊又简单——凡是从模块中导出的实例天生就是单例。

// database.js
function Database(connect, account, password) {this.connect = connect;this.account = account;this.password = password;
}
const database = new Database("localhost", "root", "123456");
export default database;// index.js
import database1 from "./database.js";
import database2 from "./database.js";console.log(database1 === database2); // true

这是由于 Node.js 加载完某个模块后,会创建模块与模块标识符
(module specifier) 的映射关系并将其缓存起来供后续使用,就像下面这样:

pseudo code (CommonJS module)

function loadModule(filename, module, require) {const wrappedSrc = `(function(module,exports,require) {${fs.readFileSync(filename, "utf8")} })(module,module.exports,require)`;eval(wrappedSrc);
}function require(moduleName) {const id = require.resolve(moduleName);// 加载缓存if (require.cache[id]) {return require.cache[id].exports;}const module = { exports: {} };// 缓存模块require.cache[id] = module;loadModule(id, module, require);return module.exports;
}
require.cache = {};
require.resolve = function (moduleName) {return moduleName;
};

得益于 CommonJS module 的缓存功能,在 Node 中我们不再需要专门的单例设计模式了。

循环依赖

CommonJS module 基于一个简单的原理,在立即执行函数(IIFE)中创建私有域,然后一边执行代码一边构建图。如果 a,b 两个模块互相依赖,在 a 模块执行完之前 b 模块中引用了 a 模块导出的
变量,就可能会导致 b 模块无法看到 a 模块的最终状态。
ECMAScript Modules 解决这个问题的方式是先通过分析构建图,当图构建完毕以后使用后序深度优先遍历从最后一个节点开始执行代码。这样能保证当其他节点依赖这个模块时,获取到的模块导出变量为最终状态,不会再被 b 模块自身改动。

实时绑定

CommonJS module 的 import 是通过将 b 模块导出对象进行浅拷贝到 a 模块的作用域中来实现的。如果对象的属性是原始类型的话,a 模块获得的是一份拷贝,b 模块的修改可能无法反映到这份拷贝上。
而 ECMAScript Modules 有一种名叫 live bindings 机制,使得 a 模块 import 的 b 模块是一个链接,它指向了 b 模块导出的那个对象。如果 b 模块在运行过程中更改了对象中的属性,a 模块马上就可以获取到最新的状态。而 CommonJS module 的 import 是通过将 b 模块导出对象进行浅拷贝到 a 模块的作用域中来实现的。如果对象的属性是原始类型的话,a 模块获得的是一份拷贝,b 模块的修改可能无法反映到这份拷贝上。

Revealing Constructor

如果你想设计一个只能在初始化时改变状态的类,就可以采用这种技巧。或者换一种说法:该类实例化完毕后就不可变(immutable)。

可能你会觉得 Revealing Constructor 有点陌生,因为它不是我们熟悉的那种设计模式。不过不用担心,因为你一定见过它了。在 Node.js 中这个模式运用的很广泛,其中最常见的就是 Promise。

pseudo code

class TinyPromise {constructor(executor) {this.status = "pending";this.value = undefined;executor(this.resolve);}resolve = (value) => {this.status = "resolve";this.value = value;};then = (func) => {if (this.status === "resolve") {func(this.value);}};
}new TinyPromise((resolve) => {resolve(1);
}).then((value) => {console.log(value);
});
// output: 1

Proxy 代理装饰与适配

ES 内置的 Proxy 支持在运行期 (runtime) 创建代理对象,使通过组合和委派解决问题的设计模式都能通过 Proxy 的方式得到体现,消除了硬编码代理类产生的冗余代码。

function Subject() {this.morning = () => {console.log("morning");};this.bye = () => {console.log("bye");};
}const subject = new Subject();let subjectHandler = {get: function (target, prop) {// 返回增强后的方法if (prop === "morning") {return function () {// 增强逻辑console.log("good");// 调用原对象逻辑target[prop]();};}// 返回原对象中的属性return target[prop];},// set、has、delete...
};const enhanceSubject = new Proxy(subject, subjectHandler);
enhanceSubject.morning(); // good morning
enhanceSubject.bye(); // bye

通过 Proxy 表达的代理、装饰器、适配器设计模式看起来都差不多,差别主要在于应用场景:

  1. 代理强调对某个对象的访问的控制。
  2. 装饰器强调对对象属性的增强。
  3. 适配器则提供与原对象不同的行为以适配新的场景。

Proxy 与对象虚拟化

下面这个例子实现了一个涵盖所有偶数的虚拟化数组——具备数组的行为但不实际存储数据。

const eventNumbers = new Proxy([], {get: (target, prop) => index * 2,has: (target, number) => number % 2 === 0,
});
console.log(2 in eventNumbers); // true
console.log(3 in eventNumbers); // false
console.log(eventNumbers[7]); // 14

举这个例子是想说明,Proxy 的作用不仅仅局限于设计模式,还有很多其他功能可以用 Proxy 来体现,比如:「元编程 (meta-programming)、运算符重载 (operator overloading) 和对象虚拟化 (object virtualization)」

Change Observer 与响应式编程

结合 Proxy 的 set 方法 (trapMethod) 还可以实现观察者模式——当某个对象的状态发生变化时通知观察者。

在强类型语言中观察者一般是一个对象,由于 js 中的所有函数都是闭包的缘故,所以观察者可以是一个回调函数。

// 被观测对象
const invoice = {subtotal: 100,discount: 10,tax: 20,
};// 状态发生变化时的回调函数(计算票据总额)
function calculateTotal(invoice) {return invoice.subtotal - invoice.discount + invoice.tax;
}
// target: 被代理对象 observe: 回调函数(calculateTotal)
function createObservable(target, observe) {const observable = new Proxy(target, {// obj: 原始对象 prop: 属性 value: 值set(obj, prop, value) {// 如果待设置的属性值和之前的属性不同if (value !== obj[prop]) {const prev = obj[prop];obj[prop] = value;// callbackobserve({ prop, prev, curr: value });}return true;},});return observable;
}let total = calculateTotal(invoice);
console.log(total); // 120// 创建代理对象(观测该对象属性发生变化)
const obsInvoice = createObservable(invoice, (change) => {console.log(change);console.log(calculateTotal(invoice));
});// 打印票据 total: 210
obsInvoice.subtotal = 200;
// 打印票据 total: 200
obsInvoice.discount = 20;
// 打印票据 total: 200
obsInvoice.discount = 20;
// 打印票据 total: 210
obsInvoice.tax = 30;

像上例这样由实例状态的变化来驱动业务,而不是由客户端(调用方)来发起业务调用的编程范式,称为响应式编程。(reactive programming RP)

结语

  1. 代理、装饰器、适配器都是结构型模式,体现的设计思想都差不多——通过对象的结构来管理实体间的关系。
  2. Proxy 不仅用来实现设计模式,也可以用来完成很多其他功能和编程范式。
  3. 就像 js 中的闭包函数一样,模块导出天生就是单例。
http://www.lryc.cn/news/462492.html

相关文章:

  • 【RoadRunner】自动驾驶模拟3D场景构建 | 软件简介与视角控制
  • 15分钟学Go 第4天:Go的基本语法
  • 【Qt】Qt的介绍——Qt的概念、使用Qt Creator新建项目、运行Qt项目、纯代码方式、可视化操作、认识对象模型(对象树)
  • 论文笔记:PTR: Prompt Tuning with Rules for Text Classification
  • 服务器和中转机协同工作以提高网络安全
  • Java利用itextpdf实现pdf文件生成
  • 2010年国赛高教杯数学建模C题输油管的布置解题全过程文档及程序
  • datawhale大模型bot应用开发--task3:工作流
  • 期货配资系统风控逻辑开发/完整源代码
  • 汽车免拆诊断案例 | 2023款零跑C01纯电车后备厢盖无法电动打开和关闭
  • 分布式存储架构 与分布式一致性协议
  • Unity Apple Vision Pro 保姆级开发教程 - Simulator 模拟器使用
  • Vue 之 插件与组件的区别
  • 了解 ChatGPT 中的公平性问题
  • 【PHP】安装swoole时报错:No package ‘libbrotlienc‘ found
  • postgresql执行计划解读案例
  • Matlab实现粒子群优化算法优化随机森林算法模型 (PSO-RF)(附源码)
  • 使用 EasyExcel 相邻数据相同时行和列的合并,包括动态表头、数据
  • 985研一学习日记 - 2024.10.16
  • 安装mysql 5.5.62
  • AnaTraf | 网络性能监控系统的价值
  • 决策树和集成学习的概念以及部分推导
  • servlet基础与环境搭建(idea版)
  • 【10月最新】植物大战僵尸杂交版新僵尸预告(附最新版本下载链接)
  • 网络编程-UDP以及数据库mysql
  • ubuntu 20.04 安装ros1
  • ShardingSphere-Proxy 数据库中间件MySql分库分表环境搭建
  • Pytest+selenium UI自动化测试实战实例
  • 服务器技术研究分析:存储从HBM到CXL
  • 下载并安装 WordPress 中文版