美文网首页
面向对象和模块化

面向对象和模块化

作者: 王大白_ | 来源:发表于2018-09-15 00:04 被阅读317次

    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。

    <!--component 1-->
    
    <template>
        <h1></h1>
    </template>
    
    <script>
    import Vue from 'vue'
    import Component from 'vue-class-component'
    
    @Component()
    export default class Test1 extends Vue {
      // initial data
      msg = 123
    
      // use prop values for initial data
      helloMsg = 'Hello, ' + this.propMessage
    
      // lifecycle hook
      mounted () {
        this.greet()
      }
    
      // computed
      get computedMsg () {
        return 'computed ' + this.msg
      }
    
      // method
      greet () {
        alert('greeting: ' + this.msg)
      }
    }
    </script>
    
    <style scoped>
    
    </style>
    

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

    <!--component 2-->
    <script>
    import Test1 from './Test1'
    import Component from 'vue-class-component'
    
    @Component()
    export default class Test2 extends Test1 {
      mounted() { 
        super.mounted()
      }
    }
    </script>
    

    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

    相关文章

      网友评论

          本文标题:面向对象和模块化

          本文链接:https://www.haomeiwen.com/subject/hftzgftx.html