浅学设计模式

1,465 阅读17分钟

学习的时候就在想,为什么叫设计模式?设计的意思是“把一种设想通过合理的规划、周密的计划、通过各种方式表达出来的过程”;模式的意思是“是理论和实践之间的中介环节,具有一般性、简单性、重复性、结构性、稳定性、可操作性的特征;是事物的标准样式”。那么设计模式是不是可以理解为将一个解决问题的过程转变为一个解决问题的方案模板。学习设计模式就相当于在学习前辈们解决某类问题的方法,就是站在巨人的肩膀上前行~。

本文主要涉及以下几种设计模式:

  1. 工厂模式
  2. 单例模式
  3. 观察者模式
  4. 迭代器模式
  5. 原型模式
  6. 装饰器模式
  7. 代理模式
  8. 策略模式

工厂模式

什么是工厂模式?从名字来看就能略知一二。应该是类似于工厂那种,能够批量的生产出对象,且这些对象都是具有某些相似之处的。

比如一个工厂能够生产出车子,这些车子都具有四个轮子,一个发动机。工厂不会考虑这些轮子和发动机到底是什么牌子,什么颜色等具体信息,它只是负责生产。这些具体的信息得由需要工厂生产车子的人来提供。需要什么样的轮子和发动机,提供具体消息给工厂,工厂就能给你造出来。这就是工厂模式。不考虑具体信息(轮子的颜色和牌子),只关注抽象信息(是轮子就行)。

使用工厂模式的好处就是实现代码复用。一个工厂能够生产出多种车子,只要符合了四个轮子一个发动机的标准就行。

基本的代码样式如下`

//  人的抽象信息
interface IPerson {
  name: string;
  eat: () => void;
  speak: () => void;
};

class Teacher implements IPerson {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat() {
    console.log(`${this.name} eat something`);
  }
  speak() {
    console.log(`${this.name} speak something`);
  }
}

class Student implements IPerson {
  name: string;
  constructor(name:string) {
    this.name = name;
  }
  eat() {
    console.log(`${this.name} eat something`);
  }
  speak() {
    console.log(`${this.name} speak something`);
  }
}

// 返回接口对象
class CreatePerson {
    create(type: number, name: string): IPerson {
    // 逻辑判断 封装
      if (type === 1) return new Teacher(name);
      else if (type === 2) return new Student(name);
      else throw new Error('没有该角色!');
    }
};

const creator = new CreatePerson(); // 构造器
const student = creator.create(2, '小红');
const teacher = creator.create(1, '李老师');

student.eat();
teacher.eat();

这里有老师和学生两个对象,都是通过构造器来构建的。哪里需要对象,就可以在哪里使用构造器来构造。节约时间和代码量,同时也可以方便的进行扩充,扩充时实现这个接口就能拿来使用。

工厂模式的使用场景:简单模拟Jquery使用以及vue的render函数

先说第一个,模拟Jquery的使用。对于Jq应该都不陌生,或多或少都知道,真的很溜。这里简单模仿一下它的使用,看看到底哪里用到了工厂模式。

// window对象扩展 $ 属性
declare interface Window {
  $: (selector: string) => JQuery;
}


// JQ对象
class JQuery {
  selector: string;
  length: number;
  constructor(selector: string) {
    const domList = Array.prototype.slice.call(document.querySelectorAll(selector));
    const length = domList.length;
    for (let i = 0; i < length; i++) {
      this[i] = domList[i];
    }

    this.selector = selector;
    this.length = length;
  }
}

// 工厂函数  里面可以做一些逻辑判断
const $ = (selector) => {
  return new JQuery(selector);
};

// 将jQ挂载到window.$上
window.$ = $;

// 输出结果
console.log($('span'))

image.png 代码写完后,用 tsc factory.ts将.ts文件转为.js 文件放在html中执行。可以看到已经打印出了JQuery对象。使用时就可以随时随地的使用$就行,不必再重新创建JQuery对象了。

第二个是vue中的render函数,这个可以直接看vue提供的模板转换(template-explorer.vuejs.org/)。 image.png 左边span标签和p标签在右边都是由_createElementVNode函数来构造,类似于creator.create(2, '小红');,传递参数就能直接返回出一个VDom,再由diff算法变成真正的DOM,最后页面更新。

单例模式

单例单例,就是单个,独一无二的存在。即一个对象/实例只能被创建一次,创建出来之后被缓存,在各个地方继续使用。在单个系统中是唯一的,不能再被重复创建出来。

看到单例模式的时候,第一反应就是这不就是vuex吗。vuex中的属性是全局使用的,且唯一。没深入学习过vuex,但vuex应该是采用了单例模式(有待考证)。这个单例模式怎么理解呢,比如有一个登录弹窗,然后在一个复杂的商城系统中,用户在很多个页面中都需要登录操作。问题就在这里,如果是正常去写代码,那么需要在每个需要登录功能的页面中都写一个登录弹窗,这是非常耗时耗力的。那么采用了单例模式,就只需要写一个登录弹窗,然后在每个需要的页面中直接调用即可,简单方便。如果再需要验证用户角色和权限什么的,也可以在登录弹窗功能里进行封装。

既然是单例模式,那么怎么才能做到单例呢,如何限制住使用者只能创建一个实例呢?可以使用private + static的组合方式来控制,以下代码简单演示了如何做到这一点

class Person {
  name: string;
  // static 属性 静态属性
  private static instance: Person | null;
  // private - 外部无法初始化
  private constructor(name: string) {
    this.name = name;
  }

  // static 方法
    static getInstance(name: string): Person {
      // 判断  如果为null 则创建实例 否则直接返回原有实例
      if (Person.instance == null) {
      Person.instance = new Person(name);
    }
    return Person.instance;
  }
}

// const p1 = new Person('张三') // 报错
// const p1 = Person.instance // 报错

// 只有通过调用getInstance方法才能创建和获取实例,且多次创建的是同一个对象
const p1 = Person.getInstance('张三')
const p2 = Person.getInstance('张三')
console.log(p1);
console.log(p2);
console.log(p1 === p2);

image.png 可以看到p1和p2是全等的,说明是同一个实例。利用pravite + static的组合方法就能轻松的创建出单例,保证这个实例是全局唯一的。

那怎么使用单例模式呢,还是回到开始那个登录框的例子,这里模拟使用登录框的代码

// 只暴露给外部 hide 和 show 方法
class LoginForm {
  private state: string = 'hide'; //默认为hide
    private constructor() {
      // 初始化操作
  }

  private static instance: LoginForm | null = null;
  static getInstance(): LoginForm {
      if (this.instance == null) {
          this.instance = new LoginForm();
      }
    return this.instance;
  }

  hide() {
    if (this.state === 'hide') {
      console.log('已经是隐藏了');
      return;
    }
    // .......执行一些操作
    this.state = 'show';
  }

  show() {
    if (this.state === 'show') {
      console.log('已经是显示了');
      return;
    }
    // .......执行一些操作
    this.state = 'hide';
  }
}

const login1 = LoginForm.getInstance();
const login2 = LoginForm.getInstance();
console.log(login1);
console.log(login2);
console.log(login1 === login2);

image.png 可以看到两次获取的登录实例都是同一个,可以放心使用了。感觉单例模式和工厂模式有些类似,都是返回一个实例。区别就是工厂模式能返回按需定制的对象,而单例模式只能返回统一的实例,其他的都没啥区别。

观察者模式

观察者模式应该是前端来说用的最多的模式了,那么啥叫观察者模式呢?从名字来理解就是好比一个侦察兵,随时观察远处的动向,一有风吹草动就做出报告。就像下面这个土拨鼠一样

R-C.gif

但其实并不是这么理解的。如果是像侦察兵一样一直盯着,那就会消耗他的时间和精力,导致他无法对其他事情做出反应,比如身边爬过一只老鼠可能都没法发现。观察者模式应该是我订阅了一份报纸,那么在报纸送到之前,我都可以安心的做其他事情,直到送报人把报纸扔到门口,这时候我才去拿报纸。可能有点难以理解,明明没有观察,只是在等待,为什么要叫观察者模式呢?完全可以叫订阅模式,订阅了之后不用管了,直到送货上门之后再去拿,无需一直等待。

不过,难理解归难理解。这个模式确实是前端来说非常常见的,DOM事件,还有各种异步回调函数都是这个模式,点击事件,定时器啥的都是。那么这个模式到底咋实现呢

// 主题 即被观察者
class Subject {
    private state: number = 0 // 初始状态
    private observers: Observer[] = [] // 注册观察者 可以有多个

    // 获取属性
    getState(): number {
        return this.state
    }

    // 设置属性
    setState(state: number) {
        this.state = state // 当state修改后
        this.notify() // 通知观察者
    }

    // 添加观察者
    attach(observer: Observer) {
        this.observers.push(observer)
    }

    // 进行通知
    private notify() {
        this.observers.forEach(observer => {
            observer.update(this.state)
        })
    }
}

// 观察者
class Observer{
   private name: string  // 名字
    constructor(name:string) {
        this.name = name
    }

    // 当接收到通知后触发
    update(state: number) {
        console.log(`${this.name} update ${state}`)
    }
}

const sub = new Subject(); // 被观察者

// 两个观察者
const observer1 = new Observer('张三'); 
const observer2 = new Observer('李四');

// 订阅 被观察者
sub.attach(observer1);
sub.attach(observer2);

sub.setState(100); // 状态更新,发布通知

image.png

整个事件就是,有两个观察者订阅了一个主题(被观察者),然后这个主题状态更新之后,发出通知,通知观察者它的状态已经更新,让这些观察者做出反应(简单明了)。需要注意的一点就是要在状态更新事件里及时触发通知事件。

与观察者模式相提并论的还有一个发布订阅模式,二者神似,差别如下图。我的理解就是观察者模式是不能自己主动去触发,而发布订阅模式是可以自己去触发。还有就是观察者模式中subject和observer是直接绑定的,无媒介,在同一个组件内。而发布订阅模式中publisher和observer互不认识,比如publisher在a组件,而observer在b组件,但是二者仍能相互通讯。

image.png

发布订阅模式很典型的场景就是自定义事件。在a组件中发布了点击事件,那么在b组件中可以通过事件总线来主动触发这个点击事件。比如下面这个简单版自定义事件

    1. on 多次绑定,即可以多次执行,直到被off掉
    2. once  一次绑定,即只能执行一次便自动off掉,或未执行也可以被off掉
    3. emit 触发事件,且能够传入参数
    4. off 取消事件
    
class EventBus {
    // 初始化
    constructor(){
        this.bus = {}
    }
    
    on(type, fn, isOnce = false) {
        const bus = this.bus
        if(bus[type] == null) bus[type] = [] // 如果该类型不存在,则定义
        bus[type].push({ fn, isOnce }) // 将该事件注册到该类型中
    }
    once(type,fn) {
        // 做法类似与on,所以直接调用即可,isOnce是用来区分的
        this.on(type,fn,true)
    }
    off(type, fn) {
        const fnList = this.bus[type] // 拿到该类型下的所有事件
        if (fnList == null || !fnList.length) return; // 当该类型没有事件或没有该类型时直接返回
        if(!fn) {
            // 没有传入fn,则off掉该类型的所有注册事件
            this.bus[type] = []
        } else {
            this.bus[type] = this.bus[type].filter(item => item.fn !== fn)
        }
    }
    emit(type,...arg){
        const fnList = this.bus[type] // 拿到该类型下的所有事件
        if (fnList == null || !fnList.length) return; // 当该类型没有事件或没有该类型时直接返回
        
        this.bus[type] = fnList.filter(item => {
            // 执行事件
            const {fn,isOnce} = item
            fn(...arg)
            
            // 用来区分是否为once,如果是once,则执行完后过滤掉,不返回
            if(!isOnce) return true
            return false
        })
    }
}

const fn1 = (a,b,c) => console.log(`1 ${a} ${b} ${c}`)
const bus = new EventBus()
bus.on('key', fn1)
bus.emit('key', 'a','b','c')

image.png

上面这个例子,在a组件中发布了fn1事件,类型为key。那么在b组件中就能主动触发这个事件,不需要被动等待。还想到一个使用场景就是postMessage通讯,比如WebWorker中主线程和子线程之间的通讯也是类似的。在vue2中自带EventBus总线机制也是这个模式。但是vue3好像去除了,不过引入mitt这个轻量级库也能起到一样的作用。

迭代器模式

这个模式是啥呢,就是遍历有序结构的一种方式,研究的是如何做到更好更方便的去遍历一个有序结构。说白了就是多使用for-of,还有forEach,map,filter,reduce,还有数组结构,拓展操作符这些方法去遍历有序结构。至于为什么有序结构能够被遍历呢,是因为这些结构中内置了一个Symbol.iterator属性,它的属性值是一个函数。执行该函数会返回 iterator 迭代器,有 next() 方法,执行返回 { value, done } 结构。就像下面这样

const arr = [10,20,30]
const iterator = arr[Symbol.iterator]()

console.log(iterator.next() )// {value: 10, done: false}
console.log(iterator.next())// {value: 20, done: false}
console.log(iterator.next())// {value: 30, done: false}
console.log(iterator.next()) // {value: undefined, done: true}

image.png

看着这个输出结果,感觉有点神奇,原来数组还能这么玩。不光是数组,其他的有序结构都能这么玩。那有序结构都有哪些呢?下面这些都是

  • 字符串
  • 数组
  • NodeList 等 DOM 集合
  • Map
  • Set
  • arguments (函数参数)

那么这个迭代器模式还有啥呢,没了,下一个。

原型模式

说起原型,想到我当初抱着书啃原型那会,那时候被prototype和__proto__给绕晕了,经常想不起谁是谁的谁,谁又和谁相等。学习原型模式的时候又给复习了一遍,稍稍加深了一点印象。

原型模式的定义是创建对象的一种模式,我们不需要知道这个对象的具体类型,而是直接找到一个对象,通过克隆来创建一个一模一样的对象。说白了用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。这个新对象可以进行扩充,也可以修改原有功能。

关于原型模式涉及到的知识点主要有原型链和对象属性描述符。把这俩整明白了,原型模式也就能理解了。

要说原型链还得先知道原型的几个知识

  • 函数和class都有显示原型prototype(就叫prototype)
  • 对象都有隐式原型__proto__(就叫__proto__)
  • 对象的__proto__指向其构造函数的prototype

看下面这段代码

function Foo(name,age) {
    this.name = name
    this.age = age
}

Foo.prototype.getName = function () {
    return this.name
}

Foo.prototype.sayHi = function () {
    alert('hi')
}


const obj = {} // 相当于 new Object()
console.log(obj.__proto__ === Object.prototype) // true

const arr = [] // 相当于 new Array()
console.log(arr.__proto__ === Array.prototype) // true

const f1 = new Foo('张三',20)
console.log(f1.__proto__ === Foo.prototype) // true 
const f2 = new Foo('李四',21)
console.log(f2.__proto__ === Foo.prototype) // true

可以看出,无论是内置函数还是自定义函数,都有一个prototype。而其构造出来的对象都有一个__proto__。并且对象.__proto__是等于构造函数.prototype。那么问题来了,prototype也是一个对象,那它有__proto__吗?有的,console.log(Foo.prototype.__proto__ === Object.prototype); // true。往下还有吗?有的,console.log(Object.prototype.__proto__ === null);。往下还有吗?没了,到了链路的终点了,终点就是null(空)。这样一条一条的链接关系就叫原型链。引用之前看的一篇博客中的图,感觉画的是最形象的

850375-20190708153139577-2105652554.png

那么啥叫属性描述符呢?如其名,就是描述属性特征的字符,有value,configurable,writable和enumerable。当然还有set方法和get方法。下面逐个介绍一下:

  1. value,属性值为值类型、引用类型、函数等。必须要有,如果没有的话,那么输出这个对象时候就看不到这个属性,但这个属性又真实存在。比如下面这个
const obj = {}
let x = 100
Object.defineProperty(obj,'x',{
    get() {
        return x
    },
    set(newValue) {
        x = newValue
    }
})
console.log(obj) // {}
console.log(obj.x) // 100

image.png

可以看到,打印obj对象时是个空的,但是打印obj.x时又有值100。感觉有点神奇,薛定谔的属性是吧。感觉这种方法可以用在三方库上,用来保护数据。

  1. configurable,这个属性符很厉害,可以直接决定这个属性整体能不能被修改。如果设置为false则这个属性相当于被封印了,任何修改都不起作用,还会报错,且还不能改回true。
const obj = { x: 100 }
Object.defineProperty(obj,'y',{
    value: 200,
    configurable: false,  // false
})
Object.defineProperty(obj,'z',{
    value: 300,
    configurable: true,
})

delete obj.y  // 不成功

// 重修修改 y 报错(而修改 z 就不报错)
Object.defineProperty(obj,'y',{
    value: 210
})

image.png

  1. writable,属性是否可以被修改。当设置为false的时候,即使修改了也是不会起作用的
const obj = { x: 100 }
Object.defineProperty(obj,'x',{
    writable: false,
})
obj.x = 101
obj.x // 依然是 100

console.log(obj.x);

image.png

另外,这里还有两个与之相关的方法,Object.freeze()Object.seal()

Object.freeze() 冻结对象:1. 现有属性值不可修改;2. 不可添加新属性;

Object.freeze(obj) // 冻结属性
obj.x = 101 // 修改x值
obj.x // 100 
obj.z = 300 // 添加新属性
console.log(obj);

image.png

Object.seal() 密封对象,1. 现有属性值可以修改;2. 不可添加新属性;就是在freeze方法上弱化了一点。

const obj = { x: 100,y: 200 }
Object.seal(obj) // 冻结属性
obj.x = 101 // 修改x值
obj.x // 100 
obj.z = 300 // 添加新属性
console.log(obj);

image.png

这里有一个用处就是在 Vue 中,如果 data 中有比较大的对象,且不需要响应式,则可以使用 Object.freeze() 冻结。比如在海量数据滚动展示,不会发生变动,那就可以使用该方法来提升渲染速度(有待尝试,看见有博文中试过)。

  1. enumerable,就是判断该属性能否通过fon-in来遍历。为false就不能被遍历出来,反之则能。
const obj = { x: 100 }
Object.defineProperty(obj,'y',{
    value: 200,
    enumerable: false,
})
Object.defineProperty(obj,'z',{
    value: 300,
    enumerable: true,
})

for (const i in obj) {
    console.log(i) ;
}

image.png

  1. 当使用get和set方法时,不需要value属性符。
const obj = { x: 100 }
Object.defineProperty(obj,'y',{
    get() { return y },
    set(newVal) {y = newVal}
})

obj.y = 200
console.log(obj);

console.log(Object.getOwnPropertyDescriptor(obj,'y'))

image.png

看打印结果,obj中没有y属性,但是又能打印出来y的属性值,有点类似于把y的value属性符设置为了false。再看y的所有属性符,其中并没有value和writable,这是被get和st方法取代了。

装饰器模式

装饰器模式就是针对一个对象,动态的添加新功能,但是不改变它原有的功能。就是手机加了个手机壳一样,变好看了,也多了个手环,但是没有影响手机原有的功能。像下面这个简单代码一样:

function  decorator(phone) {
    phone.fn2 = () => {
        console.log('手机壳');
    }
}

const phone = {
    name: '手机',
    fn1 () {}
}

decorator(phone)

phone.fn2()

当然,这个具体实现肯定不是这么简单的。首先,你就得解决使用问。如果直接使用这个模式,指定会给你个红叉叉,说这个模式未来可能会变动,不能直接使用。就这问题花了我近一个小时,当然还是给我解决了。可以参考这两篇博文(编译报错解决typescript使用装饰器报错)。

那咋使用呢?这个装饰器模式可以接收三个参数,分别是targetkeydescriptor。很好理解,就是对象,对象属性和对象属性描述符。举个实际使用的例子,就是想把一个对象的属性改成只读,除了直接使用Object.defineProperty,也可以使用这个模式,下面这个代码就是:

function readOnly(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log(descriptor);
  descriptor.writable = false
  console.log(descriptor);
}

class Foo {
  private name:string
  constructor(name: string) {
  this.name = name
}

  @readOnly
  getName() {
    return this.name
  }
}

image.png

至于装饰器模式的使用场景,目前了解的还不太清楚,暂时略过。

代理模式

就是针对这个对象设置代理,拦截所有对这个对象的访问,只能通过代理去访问这个对象。就是类似于房产中介一样,用户想买房子可以通过中介来买,而不是直接去找到房主。同样,房主卖房子也是放在中介那里去卖。有了中介作为第三方,可以确保双方交易的安全、有效性。

那代理模式有哪些使用场景呢?我使用过的就有几个,比如DOM事件代理,webpack devserver proxy和Nginx反向代理。老实说,后面两个好难处理,错都不知道错哪里了。先说第一个DOM事件代理,这个比较简单。就是将事件绑定到父容器上,让父容器去匹配各个目标节点。这种方式很适合目标较多或者数量不一定的列表方式(如无线加载的瀑布流图片列表),代码演示如下:

    <div id="div1">
        <a href="#">a1</a>
        <a href="#">a2</a>
        <a href="#">a3</a>
        <a href="#">a4</a>
    </div>
    
    <script>
        var div1 = document.getElementById('div1')
        div1.addEventListener('click',function (e) {
            var target = e.target
            if (e.target.nodeName === 'A') {
                console.log(target.innerHTML)
            }
        })
    </script>

image.png

很简单的一段代码,在父级容器上监听一个点击事件,然后在这个事件中处理要判断的标签,就不必在每个目标节点上都绑定一个事件了。

然后就是webpack devserver proxy和Nginx反向代理了。这里贴出我之前写的代码,搞定这些代理可是花了我不少时间。但是,其中的一些用法我也只是在需要的时候再去官网上查看,所以知道的也不多。

image.png

image.png

当然,代理肯定不止这些东西,还有Proxy的使用。举个明星和经纪人的例子。当想要和明星合作时,一般都是找的经纪人;然后想要明星的手机号码时,一般也不会直接给出去,都是给的经纪人的手机号码。那么在代码中,这种代理的方式怎么实现呢

// 明星
const star = {
  name: '李小龙',
  age: 25,
  phone: '123456789',
  price: 0, // 明星不谈钱
};

// 经纪人
const agent = new Proxy(star, {
  get(target, key) {
    if (key === 'phone') {
      return '10086'; // 返回经纪人的的电话
    }
    if (key === 'price') {
      return 100 * 1000; // 报价
    }
    return Reflect.get(target, key); // 返回原来的属性值
  },
  set(target, key, val): boolean {
    if (key === 'price') {
      if (val < 100 * 1000) {
        throw new Error('价格太低了...');
      } else {
        console.log('报价成功,合作愉快!', val);
        return Reflect.set(target, key, val);
      }
    }
    // 其他属性不可设置
    return false;
  },
});

// 主办方
console.log(agent.name);
console.log(agent.age);
console.log(agent.phone);
console.log(agent.price);
agent.price = 190000 // 会自动打印 报价成功,合作愉快

image.png

可以看到,经纪人的姓名和年龄都是明星的,但是手机号码和报价却是被经纪人所控制。然后注意到,当给出报价后,不需要手动执行什么函数就会立即判断出是否能合作。这就是代理,在中间劫持做出判断,并立即给出相应的操作。无论执行什么操作,get或set都能监听到,并能在get或set时做出一些额外的操作。比如上面的,如果就干脆不想让别人拿到明星的手机号码,那么只要判断key等于phone时,直接返回undefined就行了。

Proxy的作用有大概有三个:1. 跟踪属性访问get和set(watch就是基于此特性实现的);2. 隐藏属性,不对外开放;3. 收集get和set的信息,做记录使用

策略模式

有种田忌赛马的赶脚,就是针对不同情况,选择不同的策略来应对。这些策略是相互隔离,互不干扰的。每个策略就是单独应对某种情况。那么具体使用就是当有多个条件分支时,将这些条件分支进行单独处理,而不是使用很多个if-else或switch-case。

使用这个有啥好处呢?好处就是看起来很舒服,一眼就知道这个分支是干嘛的。然后益于拓展,也不会干扰到其他分支。就是增加或减少一种情况,对于其他处理情况没有任何影响。这个主要是用在较为复杂的处理情况。假如只有一两种分支,且比较简单,那还是if-else一把梭哈吧。

看看下面这种情况:

class user {
    type: string
    constructor(type: string) {
        this.type = type
    }

    buy() {
        const { type } = this
        if (type === 'oridinary') {
            console.log('普通用户');
            //  TODO
        }
        if (type === 'member') {
            console.log('会员用户');
            //  TODO
        }
        if (type === 'vip') {
            console.log('VIP用户');
            //  TODO
        }
    }
}

const user1 = new user('vip')
user1.buy();

此时,没有使用策略模式,而是if-else一把梭哈。管他啥用户,全都放在buy这个函数中。当情况足够多的时候,这个buy函数会直接让你丧失编写的欲望。我是谁?我在哪儿?我要干嘛来着?。但是,当使用了策略模式,情况就不一样了。

interface Iuser {
    buy:() => void
}

class OridinaryUser implements Iuser{
    buy() {
        console.log('普通用户');
    }
}

class MemberUser implements Iuser {
  buy() {
    console.log('会员用户');
  }
}

class VIPUser implements Iuser {
  buy() {
    console.log('VIP用户');
  }
}

const user1 = new OridinaryUser();
user1.buy();

可以看到,使用了策略模式来编写,就看起来舒服多了。而且用户种类多了,拓展起来也很方便,直接在后面添加就行。

写到这里,忽然感觉这个模式和工厂模式大为相似。不确定,再回去看一眼工厂模式。确定了,还真是非常相似。工厂方法模式中只管生产实例,具体怎么使用工厂实例由调用方决定。策略模式是将生成实例的使用策略放在策略类中配置后才提供调用方使用。 工厂方法模式调用方可以直接调用工厂方法实例的方法属性等,策略模式不能直接调用实例的方法属性,需要在策略类中封装策略后调用。

总结

  1. 工厂模式:创建对象的一种方式。不用每次都亲自创建对象,而是通过一个既定的“工厂”来生产对象。
  2. 单例模式:即一个对象/实例只能被创建一次,创建出来之后被缓存,在各个地方继续使用。在单个系统中是唯一的,不能再被重复创建出来。
  3. 观察者模式:订阅事件然后被动等待通知。
  4. 发布订阅模式:订阅事件然后主动通知。
  5. 迭代器模式:顺序访问有序结构,不需要知道有序结构的底层表示。多使用for-of,map,filter,reduce。
  6. 原型模式:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
  7. 装饰器模式:针对一个对象,动态的添加新功能,但是不改变它原有的功能。
  8. 代理模式:针对这个对象设置代理,拦截所有对这个对象的访问,只能通过代理去访问这个对象。
  9. 策略模式:对多个分支进行隔离处理,不使用if-else或switch-case。

就这几种模式而言,也要花上很多时间去了解学习再实践了。当然,还有好多其他模式也是很值得学习的。但是一步一个脚印吧,以后慢慢学习。