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

锐评 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;
        // callback
        observe({ 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.kler.cn/news/353447.html

相关文章:

  • FFmpeg源码:avformat_new_stream函数分析
  • 2.4.ReactOS系统运行级别降低IRQL级别KfLowerIrql 函数
  • 搜维尔科技:SenseGlove Nova 2触觉反馈手套开箱测评
  • 【LaTex】12 usenix投稿模版 删除接受信息
  • android app执行shell命令视频课程补充android 10/11适配-千里马android
  • 三、ElementPlus下拉搜索加弹窗组件的封装
  • Python 基础之条件语句
  • 【JavaScript】LeetCode:66-70
  • Linux在命令行直接使用密码来远程执行命令和远程复制 + 新安装的库找不到问题解决方式
  • HDFS开启审计日志
  • 自动驾驶系列—厘米级精度:RTK技术如何革新自动驾驶定位
  • Android终端GB28181音视频实时回传设计探讨
  • 嵌入式系统---看门狗
  • 选项模式(Option Pattern)在 Go 语言中的应用
  • Unity RPG梦幻场景素材(附下载链接)
  • React中的useState和useEffect解析
  • 容器化部署kafka内外网分流
  • Unity开发Hololens项目
  • 点菜问题(北京大学考研机试题01背包)
  • 上海数据集团到访蓝象智联 探讨数据基础设施建设