面向对象和模块化

JS是否有必要使用面向对象、设计模式

在一次面试过程中,一位已经有5年工作经验的前端,在回答面试问题时这样说到。

问:你能说说JS的面向对象和设计模式吗?
回答说:这些内容主要是后端的Java,C#这种高级语言才会用到的,前端一般我们没有用到。

对于这样的回答,不禁让我有点无话可说,JS中是否有必要使用面向对象以及设计模式呢?我列举了以下几个场景:

数据接口请求

一般的,在请求接口方面,我们一般会使用一些第三方库,比如axios。然后在逻辑代码部分,比如在组件中直接使用axios进行请求,例如:

  let methods = {
    /**
     * 获得分类信息
     */
    async getBarData () {
      try {
       let res = await axios.get(url,params)
      }catch (e) {
        console.error('something error' ,e)
      }
    },
   }

这样的做法在功能上讲没什么问题,但在新增一些其他动作后,这样的做法就变得非常难以管理。比如,需要在请求中加入一些关联请求,需要获取一个商品页的列表,查询参数包含,分页参数(当前页,查询数),分类Id,搜索内容,排序方式,筛选项。执行该去请求时,发现分类Id也需要另外一个接口去获取。于是代码成了:

  let params2 = {
     sort:-1,
     search:'',
     filter:'',
     page:{
       start:1,
       number:10
     }
  }
  let methods = {
    /**
     * 获得商品列表
     */
    async getGoodsData () {
      try {
       let {id:typeId} = await axios.get(url.goodsType,params1) // 获取所有分类Id
       let res = await axios.get(url.goods,{...params2,typeId}) // 获取商品
      }catch (e) {
        console.error('something error' ,e)
      }
    },
   }

上面的代码中,我们简单的实现了获取一个接口的值然后请求另外一个接口。那么如果当前的搜索内容、或者分页数据修改了,还需要重新获取新的商品数据,此时getGoodsData还需要执行一遍,而获取分类的请求又需要请求一遍,所以需要改动代码为:

  let params= {
     sort:-1,
     search:'',
     filter:'',
     page:{
       start:1,
       number:10
     }
  }
  let methods = {
    /**
     * 获得分类信息
     */
    async getTypeData () {
      try {
       let {ids} = await axios.get(url.goodsType,params1) // 获取所有分类Id
       this.typeIdNow = ids[0]
      }catch (e) {
        console.error('something error' ,e)
        throw e
      }
    },
    /**
     * 获得商品列表
     */
    async getGoodsData () {
      try {
       (this.typeIdNow === undefined) && await getTypeData()
       let typeId = this.typeIdNow
       let res = await axios.get(url.goods,{...params,typeId}) // 获取商品
      }catch (e) {
        console.error('something error' ,e)
      }
    },
   }

params的任意数据改变后会请求getGoodsData,这样暂时我们已经实现了一个商品请求的逻辑,并且支持数据暂存。

紧接着问题又来了,切换类别时会要求获取新的筛选列表(不同的分类下筛选列表是不同的)。

切换类别后,会要求重置params,因为之前的搜索值,分页值在切换类别后不能继续使用。

字段组装,比如筛选字段的filter,一般的后台可能会用一些特殊的分隔符比如(|)来做多个筛选项的分割,此时我们又需要处理以下的代码:

return this.types.map(val=>val.id).join('|')

节流优化,用户输入Value时,需要做防抖函数的优化,防止一直请求接口。

恩,终于,当一大堆问题都解决后,需求来了,能不能在其他组件使用这些数据?? 哇特?

回顾一下,我们要做的就是一个内容,getGoodsData为什么会同时出现这么多代码呢?一个商品列表的组件会需要这么多组装数据的代码吗?

面向对象优化

面对这种让人抓耳挠腮,看着头晕的代码难道就没有更优雅的实现方式吗?面向对象了解一下,数据模型了解一下!

我们可以将Goods这一中数据类型抽象成为一种资源对象,在Model中专门处理Goods获取时所需要的数据组装等工作。

import { API, axios } from '../api'

/**
 * 商品列表数据
 */
class Goods {
  private params: Object = {};
  private initParamsData: Object = {
     sort:-1,
     search:'',
     filter:'',
     page:{
       start:1,
       number:10
     }
  };

  constructor() {
    this.initParams()
  }

  /**
   * 初始化所有请求参数
   */
  public initParams() {
    this.params= JSON.parse(JSON.stringify(this.initParamsData)) // 深拷贝
  }

  /**
   * 设置请求参数
   * @param key
   * @param val
   */
  public setParams(key, val) {
    this.params[key] = val
    
  }
  /**
   * 获取商品请求
   */
  public async get(params = {}) {
    let {id:typeId} = await Type.get() // 在另外一个Type类中获取并做缓存处理
    params = { ...this.params, ...params ,typeId}

    let res = await axios.get(API.GOODS_LIST, { params })

    return res
  }


  public async save() {

  }

}

export default new Goods()

然后就可以在组件中优雅的进行使用,Goods的数据模型中已经可以自行处理依赖请求,缓存数据,参数组装等功能,在另外组件使用中也同样可以使用相同的数据和缓存,代码如下:

  let methods = {
    /**
     * 获得商品列表
     */
    async getGoodsData () {
      try {
       let res = await Goods.get() // 获取商品
      }catch (e) {
        console.error('something error' ,e)
      }
    },
    /**
     * 设置请求参数
     * @param key
     * @param val
     */
    setParams(key,val){
      // 设置请求参数,对于部分需要特殊组装的字段可以在类中单独分装方法处理
      Goods.setParams(key,val) 
    }
   }

状态管理

一般的,在处理多组件数据通信时,会使用Redux/Mobx/Vuex这类Flux模式的状态管理库来处理。对于Vuex来讲,一般会在实例化一个Vuex,设置其state对象以及对应的mutations,然后将其挂载到Vue的原型链中,就可以方便的完成响应式的状态管理。

实际上,当项目中的不同路由层级,不同组件,不同生命周期的状态都混在一个state中管理是很混乱,很不明智的做法。当然Vuex中也提供了Modoles的用法,让我们可以通过不同的模块化来管理不同状态,从某种程度上来讲这也是一种面向对象的做法。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

但相对于Mobx的Class式用法,这种VuexModule的用法还是显得有些麻烦,让我们来看看Mobx的Class定义状态管理是如何处理的:

import {computed, observable} from "mobx"

class StoreData {
  @observable isSideBarShow = true // 是否显示侧边栏
  @observable routeNow: any = {}


  public setSideBarShow( val ) {
    this.isSideBarShow = val
  }

  public setRoute( val ) {
    this.routeNow = val
  }
}

export default new StoreData()

相比与Mobx的这种状态管理,引入了Es7中的装饰器,在编写代码中如果有使用其中所需要的状态值,可以直接方便的引入StoreData,然后直接使用即可。

从定义、使用、修改各种步骤都更容易理解和使用。

很遗憾的是,Mobx目前还没有完善的方案在Vue中使用。

Vuex社区中,也有很多开发者有着同样的感受,所以出现了类似Vuex-class这样的库,来使用Class模式编写状态管理。也希望Vuex官方能对面向对象编程有更好的支持!

组件继承

组件可以继承吗?比如我写了一个投票页面,用户是可以操作进行投票的,但是当他投票结束后,这个页面就不能再进行交互操作了,用户只能查看已经投票的结果。

正常的,我们可能会使用一个状态值来判断是否开放编辑功能。但是,当投票活动再加入:开始前,正在投票,投票结束等状态后。需要处理的就是:开始前已经投票的状态,开始前未投票的状态,正在投票已经投票的状态....这样6种状态。虽然从其他维度可以解决这样的问题,但状态再多复杂度就会再增加一层。如果还这样写,那么,恭喜你,传说中的面条代码就产生了!

所以我们最好还是用两个组件来做这件事情,一个组件用于提交,一个组件用于查看。在完成提交组件后,再实现查看组件时,会发现非常多的代码都是重复的 ,就比如投票数据获取,样式,模板,状态管理。

此时,组件继承的需求就变得愈发强烈,实际上,组件继承是可以实现的。拿Vue来说,在Vue2.5后Vue推出了vue-class-component。看看以下组件Test1。








继承组件Test2,在继承Test1后就可以使用Test1中定义的变量、样式,对于模板,实际上Vue在template中的内容最终也会被转移为render函数中返回的模板值,如果你的JSX了解的话,你可以把它理解为JSX,只不过Vue把它换了一个位置,如果你想在Vue中使用JSX同样是可以实现的,参考文档。那么Test1就可以写成:



JavaScript中是如何实现面向对象的

说了这么多,那么在JavaScript中是如何实现Class的呢?在ES5的标准中,是并没有Class关键字的。

在JavaScript中的所有数据跟对象都是Object.prototype对象。我们在JavaScript遇到的每个对象,实际上都是从Object.prototype对象克隆而来的,Object.prototype就是他们的原型。

在JavaScript中执行的new Object(),在内部引擎中实际上是从Object.prototype上面克隆一个对象出来,我们最终得到的才是这个对象。

function Person(gender){
  this.gender = gender
}
Person.prototype.getGender=function(){
  return this.gender
}

var me = new Person(1)
console.log(me.getGender())

在JavaScript中没有类的概念,但上面这段代码中不是明明调用了new Person()吗?

在这段代码中,Person并不是一个类,而是函数构造器。当使用new来调用函数时,实际上是在克隆Object.prototype对象,然后再才开始运行函数。

所以当我们得到me时,内部已经完成了对Person.protorype的克隆,当请求me.getGender时,JavaScript完成了以下几步操作:

  • 尝试查找me对象中是否有getGender属性
  • 没找到genGender,把该请求委托给Person的构造器原型,原型会被记录在__proto__中。
  • 在原型链中找到了getGender属性,并返回它的值

多态与继承

多态的实际含义是:同一操作作用于不同对象上面,可以产生不同的解释和不同的执行结果。多态分为编译期多态、运行期多态。

举个例子:两个人打招呼,不同的人见面打招呼的方式不一样。比如好基友见面,会说:Hey,老哥~。陌生人见面会说:你好,幸会。Github上提问会说:Hello,nice to ...。韩国人见面打招呼会用韩语,日本人见面打招呼用日语等等。

用一段JavaScript代码来实现英国人和中国人打招呼:

var Chinese = function(){}
var British = function(){}

var sayHello = function(man) {
  man instanceof Chinese && console.log('你好')
  man instanceof British && console.log('Hello')
}

sayHello(new Chinese()) // 编译期多态
sayHello(new British())

这段代码确实实现了"多态性",当在发出sayHello的命令后,不同的人会执行不同的打招呼方式,但却不是理想化的。试想如果后来新增了一个俄罗斯人,就必须要修改sayHello函数,才能实现俄罗斯人打招呼。那么后面再加入对不同人打招呼,再增加其他国家的人,sayHello函数将会变得非常庞大和难以维护。

从源头来看sayHello这个动作中要输出什么的逻辑是由不同类型的人定义的,所以应该将sayHello封装起来,作为不同类型的人sayHello的一种方法。这就属于一种面向对象,代码变成了一种可扩展,可生长的代码。修改,并加入俄罗斯人的代码:

var Chinese = function(){}
Chinese.prototype.sayHello = function(){
  console.log('你好')
}
var British = function(){}
British.prototype.sayHello = function(){
  console.log('Hello')
}
var Russian = function(){}
Russian.prototype.sayHello = function(){
  console.log('#&(*$(K')
}

var sayHello = function(man){
  man.sayHello() // 运行期多态
} 

sayHello(new Chinese()) // 编译期多态
sayHello(new British())
sayHello(new Russian())

在实现多态的同时,JavaScript中同样可以使用继承来实现类的多样性。比如我和MilkGao都是中国人,我们一般遇到其他人会说"你好"来打招呼,但是我们俩见面后因为一些其他的原因,打招呼的方式会不一样。我见到他会说:Hey,老哥。他见到我会说:哇,帅哥!

分析这段逻辑,两个人都是在跟特定的人打招呼(做同样的动作),两个人都是中国人,遇到陌生人都会说"你好",来打招呼。两个人不同的地方是,相互见面后打招呼的内容不同。所以可以都继承中国人来处理相同的打招呼逻辑,又有各自不同的遇到朋友的打招呼方法。

var Chinese = function(){}
Chinese.prototype.sayHello = function() {
  console.log('你好')
}

var YeeWang = function() {
  Chinese.call(this)
}
YeeWang.prototype = Object.create(Chinese.prototype)
YeeWang.prototype.constructor = YeeWang
YeeWang.prototype.sayHelloTo = function(man){
  if(man instanceof MilkGao) console.log("Hey,老哥!")
  else this.sayHello()
}

var MilkGao = function() {
  Chinese.call(this)
}
MilkGao.prototype = Object.create(Chinese.prototype)
MilkGao.prototype.constructor = MilkGao
MilkGao.prototype.sayHelloTo = function(man) {
  if(man instanceof YeeWang) console.log("哇,帅哥!")
  else this.sayHello()
}

var twoPersonSayHello = function(man1,man2){
  man1.sayHelloTo(man2) // 运行期多态
}


twoPersonSayHello(new YeeWang(),new MilkGao()) // 编译期多态
twoPersonSayHello(new MilkGao(),new YeeWang())

TypeScript

TypeScript

既然说JavaScript的面向对象,就不能不提TypeScript。
关于TypeScript的文档我就不具体介绍了,如果官网有详细的TypeScript的使用、规范说明。我列出了几点关于TypeScript相对于JavaScript的优势点和注意事项。

  • 首先TypeScript编码过程中要求对变量进行类型定义,比如在项目中一旦定义一个变量的类型后,如果赋值类型不同,在编译器中就会直接报错,这或许在你看来比起JavaScript这显得非常麻烦,但对于长期受益来讲这会显得非常有用。


    image
  • 自动提示,使用TypeScript定义好Class后,在使用过程中,都会有对这个类的自动提示,在编码过程中一路回车,体验真的不要太好!相比于之前使用JS时借助IDE一些插件实现的关键字自动检索,TypeScript的提示速度更快更准确!

    image

  • 参数提示,在使用TypeScript编码时,如果遇到陌生的方法,可以直接快速追溯到该方法的定义,迅速查找参数类型。比如在使用lodash中的方法函数,就可以快速查到findIndex中所需要到参数类型,以及返回类型。

    image

  • 定义文件(.d.ts),使用TypeScript一定要注意的一点是,如果引入非TypeScript写的库。发现import报错,那么很有可能该库没有更新配置TypeScript,目前大多数用到的库都已经有对TypeScript的支持包括Vue,React,Lodash等等,但还是有一些库官方并没有更新.d.ts的类型定义文件,对于这类文件TypeScript另外做了一个开源项目,专门整理各大库的定义文件。比如three这个库,如果要使用TypeScript,只需要运行npm i @types/three -D就可以匹配找到该库的类型定义文件啦。

模块化

一套优秀的系统源码,是文件多、还是文件大?

对于上面这个问题答案是肯定的,一套优秀的系统源码应该是尽可能将逻辑颗粒度细化,尽可能的抽象和模块化可以使业务代码变得相对较少。

究竟什么是模块化?其实在Vue/React中的组件,就属于模块化,每个组件都被抽象成为一个module暴露出来,在其他组件中被使用,并被框架按照自己的组件处理方式制作成最终业务效果。

以下代码都是在对外暴露一个模块。

export default { } // ES6
module.exports = {}  

静态加载与动态加载

  • 静态加载:在编译阶段进行,把所有需要的依赖打包到一个文件中
  • 动态加载:在运行时加载依赖

AMD标准是动态加载的代表,而CommonJS是静态加载的代表。
AMD的目的是用在浏览器上,所以是异步加载的。
而NodeJS是运行在服务器上的,同步加载的方式显然更容易被人接收,所以使用了CommonJS。

import Gallery from '@/views/Gallery' // 静态加载
const Gallery = () => import('@/views/Gallery') // 动态加载

为什么要使用模块化?

为什么要使用组件呢?

在很早以前(我还在做PHP的时候)和朋友在谈起Laravel框架时说到:恩,我觉得这个框架很强大,很多代码都是一个方法里面嵌套了很多其他方法,代码阅读起来非常舒服。朋友:我最讨厌这样的写法,一层嵌一层都不知道他在干什么。我:...

为什么要使用模块化?为了尽可能的少写代码。

使用模块化可以让我们在编写代码时,会"少写"很多代码。
我们在实现业务逻辑时可以尽可能的对代码复用,从而减少很多可能会出错的几率,增加开发效率和可维护性。

// 常量
export const HOST = "127.0.0.1"
export const HELLO_MSG = '你好'

// 方法
export function wait(time) {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}

// 类
export default class Vector2 {
  x=null
  y=null
  add(){}
  sub(){}
  distence(){}
}

UML

什么是UML

Unified Modeling Language (UML)又称统一建模语言或标准建模语言,是始于1997年一个OMG标准,它是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持,包括由需求分析到规格,到构造和配置。 面向对象的分析与设计(OOA&D,OOAD)方法的发展在80年代末至90年代中出现了一个高潮,UML是这个高潮的产物。它不仅统一了Booch、Rumbaugh和Jacobson的表示方法,而且对其作了进一步的发展,并最终统一为大众所接受的标准建模语言。

UML 实际上在前期设计项目数据模型时是非常有用的一套工具,个人认为在构造一个关联级超过3层以上的功能时,都应该针对这个功能抽象制作UML图,这样非常有利于后面的代码编写。正所谓,磨刀不费砍柴工。

UML类图关系

继承(Generalization)

指的是一个类继承另外一个类的功能,并可以增加自己的新功能的能力,继承是类与类或接口与接口之间最常见的关系。

image

实现(Realization)

指的是一个class类实现interface接口(可以是多个)的功能;实现是类与接口之间最常见的关系。

image

依赖(Dependency)

可以简单的理解,就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船的关系就是依赖。

image

关联(Association)

他体现的是两个类,或者类与接口之间语义级别的一种强依赖关系,比如我和我的朋友,双方关系是平等的。


image

聚合(Aggregation)

聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具体各自的什么周期,部分可以属于多个整体对象,也可以为多个整体对象共享;


image

组合(Composition)

组合也是关联关系的一种特征,他体现的是一种contains-a的关系,这种关系比聚合更强,也称强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;

image

UML的简单应用

说了这么多,主要是简单介绍一下UML最简单的一些类图关系定义。这个在画UML图、看UML图时都非常有用!如果不了解上面的这些箭头的含义,那么是很难理解UML类图的。

我举个例子,比如现在需要构建一个房子的全部数据。

一个房子需要些什么抽象模型?楼层,房间,墙,家具,吊顶,地板,踢脚线,窗口,门,墙角等等。

只看户型信息的话有哪些内容?楼层,房间,墙,墙中门、窗所需要的洞,墙面,吊顶,地板,每层的高度,地面、吊顶、墙面所需要的铺贴材质,材质铺贴的方向,墙的长、厚,墙洞的长宽。

家具这些需要什么内容?普通家具的长宽高、位置坐标和旋转方向,组合家具的长宽高、位置坐标和旋转方向。

面对这么多复杂的数据内容,我们必须要细化到每个类中才可以实现整体的House数据。我简单的做了一个UML图,不是很完善,但可以正常说明问题,请大家参考学习。

image

你可能感兴趣的:(面向对象和模块化)