前端设计模式(一)

场景
是否经常review别人的代码看不懂?
是不是也怕自己写的代码别人看不懂?
那是因为你不会使用套路。
万物皆有套路,coding也是。
coding的套路便是设计模式(Design Patterns)
不好的代码可以使用设计模式来提升自己的代码质量。让别人一看就知道你在干什么,你写的代码意义是什么。
设计模式都是从后端代码总结出来的,当然也适合前端,虽然Javascript设计之初不是那么严谨,但可以用过我们的双手让前端代码更加“严谨”。

下面介绍几个适合前端的设计模式

工厂模式 Factory Pattern

工厂模式是一种很常用的模式。由一个工厂对象决定创建某一种产品对象类的实例,主要来创建同一类型产品对象。
讲人话:我想要一个64G,白色,iphone12。 中间怎么实现,是你工厂的事,你给我想要的就好了。
前端实现:通过一个js方法,return一个结构相同数据不同对象。

以我们后台系统(如mice2)为例子。
场景如下:
后台系统往往根据角色权限不同,显示页面不同。
也就是根据不同角色,在Vue, React等SPA应用中的路由不同,返回不同页面。
那让我们创建一个权限工厂吧。


//安全模式工厂
let PermissionFactory = function(role) { 
    if(this instanceof PermissionFactory) { 
        return new this[role](); 
    } else { 
        return new PermissionFactory(role); 
    } 
};

//不同角色获得不同实例
PermissionFactory.prototype = { 
    staffs: function() { 
        this.name = "企业职员", 
        this.route = ['authorityManager', 'recommendPartner', 'goodsOperation', 'goodsManager', 'supplierManager'],
        this.auth = function(){
            //企业职员授权方法
        }
    }, 
    supplier: function() { 
        this.name = "供应商", 
        this.route = ['supplierManager', 'goodsManager', 'assignOrder'],
        this.auth = function(){
            //供应商授权方法
        }
    }, 
    admin: function() { 
        this.name = '管理员', 
        this.route = ['admin', 'setting'],
        this.auth = function(){
            //管理员授权方法
        }
    } 
};

let staffs = PermissionFactory('staffs'); //获取企业职员权限
let supplier = PermissionFactory('supplier');//获取供应商权限
let admin = PermissionFactory('admin');//获取管理员权限

为了让代码更加抽象,我们应把所有角色行为统一起来。

//main 主流程
var role = UserFactory(role);
role.auth();
initRouter(role.route);

我们把不同角色的操作行为统一了起来。多个角色,一个流程。
这样写,我们的工厂模式就物有所值了。

思考
那如果每个实例都是大段代码,我们该如何更好的组织?

单例模式 Singleton Pattern

单例模式是保证一个类只有一个实例,并且提供一个访问它的全局访问点。
说人话:我要你一个就够了。
前端实现: if木有,返回创建一个新的。else,返回刚才创建的那个。

const single = (function(){ 
    let unique; 
    function getInstance(){ 
        return unique? unique : unique = new Construct(); 
    } 
    function Construct(){ 
        //具体实现
    } 
    return { 
        getInstance 
    } 
})();

这个实战中能解决什么问题?

你是不是经常看到很多项目里,把全局函数/属性,挂在到window上?
window.globalFunctionA
window.globalFunctionB
window.eventbus = new Vue();

甚至不写window.会有直接调用globalParamA,然别的同学看的一脸懵逼。
还有为了多平台,比如在Nodejs里使用,避免挂在在window上

2.例如axios, EventBus, 播放器,组件里面的黑色背景组件等,一切一个项目里只有一个实例的对象都应使用单例模式。

思考

  1. 是不是每次用使用单例模式都需要重复写上一段代码呢?
  2. 如何改造一下,让单例模式更通用?
  1. 让工厂模式和单例模式结合起来看起来更有高级感呢?

观察者模式 Observer Pattern

观察者模式是一种一对多的依赖关系。
人话: 谁关注我,谁就看得到我动态。
前端实现:定义一个Array,存放订阅者,当我有新消息时,通知订阅者。

class Observable {
    constructor() {
        //定义事件池
        this.events = {};
    }

    add(name, handle) {
        //增加一个事件
        this.events[name] = handle;
    }

    notify(name, ...params) {
        //通知
        if (this.events[name]) {
          this.events[name].apply(this, params);
          return;
        }
        throw new Error('not find!');
    }
}

这是极简的观察者模式。
解决什么问题呢?
当Vue, React项目中,跨组件通信时,有时候会非常不方便。
比如Car项目里是由一个父容器,包含所有的异步路由子容器的结构。总不能把所有通信方法都写在父容器上吧?
所以一般会引用第三方的event库,那你有没有想过实现方式呢?
那说到观察者模式,必然会说到发布/订阅模式。
让我们看看两种模式的全貌吧。

观察者模式(Observer pattern)代码示例

function ObserverList(){
  this.observerList = [];
}
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
ObserverList.prototype.count = function(){
  return this.observerList.length;
};
ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};
ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;
  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }
  return -1;
};
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};

function Subject(){
  this.observers = new ObserverList();
}
Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};
Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};
Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
    this.observers.get(i).update( context );
  }
};

function Observer(){
  this.update = function(){
    // ...
  };
}

发布订阅模式(Publish subscribe pattern)代码示例

var pubsub = {};
(function(myObject) {
    var topics = {};
    var subUid = -1;
    myObject.publish = function( topic, args ) {
        if ( !topics[topic] ) {
            return false;
        }
        var subscribers = topics[topic],
            len = subscribers ? subscribers.length : 0;
        while (len--) {
            subscribers[len].func( topic, args );
        }
        return this;
    };
    myObject.subscribe = function( topic, func ) {
        if (!topics[topic]) {
            topics[topic] = [];
        }
        var token = ( ++subUid ).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };
    myObject.unsubscribe = function( token ) {
        for ( var m in topics ) {
            if ( topics[m] ) {
                for ( var i = 0, j = topics[m].length; i < j; i++ ) {
                    if ( topics[m][i].token === token ) {
                        topics[m].splice( i, 1 );
                        return token;
                    }
                }
            }
        }
        return this;
    };
}( pubsub ));

来源:《Learning JavaScript Design Patterns》 by Addy Osmani

是不是看出一点差异?
观察者模式只有【观察者】-【被观察者】两个角色。
发布/订阅模式则有【发布者】-【经纪人】-【订阅者】,三个角色。经纪人作为“中间人”不一定要通知发布者,订阅者可以主动问经纪人要数据。市面上最佳实践就是各种消息队列应用。

subscribe-pattern.jpeg

思考
浏览器中有哪些常见的观察者模式?

策略模式 Strategy Pattern

策略模式指的是定义一些列的算法,把他们一个个封装起来,目的就是将算法的使用与算法的实现分离开来,避免多重判断条件,更具有扩展性。
人话: 把很多判断分成小的方法增加扩展性。
前端实现: 利用prototyp进行扩展。

在机票,火车票,酒店等情景下,有普通客户,Vip客户,白金客户折扣等等。
如果我们正常写就会有如下代码。

function Price(personType, price) { 
    if (personType == 'vip') { 
        //vip5折 
        return price * 0.5; 
    } 
    else if (personType == 'superVip'){ 
        //白金3折 
        return price * 0.3; 
    } else { 
        //其他都全价 
        return price; 
    } 
}

这样写对不对呢? 对
这样写好不好? 不好。
原因:当我们继续增加各种折扣,满减等等销售手段计算,持续增加ifelse判断。代码将很难维护。
使用策略模式,代码改进如下

//普通客户
const price = function() { 
    return this.price; 
} 
// vip客户
const vipPrice = function() { 
    return this.price * .5; 
} 
// 白金客户
const superVipPrice = function() { 
    return this.price * .3; 
} 

// 收银台
function Cashier() { 
 this.name = ''; 
 this.strategy = null; 
 this.price = 0; 
} 
Cashier.prototype.set = function(name, strategy, price) { 
 this.name = name; 
 this.strategy = strategy; 
 this.price = price; 
} 
Cashier.prototype.getResult = function() { 
 console.log(this.name + ' 的结账价为: ' + this.strategy.call(this)); 
} 

var cashier = new Cashier(); 
cashier.set ('普通客户', price, 200); 
cashier.getResult(); // 普通客户 的结账价为: 200

cashier.set ('vip客户', vipPrice, 200); 
cashier.getResult(); // vip客户 的结账价为: 100 

cashier.set ('老客户', superVipPrice, 200); 
cashier.getResult(); // 白金客户 的结账价为: 60 

思考
虽然乍眼看,代码比原来的多了。
但好处:

  1. 代码更清晰了。
  2. 不同客户折扣计算之间解耦了。
  3. 实际项目中每个折扣(策略)中会增加其他校验,审核等其他操作,代码将十分优雅。扩展性,可维护性好了很多。

总结

本期讲了四种最常用的模式。
设计模式只是新手入门很好的套路,优秀的代码也不会被设计模式所限,把设计模式融会贯通到自己的代码里,才是真正理解了设计模式。

你可能感兴趣的:(前端设计模式(一))