TypeScript设计模式之组合、享元

看看用TypeScript怎样实现常见的设计模式,顺便复习一下。 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想记住就好。 这里尽量用原创的,实际中能碰到的例子来说明模式的特点和用处。

组合模式 Composite

特点:以树的形式展示对象的组合,并且可以以类似的方式处理每个枝点。

用处:当对象组合以树状存在,有父有子,并且对象的行为差不多时可以考虑组合模式,如菜单,游戏里的技能树。

注意:遍历组合的性能要求。

下面用TypeScript简单实现一下组合模式: 技能树麻烦了点,技能激活要引入观察者模式,就以菜单为例吧。 菜单可以包括子菜单,点击菜单项时有子菜单则显示子菜单,没有时触发点击事件。

先声明一个抽象,包含菜单的名字,点击事件和添加Child,一般情况下Menu会维护一个childs集合,通过这个集合来添加子菜单,不过这里没有用这种方式,采用的是继承一个集合来让本身拥有集合的能力,这样更方便,是父还是子可以通过字段来控制,也可以通过是否有child来表示。

abstract class MenuBase extends Array<MenuBase>{

    name: string;

    abstract click();

    addChild(...childs: Array<MenuBase>){
        childs.forEach(o=>this.push(o));
    }
}

实现具体MenuItem类,一般情况下可以用两个类,一个代表菜单,一个代表菜单项,不过这里就不需要区别是枝还是叶了,简单一点,只用一个MenuItem代表所有。

可以传click处理事件进来,click时会触发click事件,另外如果有子则显示所有子。

class MenuItem extends MenuBase{
    constructor(public name: string, private clickFunc: () => string = undefined){
        super();
    }

    click(){
        console.log(`click ${this.name}`);

        if(this.clickFunc){
            console.log(this.clickFunc());
        } 

        if(this.length > 0){
            let childs = this.reduce((p, c)=><MenuBase>{name:`${p.name},${c.name}`}).name;
            console.log(`${this.name}'s childs: ${childs}`);
        }
    }
}

现在来运行一下:

let root = new MenuItem('root');

let A1 = new MenuItem('A1');
let A2 = new MenuItem('A2', ()=>{return 'my name is A2'});

let B1 = new MenuItem('B1');
let B2 = new MenuItem('B2', ()=>{return 'my name is B2'});
let B3 = new MenuItem('B3', ()=>{return 'my name is B3'});

root.push(A1, A2);

A1.push(B1, B2, B3);

root.click();
A1.click();
A2.click();
B1.click();

结果:

click root
root's childs: A1,A2

click A1
A1's childs: B1,B2,B3

click A2
my name is A2

click B1

符合预期行为,这种组合就是非常简单的,但如果组合得非常深且枝非常多时就需要考虑查找枝时的效率问题了,通常的办法是采用缓存来把一些常用的查找结果缓存起来,避免频繁遍历。

享元模式 FlyWeight

特点:通过缓存来实现大量细粒度的对象的复用,从而提升性能。

用处:当有很多类似的对象要频繁创建、销毁时可以考虑享元模式,如线程池。

注意:对象的内部状态和外部状态。

下面用TypeScript简单实现一下享元模式:

假设一个画图场景,里面有很多图元,如圆,矩形等,由于量非常多,且经常刷新,每次刷新new出一批图元,浪费内存是小事,临时小对象太多导致频繁GC才是麻烦事,UI会表现出卡顿。 现在用享元模式来解决这个问题,先定义一个图形接口,可以画图,可以被删掉,同时有个isHidden状态来控制该图元是否显示,删除也就是把isHidden设为true,并不真的删除。 name是个标记,方便看记录信息。

interface Element{
    isHidden: boolean;
    name: string;
    draw();
    remove();
}

实现两个图元:圆和矩形,draw只会画不是hidden的,remove则是把isHidden设为true

class Circle implements Element{
    isHidden: boolean = false;

    constructor(public name: string){

    }

    draw(){
        if(!this.isHidden){
            console.log(`draw circle: ${this.name}`);
        }
    }

    remove(){
        this.isHidden =  true;
        console.log(`remove circle: ${this.name}`)
    }
}

class Rectangle implements Element{
    isHidden: boolean = false;

    constructor(public name: string){

    }

    draw(){
        if(!this.isHidden){
            console.log(`draw rectangle: ${this.name}`);
        }
    }

    remove(){
        this.isHidden =  true;
        console.log(`remove rectangle: ${this.name}`)
    }
}

下面建个图元工厂来创建、维护图元,工厂里维护了一个图元池,包括圆和矩形,工厂除了创建图元外,还可以删掉所有图元,得到图元的数量(可见或不可见的)

class GraphFactory{
    static readonly instance: GraphFactory = new GraphFactory();

    private graphPool: {[key: string]: Array<Element>} = {}; // 图元池
    
    getCircle(newName: string): Circle{
        return <Circle>this.getElement(newName, Rectangle);
    }

    getRectangle(newName: string): Rectangle{
        return <Rectangle>this.getElement(newName, Rectangle);
    }

    getCount(isHidden: boolean){  // 图元数据
        let count = 0;
        for(let item in this.graphPool){
            count += this.graphPool[item].filter(o=>o.isHidden == isHidden).length;
        }
        return count;
    }

    removeAll(){ //删除所有
        console.log('remove all');
        for(let item in this.graphPool){
            this.graphPool[item].forEach(o=>o.isHidden = true);
        }
    }

    //获取图元,如果池子里有hidden的图元,就拿出来复用,没有就new一个并Push到池子里
    private getElement(newName: string, eleConstructor: typeof Circle | typeof Rectangle): Element{ 
        if(this.graphPool[eleConstructor.name]){
            let element = this.graphPool[eleConstructor.name].find(o=>o.isHidden);
            if(element){
                element.isHidden = false;
                element.name = newName;
                return element;
            }
        }
        let element = new eleConstructor(newName);
        this.graphPool[eleConstructor.name] = this.graphPool[eleConstructor.name] || [];
        this.graphPool[eleConstructor.name].push(element);
        return element;
    }
}

代码写完了,分别拿5个圆和5个矩形测试:

  1. 先从工厂里分别创建出来并调用draw
  2. 打印出当前可见图元数量
  3. 移除所有
  4. 分别打印可见和不可见图元数量
  5. 从工厂里再次分别创建出5个并调用draw
  6. 再次分别打印可见和不可见图元数量
const num = 5;
console.log('create elements');

for(let i = 1;i<=num;i++){
    let circle = GraphFactory.instance.getCircle(`circle ${i}`);
    let rectangle = GraphFactory.instance.getRectangle(`rectangle ${i}`);

    circle.draw();
    rectangle.draw();
}

console.log(`element count: ${GraphFactory.instance.getCount(false)}`);

GraphFactory.instance.removeAll();

console.log(`visible element count: ${GraphFactory.instance.getCount(false)}`);
console.log(`hidden element count: ${GraphFactory.instance.getCount(true)}`);

console.log('create elements again');

for(let i = 1;i<=num;i++){
    let circle = GraphFactory.instance.getCircle(`new circle ${i}`);
    let rectangle = GraphFactory.instance.getRectangle(`new rectangle ${i}`);

    circle.draw();
    rectangle.draw();
}

console.log(`visible element count: ${GraphFactory.instance.getCount(false)}`);
console.log(`hidden element count: ${GraphFactory.instance.getCount(true)}`);

结果:

create elements

draw rectangle: circle 1
draw rectangle: rectangle 1
draw rectangle: circle 2
draw rectangle: rectangle 2
draw rectangle: circle 3
draw rectangle: rectangle 3
draw rectangle: circle 4
draw rectangle: rectangle 4
draw rectangle: circle 5
draw rectangle: rectangle 5

element count: 10

remove all

visible element count: 0
hidden element count: 10

create elements again

draw rectangle: new circle 1
draw rectangle: new rectangle 1
draw rectangle: new circle 2
draw rectangle: new rectangle 2
draw rectangle: new circle 3
draw rectangle: new rectangle 3
draw rectangle: new circle 4
draw rectangle: new rectangle 4
draw rectangle: new circle 5
draw rectangle: new rectangle 5

visible element count: 10
hidden element count: 0

可以看到remove all后图元全部hidden,再次创建时并不new出新的,而是从池子里拿出hidden的来复用,也就是利用享元模式,小对象的数量就可以限制在单次显示最多的那次的数量,少于这个数量的都会从池子里拿,小对象也没有频繁创建销毁,对内存,对GC都是有利的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏偏前端工程师的驿站

WebComponent魔法堂:深究Custom Element 之 标准构建

前言  通过《WebComponent魔法堂:深究Custom Element 之 面向痛点编程》,我们明白到其实Custom Element并不是什么新东西,...

17210
来自专栏AhDung

【SQL】小心字符串拼接导致长度爆表

别想当然以为它会返回8002,而是8000,select @max也只会得到8000个a,后面两个b没了。我们知道,varchar(max)类型不受字符数限制,...

643
来自专栏三流程序员的挣扎

Flutter 学习记3 - Widget 框架

通过 widgets 构建 UI,描述当前的配置和状态,当状态改变时,框架找出前后的变化,以确定底层 Render Tree 要做的最小更改,在内部变成另一个状...

541
来自专栏编舟记

Idris 关于环境和数据类型

emacs 打开任何以*.idr和*.lidr作为后缀的文件,都可以启用idris-mode. 另外,使用C-c C-l可以在*idris-repl*中加载当...

554
来自专栏pangguoming

不得不知的ES6十大特性

ES6(ECMAScript2015)的出现,无疑给前端开发人员带来了新的惊喜,它包含了一些很棒的新特性,可以更加方便的实现很多复杂的操作,提高开发人员的效率。...

3424
来自专栏光变

你所不知道的Java之Switch

??? Enum,String,Character,Byte,Short,Integer

1190
来自专栏娱乐心理测试

ios 几种容易忽略的内存泄漏方式

一、从AFNetWorking说起 对于AFNetWorking的使用我们通常会对通用参数、网址环境切换、网络状态监测、请求错误信息等进行封装。在封装网络请求...

402
来自专栏coding

Linux笔记5.展开与引用

1014
来自专栏進无尽的文章

UI篇-VC的生命周期以及UIView的layoutSubviews和drawRect方法

看似常用的UIView,其实有很多不经常用到的方法和应该注意的机制,我是一个喜欢打破砂锅问到底的人,可是很多问题在网上搜索不到答案,大部分的博客都是转载相同的东...

703
来自专栏沃趣科技

MySQL的一个表最多可以有多少个字段

问题由来 引用我们客户的原话: *创建如下表,提示我:* ? *如果我将下面表中的varchar(200),修改成text(或blob):报错变为另一个:* ?...

5049

扫码关注云+社区