前端常用设计模式

3,889 阅读9分钟

设计模式一个比较宏观的概念,设计模式定义是软件开发人员在软件开发过程中面临的一些具有代表性问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。js中函数、类、组件等实际上都是实现了代码的复用,那么设计模式可以说成是经验的复用。当然实际开发中不用设计模式同样也是可以实现需求的,只是在业务逻辑复杂的情况下,代码可读性及可维护性变差。所以随着业务逻辑的扩展,了解常用设计模式解决问题是必须的。 设计模式可以按照字面意思来看其实可以分成2个部分。一是设计,二是模式。设计其实就是设计原则,模式就是设计模式。设计模式一般需要遵循设计原则。那么设计原则有哪些呢?如下:

  • 单一职责原则(Single Responsibility Principle) 一个类应该只有一个发生变化的原因。简而言之就是每个类只需要负责自己的那部分,类的复杂度就会降低。

  • 开闭原则(Open Closed Principle)

    • 一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
  • 里氏替换原则(Liskov Substitution Principle) 所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。

  • 迪米特法则(Law of Demeter) 迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

  • 接口隔离原则(Interface Segregation Principle)

    • 多个特定的客户端接口要好于一个通用性的总接口
  • 依赖倒置原则(Dependence Inversion Principle) 1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。 2、抽象不应该依赖于细节,细节应该依赖于抽象

    在实际开发中是不是所有的设计都必须遵循所有的设计原则呢?很多情况下也并不能完全被设计原则束缚起来了。具体情况还是需要具体分析,我们会发现某些程序设计其实也违背了某些设计原则,所以在使用的时候还是需要灵活运用。接下来是设计模式,设计模式可以分成三大类:1.创建型。2.结构型。3.行为型。

单例模式

  • 单例模式 (Singleton Pattern)又称为单体模式,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。 实现一个实例可以通过静态属性来实现代码如下:
class Person{
  static instance;
  constructor(name){
      if(Person.instance){
          return Person.instance;
      }else{
          Person.instance = this;
      }
      this.name = name;
  }
}

通过静态属性instance来记录Person类的实例化状态来实现单例。但是这种实现方式并不灵活,假如我们有多个类都需要实现单例,我们需要给每个类都写上静态成员来保存实例化状态。有没有一种通用的办法来实现呢?其实我们可以通过高阶函数及闭包的缓存特性来记录类的实例化状态。具体代码如下:

class Person{
    constructor(name){
        this.name = name;
    }
}
class Animal{
    constructor(name){
        this.name = name;
    }
}

function getSingle(fn){
    let instance;
    return function(...args){
        if(!instance){
            instance = new fn(...args); 
        }
        return instance;
    }
}
let PSingle = getSingle(Person);
let PAnimal = getSingle(Animal);
let  zhangsan = new PSingle("张三");
let  lisi = new PSingle("李四");
console.log(zhangsan,lisi);

上述代码通过getSingle高阶函数来实现多类的单例。单例优点:单例模式节约内存开支和实例化时的性能开支,节约性能。当然单例也会有缺点:单例模式扩展性不强。在具体使用中如设计一款程序只需要一个实例的情况我们需要用到单例模式。例如 原生js这块document、window对象其实都是单例的一种体现,全局范围内只有一个实例。

工厂模式

封装具体实例创建逻辑和过程,外部只需要根据不同条件返回不同的实例。例如:

class Luban{
   constructor(){
       this.name = "鲁班";
   }
}
class Yase{
   constructor(){
       this.name = "亚瑟";
   }
}

function Factory(type){
   if(type==="鲁班"){
       return new Luban();
   }else if(type==="亚瑟"){
       return new Yase();
   }
}
let luban =  Factory("鲁班");
console.log(luban);

工厂模式在js这块的使用很多情况下可以通过构造函数及类来进行取代。它的优点是实现代码复用性,封装良好,抽象逻辑。缺点也很明显增加了代码的复杂程度。

装饰器模式

使用一种更为灵活的方式来动态给一个对象/函数等添加额外信息。也就是在原本对象或者功能上来进行扩展功能或者属性。符合设计原则中的开闭原则,通过装饰器扩展功能可以尽可能保证内部的纯洁性,保证内部少的修改或者是不修改。代码如下:

class Yase {
   constructor(){
       this.name = "亚瑟";
   }
   release(){
       console.log("释放技能");
   }
}

如上Yase类想要扩展release方法功能。我们有几种选择1.直接在release方法里来修改扩展这样违背了开闭原则。2.通过extends来继承扩展release当然这样也是可以的。3.可以选择装饰器来装饰release方法让其功能变得更强。代码如下:

function hurt(){
    console.log("造成100点伤害");
}
Function.prototype.Decorator = function(fn){
    this();
    fn();
}

let yase  = new Yase();
// yase.release();
yase.release.Decorator(hurt);  // 释放技能 造成100点伤害

上述通过Decorator装饰器来扩展了hurt方法,使release方法变得更加强大。这样做的目的在于保持原本类的纯洁性,避免过多的副作用。当然缺点就是扩展的功能也原始类无关。

观察者模式

定义一个对象与其他对象之间的一种依赖关系,当对象发生某种变化的时候,依赖它的其它对象都会得到更新。代码如下:

export default class GameEvent{
 constructor() {
     this.handle = {};
 }
 addEvent(eventName, fn) {
     if (typeof this.handle[eventName] === 'undefined') {
         this.handle[eventName] = [];
     }
     this.handle[eventName].push(fn);
 }
 trigger(eventName) {
     if (!(eventName in this.handle)) {
         return;
     }
     this.handle[eventName].forEach(v => {
         v();
     })
    }
}

观察者模式优点可以看出可以1.支持一对多关系。2.可以延迟执行事件。3.在2个对象之间解耦。在原生js、nodejs、vue、及react中观察者模式都被广泛使用 。

代理模式

代理模式 为其他对象提供一种代理以控制对这个对象的访问,类似于生活中的中介。实际上ES6中Proxy就是一个代理模式的体现,可以通过代理对象来实现对象访问的控制。比如按照中介实现如下代码:

let zhangsan = {
       sellhouse(num){
           console.log("卖了:" +num + "元");
       }
   }

   let proxySeller = {
       sellhouse(hasSold,num){
           if(hasSold){
               zhangsan.sellhouse(num-10); 
           }else{
               zhangsan.sellhouse(num);
           }
       }
   }

   proxySeller.sellhouse(true,100);

上述代码通过中介,也就是proxySeller来代理zhangsan来实现卖房子,来控制zhangsan的输出。例子很好理解。在js中防抖函数同样也用到了代理模式,如下:

function debounce(fn, time) {
       time = time || 200;
       let timer;
       return function (...arg) {
           clearTimeout(timer);
           timer = setTimeout(() => {
               fn.apply(this, arg);
           }, time)
       }
   }
   let newFn = debounce(fn,50);

   window.onresize = function () {
       // fn();
       // console.log(newFn)
       newFn();
   }

通过debounce高阶函数返还的新函数来代理fn的执行从而控制fn执行频率。优点同样是符合开闭原则扩展控制功能。缺点是代理结果受到被代理者的影响。

适配器模式

两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。简单理解就是通过适配器让2个接口可以兼容。例如:

function getUsers(){
 return [
     {
         name:"zhangsan",
         age:20
     },{
         name:"lisi",
         age:30
     }
 ]
}
function Adaptor(users){
 let arr = [];
 for(let i=0;i<users.length;i++){
     arr[users[i].name] = users[i].age;
 }
 return arr;
}
let res =  Adaptor(person());
console.log(res);

上述代码中想得到类似{zhangsan: 20}数据结构。我们通过Adaptor类实现了axios在node端及前端的适配工作来实现数据的转换。适配器模式优点: 1、可以让任何两个没有关联的类一起运行。 2、提高了类的复用。 3、增加了类的透明度。 4、灵活性好。缺点是:过多地使用适配器,会让系统非常零乱,不易整体进行把握。在axios源码中通过Adaptor类实现了axios在node端及前端的适配工作。

混入模式

Mixin是可以轻松被一个子类或一组子类继承功能的类,目的是函数复用。继承Mixin是扩展功能的方式,另外也可能从多个Mixin类进行继承。例如现在有2个类,一个Yase类,一个Skills类。如下:

class Yase{
    constructor(name){
        this.name = name;
    }
}

class Skills{
    hurt(){
        console.log("造成100点伤害");
    }
    walk(){
        console.log("走路...");
    }
    run(){
        console.log("跑步...");
    }
}

现在想同时继承yase类和技能类,我们当然可以让Skills来继承Yase类来实现,但是逻辑上没有太大关联。这个时候我们可以考虑通过mixin模式来混入2个类中的功能具体实现如下:

function mixin(receivingClass,givingClass){
    if(arguments[2]){
        for(let i=2;i<arguments.length;i++){
            receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]
        }
    }
}
mixin(Yase,Skills,"run","walk");
let yase  = new Yase("亚瑟");
console.log(yase);
yase.walk();

如上,代码设计上可读性更强。成功的将多个类中的功能混合在一起。混入模式优点可以提高可读性增加代码的复用性。缺点是将功能注入到原型中,会导致类或者方法指向混乱,找不到扩展方法的来源。混入模式在vue及react都有mixin的影子。

设计模式起初在后端应用中使用的比较多,随着前端工程量及业务逻辑的复杂很多设计模式在前端中也逐渐被使用。所有了解前端设计模式是提升前端编程思维的必经之路。当然设计模式远不止如此。后期再逐渐更新。此时,得下班了。溜了,溜了。。。。