跳到主要内容

27 篇博文 含有标签「JS」

Javascript

查看所有标签

命令模式 (Command)

· 阅读需 4 分钟

命令模式是一种行为设计模式,它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。

  • 单一职责原则。可以解耦触发执行操作的类。
  • 开闭原则。可以在不修改已有客户端代码的情况下在程序中创建新的命令。
  • 可以实现撤销和恢复功能。
  • 可以实现操作的延迟执行。
  • 可以将一组简单命令组合成一个复杂命令。
  • 代码可能会变得更加复杂,因为在发送者和接收者之间增加了一个全新的层次。

示例

下面的示例模拟了一个应用,点击按钮,复制文本到剪贴板的场景:

Command 是抽象命令类,基于该类派生出 CopyCommand 具体类,用于执行复制操作。Application 是发送者类,将复制命令发送给 Button 接收者类,由他执行命令。

// 抽象命令类
abstract class Command {
protected editor: Editor;
protected app: Application;

constructor(editor: Editor, app: Application) {
this.editor = editor;
this.app = app;
}

// 抽象执行方法
abstract execute(): void;
}

// 具体复制命令类,继承了抽象命令类,一定要调用 super 构造器
class CopyCommand extends Command {
constructor(editor: Editor, app: Application) {
super(editor, app);
}

// 实现执行复制的方法
public execute(): void {
const text = this.editor.getSelection();
this.app.setClipboard(text);
}
}

// 编辑器类,可以添加、选取、删除文本
class Editor {
private text: string = '';

public getSelection(start?: number, stop?: number) {
return this.text.slice(start, stop);
}

public addText(text: string) {
this.text += text;
}

public deleteText(start?: number, stop?: number) {
this.text.replace(this.text.slice(start, stop), '');
}
}

// 按钮类(接收者),可以设置、执行命令
class Button {
private command?: Command;

public setCommand(command: Command) {
this.command = command;
}

public onClick() {
this.command?.execute();
}
}

// 应用类(发送者),将命令委派给接收者 Button 执行
class Application {
private clipboard: string;
private button: Button;
private editor: Editor;

// 初始化剪贴板、编辑区、按钮
constructor() {
this.clipboard = '';
this.button = new Button();
this.editor = new Editor();
this.button.setCommand(new CopyCommand(this.editor, this));
}

// 向编辑器中写入文本
public WriteSomethingInEditor(text: string) {
this.editor.addText(text);
}

// 设置剪贴板内容
public setClipboard(text: string) {
this.clipboard = text;
}

// 获取剪贴板内容
public getClipboard() {
return this.clipboard;
}

// 复制按钮点击事件,将设置编辑器中所选文本到剪贴板
public clickCopy() {
this.button.onClick();
}
}

const app = new Application();
// 向编辑框写入文本
app.WriteSomethingInEditor('Something.');
// 点击复制按钮
app.clickCopy();
// 获取剪贴板内容
console.log(app.getClipboard()); // Something.

迭代器模式 (Iterator)

· 阅读需 3 分钟

迭代器模式是一种行为设计模式,可以在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。

  • 单一职责原则。通过将体积庞大的遍历算法代码抽取为独立的类,可对客户端代码和集合进行整理。
  • 开闭原则。可实现新型的集合和迭代器并将其传递给现有代码,无需修改现有代码。
  • 可以并行遍历同一集合,因为每个迭代器对象都包含其自身的遍历状态。
  • 可以暂停遍历并在需要时继续。
  • 如果你的程序只与简单的集合进行交互,应用该模式可能会矫枉过正。
  • 对于某些集合,使用迭代器可能比直接遍历的效率低。

示例

举例说明,以下代码对微信好友进行遍历。定义了两个接口 SocialNetwork、ProfileIterator{''} 和两个类 Wechat、WechatIterator。

Wechat 类要实现 SocialNetwork 接口,来实现创建微信迭代器的工厂方法,并储存好友列表、提供增加好友和获取好友的方法。

WeChatIterator 类要实现 ProfileIterator{''} 接口,来实现一些具体的微信迭代器方法,用来迭代好友列表。

// 声明用于生成迭代器的工厂方法
interface SocialNetwork {
createFriendsIterator(): ProfileIterator<string | null>;
}

// 声明迭代器接口
interface ProfileIterator<T> {
// 返回当前项,并向后移一位
next(): T;
// 返回当前项
current(): T;
}

// 实现生成迭代器的工厂方法,和一些 Wechat 类自有的方法
class Wechat implements SocialNetwork {
private friendList: string[] = [];

createFriendsIterator(): ProfileIterator<string | null> {
// 迭代器指向本实例
return new WeChatIterator(this);
}

public addFriend(friend: string) {
this.friendList.push(friend);
}

public getFriendList() {
return this.friendList;
}
}

// 实现具体的微信好友迭代器类
class WeChatIterator implements ProfileIterator<string | null> {
private wechat: Wechat; // 微信实例
private position = 0; // 记录当前迭代到的位置

constructor(weChat: Wechat) {
this.wechat = weChat;
}

// 返回当前项,并后移一位;若已经遍历完,没有数据了,返回 null
next(): string | null {
return this.wechat.getFriendList()[this.position++] || null;
}

current(): string {
return this.wechat.getFriendList()[this.position];
}
}

const wechat = new Wechat();
wechat.addFriend('Lee');
wechat.addFriend('Tom');
wechat.addFriend('Bella');

const iterator = wechat.createFriendsIterator();

let curr;
while ((curr = iterator.next())) {
console.log(curr);
}

输出:

Lee
Tom
Bella

外观模式 (Facade)

· 阅读需 3 分钟

外观模式,是结构型设计模式,为程序库、框架或者其他复杂类提供一个简单的接口。

  • 你可以让自己的代码独立于复杂子系统。
  • 外观可能成为与程序中所有类都耦合的上帝对象。

在现实生活中,就像电话订餐服务,你只需要给餐厅打电话,服务员会根据你的要求,进行下单、分派到后厨、记账、打包、派送,一系列复杂的操作是对客户隐藏的,客户不需要知道这么多,只需要告诉她你要吃什么。

示例

播放一个视频文件十分复杂,用到很多类,对文件解码、提取音频、计算比特率等等,在每个需要播放视频的地方,如果每个地方都用这么多代码,实在是太复杂了,代码也显得难看。比较好的办法,就是把播放功能的这些逻辑代码都写在一个外观类里,到的时候只需要用外观类就可以了。

class VideoFile {
constructor(path: string) {}
// ...
}
class BitrateCalc {
// ...
}
class AudioExtract {
constructor(path: VideoFile) {}
// ...
}
class MPEGDecoder {
constructor(path: VideoFile) {}
// ...
}

// 为了将框架的复杂性隐藏在一个简单接口背后,我们创建了一个外观类,它是在
// 功能性和简洁性之间做出的权衡
class VideoPlayer {
play(filePath: string) {
const file = new VideoFile(filePath);
console.log(new BitrateCalc());
new AudioExtract(file);
new MPEGDecoder(file);
// ...
// 一堆复杂的处理,播放视频文件
}
}

function ClientCode(filePath: string) {
// 只要调用外观类封装好的方法
new VideoPlayer().play(filePath);
}

装饰模式 (Decorator)

· 阅读需 4 分钟

装饰模式是一种结构型设计模式,通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

  • 无需创建新子类即可扩展对象的行为。
  • 可以在运行时添加或删除对象的功能。
  • 可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。可以将实现了许多不同行为的一个大类拆分为多个较小的类。

示例

写了一个数据读写的例子,定义一个 DataSource 接口规定读写操作。StringDataSource 类实现了该接口,可以读写字符串;定义装饰基类 DataSourceDecorator ,对将读写操作直接委派给被封装组件。

具体装饰类要继承装饰基类,然后扩展或改造读写方法,例如 EncryptionDecorator 类是加解密装饰类,基于装饰基类,改造了读写函数使之可以加密写入、解密读取。CompressDecorator 类同理,它封装了解压缩操作。

interface DataSource {
writeData(data: string): void;
readData(): string;
}

// 字符串内容读写类,实现了读写接口
class StringDataSource implements DataSource {
private content: string;

constructor(content?: string) {
this.content = content || '';
}

writeData(data: string): void {
this.content = data;
console.log(`Written ${data}.`);
}

readData(): string {
return this.content;
}
}

// 装饰基类
class DataSourceDecorator implements DataSource {
// 被封装的组件
protected source: DataSource;

constructor(dataSource: DataSource) {
this.source = dataSource;
}

// 装饰基类会直接将所有工作分派给被封装组件。具体装饰中则可以新增一些
// 额外的行为。
writeData(data: string): void {
this.source.writeData(data);
}
readData(): string {
return this.source.readData();
}
}

// 加解密装饰类,继承装饰基类,对读写数据函数进行了可加解密的改造
class EncryptionDecorator extends DataSourceDecorator {
writeData(data: string): void {
let chars = [];
for (let i = 0; i < data.length; i++) {
chars.push(String.fromCharCode(data.charCodeAt(i) * (i + 1)));
}
super.writeData(String.fromCharCode(6159) + chars.join(''));
}

readData(): string {
let str = super.readData();
if (str.startsWith(String.fromCharCode(6159))) {
let chars = [];
for (let i = 1; i < str.length; i++) {
chars.push(String.fromCharCode(str.charCodeAt(i) / i));
}
return chars.join('');
}
return str;
}
}

// 解压缩装饰类,继承装饰基类,对读写数据函数进行了可解压缩改造
class CompressDecorator extends DataSourceDecorator {
writeData(data: string): void {
super.writeData(`Compressed: ${data}`);
}
readData(): string {
return `Uncompressed: ${super.readData()}`;
}
}

// 字符串数据的读写
const strData = new StringDataSource('abcde');
strData.writeData('Hello'); // 写入
console.log(strData.readData()); // 读取

// 字符串数据的加解密读写,用加解密装饰对象包裹了字符串数据对象
const enc = new EncryptionDecorator(strData);
// 这里的加密写入,就是先加密,然后将加密好的内容委派由装饰基类 super.writeData() 处理,
// 基类该函数又将内容传给被封装的对象 source.writeData() 来处理
enc.writeData('Some secret.'); // 加密写入
console.log(enc.readData()); // 解密读取

输出:

Written Hello.
Hello

Written ᠏SÞŇƔ ʲ˃̘ЂϲӼȨ.
Some secret.

组合模式 (Composite)

· 阅读需 4 分钟

组合模式是一种结构型设计模式,它可以将对象组合成树状结构,并且能像使用独立对象一样使用它们。组合模式可以通过将同一抽象或接口类型的实例放入树状结构的行为方法来识别出。

  • 你可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则。无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分。
  • 对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,需要过度一般化组件接口,使其变得令人难以理解。

示例

Graphic 接口定义了一些图形应有的基础函数,绘制是否含有子项Dot 类和 Composite 类都实现该图形接口,Composite 类另外实现了添加和删除子项的函数,保存一个 children 数组,子项可以是简单的 Dot 对象,也可以是复杂的 Composite 对象,由此可以形成一个树状的结构。

客户端代码通过 Graphic 接口,来调用抽象的不具体的函数。

// 接口定义了图形应有的一些函数功能,画、有无子项
interface Graphic {
draw(): void;
isComposite(): boolean;
}

/**
* 该类代表了组合对象的末端,末端没有子项。
* 通常是该类完成实际的工作,组合对象只负责把工作委派给他们。
* 点类实现了图形接口,实现具体的画、有无子项函数
*/
class Dot implements Graphic {
protected x = 0;
protected y = 0;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
public draw(): void {
console.log('.', this.x, this.y);
}

// 可提供这个方法,让客户端代码确定某组件是否可以有子组件
public isComposite(): boolean {
return false;
}
}

/**
* 组合类实现了图形接口,它可以添加删除子项(简单项、复杂组合)
* 它把实际工作委派给子项,然后再收集汇总他们的结果
*/
class Composite implements Graphic {
private children: Graphic[] = [];

public add(graphic: Graphic) {
this.children.push(graphic);
}
public remove(graphic: Graphic) {
this.children.splice(this.children.indexOf(graphic), 1);
}
public isComposite() {
return true;
}
// 递归遍历子项,收集汇总它们的结果
public getChildren() {
const arr: any[] = [];
this.children.forEach((v) => {
if (v instanceof Composite) {
arr.push(v.getChildren());
} else {
arr.push(v);
}
});
return arr;
}
// 将子项所有结果收集汇总,并把最终结果输出
draw(): void {
console.log(this.getChildren());
}
}

// 客户端代码,通过基础接口,就可以与所有的简单项、复杂组合进行交互
function ClientCode(graphic: Graphic) {
graphic.draw();
}

const composite = new Composite();
const dot1 = new Dot(1, 1);
const dot2 = new Dot(2, 2);
const comp1 = new Composite();
comp1.add(dot1);
composite.add(comp1);
composite.add(dot2);

// [ [ Dot { x: 1, y: 1 } ], Dot { x: 2, y: 2 } ]
ClientCode(composite);