Skip to content
Eggy Shrimp

结合业务对 JavaScript 设计模式的理解

楔子

👣 先立定一下:可以从这篇文章获得什么

  • 这篇是一个楔子,不是百科全书;因为网络中对于设计模式的介绍非常普遍,没必要重复造轮子;
  • 但,什么驱动我们需要去理解设计模式,以及找到百科全书之后,如何实践:提供小小的例子,以产生大大的灵光;

🤔 再解构一下主题:设计模式是什么

面向对象软件设计过程中,针对特定问题的简洁而优雅的解决方案;

  • 面向对象软件设计过程中,如何理解?
  • 简洁和优雅是指
    • 增加 xx 可复用性以及维护性
    • 误区:减少代码行数
    • 引出标准:找出程序中变化的地方,并将变化封装起来

e.g. 设计模式和知识库一致

  • 设计模式,针对通用范围特定问题
  • 知识库,针对特定业务场景的问题

🎯 设计模式的适用性(使用的范围)

简单不需要设计模式,我们是为了增加项目的维护性和可复用性;

💦 看起来,JavaScript 的设计模式可以理解为...

  • 在业务迭代需求开发中(前提)
  • 为了增强项目的维护性和可复用性(why)
  • 针对特定的逻辑场景总结出的一套实用风格解决方案(how)

设计模型(what)

💦 如何阅读这一章节?

分辨模式的关键是意图而不是结构

原型模式

用于创建对象,区别于传统使用类型来创建对象的方式;

  • 不关注对象具体类型:找到一个对象,然后通过克隆来创建一个一模一样的对象;
  • 问题:必须新建一个属于相同类的对象。然后,你必须遍历原始对象的所有成员变量,并将成员变量值复制到新对象中。
  1. 实现

使用创建对象模式:以存在分身技能的飞机为例子;

var Plane = function() {
    this.blood = 100;
}
var plane = new Plane();
plane.attack = oldplane.attack;

// 原型模式创建分身
var clonePlane = Object.create(plane);

Object.create = function(obj) {
    var F = function(){};
    F.prototype = obj;
    return new F();
}
  1. JavaScript 使用原型模式作为编程范式
  • 所有数据都是对象:js 中大部分数据都是对象
    • 基本类型 + 对象类型
    • 但基本类型也可以通过包装类的形式存在
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆
    • JavaScript 中的 new 关键词背后的实质是:调用函数构造器
    • 先克隆 Object.prototype,再进行额外操作
  • 对象会记住它的原型
    • proto 属性
  • 如果对象无法响应某个请求,会把请求委托给原型
    • 原型层级:如果对象上不存在 xx 成员,去它的 __proto__ 原型对象去找;
💡

复习一下

单例模式

  • 保证一个类只有一个实例,并提供一个访问它的全局访问点;
  • 解决什么问题呢:
    • 保证一个类只有一个实例。为什么会有人想要控制一个类所拥有的实例数量?最常见的原因是控制某些共享资源(例如数据库或文件)的访问权限
    • 为该实例提供一个全局访问节点。存储重要对象的全局变量吗?它们在使用上十分方便,但同时也非常不安全。window.marketpageRecordData
  1. 实现
  • 将默认构造函数设为私有,防止其他对象使用单例类的 new 运算符。
  • 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。
var Singleton = function(name) {
    this.name = name;
    this.instance = null;
}

Singleton.prototype.getName = function() {
    alert(this.name);
}

Singleton.getInstance = function(name) {
    if (!this.instance) this.instance = new Singleton(name);
    return this.instance;
}

// a === b --> true
var a = Singleton.getInstance('eva1');
var b = Singleton.getInstance('eva2');

这是一个 mvp,存在小问题:都需要使用 Singleton.getInstance 才能使用单例;

变得更加自然 👇

const CreateDiv = (function() {
    let instance = null;
    const CreateDiv = function(html) {
        if (instance) {
            return instance;
        }
        this.html = html;
        this.init();
        return instance = this;
    }

    CreateDiv.prototype.init = function() {
        const div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.append(div);
    }
})()

// a === b --> true
var a = new CreateDiv('sven1');
var b = new CreateDiv('sven2');

  1. 单例模式在业务中有什么用呢?
  • 频繁实例化然后又销毁的对象,使用单例模式可以提高性能

  • (全局状态管理)项目获取到请求实例,这样不需要每次都会创建一个新的 axios 配置实例
// base.js
var instance = null;

export {
    getInstance: function() {
        if (instance) instance = new Axios();
        return instance;
    }
}

// req.js
function get() {
    return Axios.getInstance().get();
}

  • 获取到 market.getDeviceInfo() 设备信息;

    // 返回设备信息
    @javascriptInterface
    function getDeviceInfo() {
        DeviceInfo deviceInfo = new DeviceInfo();
        return deviceInfo;
    }
    
    
    let ajaxData;
    
    function getDeviceInfo() {
        if (!ajaxData) {
            ajaxData = market.getDeviceInfo();
        }
    
        return ajaxData;
    }
    
    

策略模式

  • 定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
  • 适用范围:在程序中,一个功能可能有多种方案可以选择:一个压缩文件程序,可以选择 zip 算法,也可以选择 gzip 算法;
  1. 实现
var calculateBonus = function(level, salary) {
    if (level === 's') {
        return salary * year * 4;
    }

    if (level === 'a') {
        return salary * 3;
    }

    if (level === 'b') {
        return salary * 2;
    }

    if (level === 'c') {
        return salary * -1;
    }

    if (level = 'd') {

    }
}

  • 如果后面我们想要复用这些其中算法该怎么办呢?
  • 而且逻辑也很多,分支很乱;不清晰
// 单独解耦算法,清晰
var strategies = {
    "s": v => 4 * v,    // 可复用,单独抽出
    "a": v => 3 * v,
    "b": v => 2 * v,
    "c": (v, n) => (n + 1) * v
}

var calculateBouns = function(level, salary) {
    return strategies[level](salary);
}

  1. 在业务中
  • 一个简单场景,postcss 编译逻辑

    暂时无法在飞书文档外展示此内容

  • feedbackV1 这个页面存在很多的校验逻辑,里面校验逻辑混杂,可能还有很多副作用

function submit() {
    if (!isLogin()) {
goToLogin();
      return;
    }
// 判空校验 1: 举报理由
    if (!data.issueTypes.lvl2Option) {
showTip(feedbackText('confirmIssueTypes'));
      return;
    }
// 判空校验 2: 问题描述
    if (!data.problemDesc) {
showTip(feedbackText('confirmIssueDesc'));
      return;
    }
// 判空校验 3: 证明材料,长度校验
    const issuePicList =checkImgs();
    if (!issuePicList || !issuePicList.length) {
showTip(feedbackText('confirmAttachedImg'));
      return;
    }

    // ...
}

使用策略模式该如何去实现校验呢?

function submit() {
    validator.add(data.issueTypes.lvl2Option, {
        type: 'required'
    });

    validator.add(data.issuePicList, [
        {
            type: 'maxlength',
            opt: {max: 30, min: 5}
        }        {
            type: 'required'
        }
    ]);

    validator.add(data.tel, [
        type: 'isMobile'
    ])

    validator.start();
}

var strategies = {
    'required': v => v !== undefined;
    'maxlength': (v, { max, min }) => v.length < max && v.length > min;
    'isMobile': (v) => /^1[3|5|8][0-9]{9}$/.test(value)
}

var Validator = function() {
    this.cache = [];
}

Validator.prototype.add = function(v, rules) {
    rules.reduce((p, rule) => {
        this.cache.push(function() {
            var strategy = rule.type;
            return strategies[strategy](v, rule.opt);
        })
    })
}

Validator.prototype.start = function(v, rule, msg) {
    this.cache.reduce((p, c) => c(), {});
}

发布订阅模式

又叫观察者模式,定义对象间的一对多的依赖关系:当一个对象发生改变时,所有依赖它的对象都得到通知;

  1. 实现

e.g. 是不是经典的 document.addEventListener 就是个模式的实践;

  1. 在业务中有什么用呢
  2. Vue 的依赖收集就是一种
  3. Antd 不少实现都是经典的设计模式,以 antd.grid 为例子:如何实现设备断点发生变化的时候触发钩子逻辑呢 https://github1s.com/ant-design/ant-design/blob/master/components/_util/responsiveObserver.ts#L64
  4. 场景描述:antd 定义了一批有语义的断点 xs,sm,md,想要在这些断点生效的时候触发事件
    1. 注册 subscribe
    2. 触发监听器,通过 matchMedia 实现
    3. 在 dispatch 中执行回调逻辑
subscribe(func: SubscribeFunc): number {
  if (!subscribers.size) this.register();
  subUid += 1;
  subscribers.set(subUid, func);
func(screens);
  return subUid;
}

register() {
Object.keys(responsiveMap).forEach((screen:Breakpoint) => {
    const matchMediaQuery = responsiveMap[screen];
    constlistener = ({ matches }: { matches: boolean }) => {
      this.dispatch({
        ...screens,
        [screen]: matches,
      });
    };
    const mql = window.matchMedia(matchMediaQuery);
    mql.addListener(listener);
    this.matchHandlers[matchMediaQuery] = {
      mql,
listener,
    };

listener(mql);
  });
}

const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({
  xs: `(max-width: ${token.screenXSMax}px)`,
  sm: `(min-width: ${token.screenSM}px)`,
  md: `(min-width: ${token.screenMD}px)`,
  lg: `(min-width: ${token.screenLG}px)`,
  xl: `(min-width: ${token.screenXL}px)`,
  xxl: `(min-width: ${token.screenXXL}px)`,
});

const getResponsiveMap = (osversion: number, deviceTYpe: fold | pad) => {
     if (pad) {
         return (
             xs: xxxx
             sm: ssss
             freeform: (medai)
         )
     }
}

适配器模式(包装器)

  • 适配器模式的作用是解决两个软件实体之间接口不兼容的问题
  1. 实现
  • 我想写一个渲染地图的方法,renderMap
  • Google map 和 baidumap 都提供了地图展示的方法,不过,前者 google map 的显示逻辑叫做 show,后者叫做 put
  • 我需要给 google map 写一个 renderGoogleMap,还要写一个 renderBaiduMap?
  • 地图的功能在不断扩展,里面各种判断 google map 和 baidu map 的方法,很麻烦
var renderMap = function(map) {
    if (typeof map === 'googlemap') {
        map.show();
    }

    if (typeof map === 'baidumap') {
        map.put();
    }
}

varbaiduMapAdapter = {
    show: import('baidum').then(baidumap => baidumap.put()) /* xxx */
}

var renderMap = function(map) {
    map.show && map.show();

}

renderMap()

  1. 在业务中有什么用呢

  2. appstore-mobile 经常存在一个组件因为和第一次写的业务场景耦合太深,没法把 ui 交互逻辑在别的地方使用的情况

    1. 可以分离开来,一个是 ui 组件,然后在业务场景中加一个适配器
    // xx ui 组件
    interface uiprops {
        cover: ImgUrl;
        title: string;
    }
    
    // adapter 适配器逻辑 A
    interface AdapterAProps {
        comsData: {
            title: string;
            cover: string;
        };
        keyName: string;
    }
    
    const uiprops = computed(() => ({
        title: props.comsdata.title,
        cover: props.comsdata.cover,
        // ...
    }))
    
    // adapter 适配器逻辑 B,这个场景放在应用号场景,所以信息都在 assembleInfo 中
    interface AdapterBProps {
        comsData: {
            assembleInfo: {
                title: string;
                cover: string;
            }
        };
        keyName: string;
    }
    
    const uiprops = computed(() => ({
        title: props.comsdata.assembleInfo.title,
        cover: props.comsdata.assembleInfo.cover,
        // ...
    }))
    
    

Ref