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 条评论
登录 后参与评论

相关文章

来自专栏james大数据架构

jQuery打造智能提示插件

插件根据实际需要在单功能上封装的,实现传入后台数据地址,要保存值的input,前台要传入的参数(过滤条件),来返回下拉提示数据,数据过多可上下滚动选择,选择后显...

17710
来自专栏Flutter入门

Weex是如何在Android客户端上跑起来的

Weex可以通过自己设计的DSL,书写.we文件或者.vue文件来开发界面,整个页面书写分成了3段,template、style、script,借鉴了成熟的MV...

2084
来自专栏葬爱家族

Mvvm、RxJava、Retrofit 三剑合璧

说起现在Android流行的app架构,脱口而出MVP、MVVM,要问两者区别,张口就来,balabalabala。。但是公司所有项目用的都是MVP,从没正式用...

1432
来自专栏码生

MessageHandler 高级用法二:原生调用JS 实现回调

在 上一篇中 我们实现了原生和JS 的方法调用,这篇解决一下在 APP 中调用JS方法时怎么含有 JS 的回调

604
来自专栏向治洪

ReactNative调用Android原生模块

有时候App需要访问平台API,但React Native可能还没有相应的模块包装;或者你需要复用一些Java代码,而不是用Javascript重新实现一遍;又...

1917
来自专栏developerHaoz 的安卓之旅

Android 基于 glide 4.0 封装图片加载库

很多时候只要简单的调用一行代码实现图片的高性能加载(对于 glide 这个框架不熟悉的同学,可以看一下我这篇文章 glide 一个强大的图片加载框架 ),使用起...

1132
来自专栏大内老A

ASP.NET MVC的Model元数据与Model模板:将”ListControl”引入ASP.NET MVC

我们不仅可以创建相应的模板来根据Model元数据控制种类型的数据在UI界面上的呈现方法,还可以通过一些扩展来控制Model元数据本身。在某些情况下通过这两者的结...

2896
来自专栏林冠宏的技术文章

浅谈 Glide - BitmapPool 的存储时机 & 解答 ViewTarget 在同一View显示不同的图片时,总用同一个 Bitmap 引用的原因

作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:htt...

36610
来自专栏非著名程序员

Dagger2图文完全教程

? 没有更多开场白,直接说下我对它的理解。 Dagger2 是一个Android依赖注入框架。而android开发当前非常流行的非MVP模式莫属了,Dagge...

3678
来自专栏代码GG之家

Dagger2图文完全教程

没有更多开场白,直接说下我对它的理解。 Dagger2 是一个Android依赖注入框架。而android开发当前非常流行的非MVP模式莫属了,Dagger2的...

1919

扫码关注云+社区