如何在前端代码中,应用面向对象的编程范式?

为何要面向对象?

你须要知道的面向对象

面向对象并非针对一种特定的语言,而是一种编程范式。可是每种语言在设计之初,都会强烈地支持某种编程范式,好比面向对象的Java,而Javascript并非强烈地支持面向对象。html

何时须要面向对象?

任何一名开发人员,在编写具体的代码的时候,不该该为了套用某种编程范式,而去编写代码和改造代码。任何编写方式的目的是:前端

  • 让代码逻辑清晰
  • 可读性良好
  • 没有冗余代码

前端编写过程当中何时须要面向对象?

在个人平常工做中,最不想作的的就是两点:es6

  • 复制粘贴代码
  • 不一样的代码中具有相同的逻辑或者变量

由于这两种方式,会让代码冗余,并且不易维护。为何?编程

由于相同的代码,具有相同的逻辑,也就是具有相同的业务逻辑场景,若是场景一旦改变,你将会改变两处代码。

ok,到这里,咱们来说一个具体的业务场景。redux

场景1: 前端须要显示工人的工做完成状态,若是已经完成了,前端提供一个查看详情的入口,若是没有完成,提供工人去完成任务的入口。后端传递过来显示工人完成状态的字段:user_done_status:0,表明未完成,1表明已完成。前端须要实现这样一个表格:
工人名字 完成状态 操做
小王 已完成 查看详情
老王 未完成 去完成

阶段一:实现最基本的功能

// status.js
// 1:须要一个状态映射表,来实现第二列的功能
export const statusMap = new Map([
  [0, '未完成'],
  [1, '已完成']
]);
// 2: 须要一个动做映射表,来实现第三列的功能
export const actionMap = new Map([
  [0, '查看详情'],
  [1, '去完成']  
]);
// 3: 须要一个状态判读函数,来实现第三列的功能
function isUserDone(status) {
  return +status === 1;
}

const actionMap = new Map([
  [status => isUserDone(status), userCanCheckResult],
  [status => !isUserDone(status), needUserToCompoleteWork]
]);

function handleClick() {
  for (let [done, action] of actionMap) {
    if (done()) {
      actionMap();
      return;
    }
  }
}

至于第三个为何这么写,能够看一下这篇文章后端

阶段二:坏代码的味道

上面的三段代码单独写出来没啥问题,看看下面的可能问题就出来,这至关于实现了三个函数,那么须要在显示在表格中就须要这样写:异步

import {
  statusMap,
  actionMap,
  getUserAction
} from './status.js'

.... ....
// 第二列
return (
  <span>
    {
      statusMap.get(status)
    }
  </span>
);
// 第三列
return (
  <span onClick={() => getUserAction(status)}>
    actionMap.get(status)
  </span>
);

这样的写法,看起来没啥问题,可是可读性是不好的,主要体如今两点:函数

  • 三个函数都和status相关,可是展示形式上是割裂的
  • 每一个函数都须要传递一个status

可能有的人会说,这样把上面的代码单独抽离出一个文件,也没什么问题,状态也是比较集中的,嗯,这种说法也没什么问题,单独提取一个文件,用做处理用的状态,是一种常见的抽象方法。可是可能会遇到下面集中状况,就会让你很难受:post

  • 后端改了下字段,那么你就须要在阶段二中的第二列和第三列中传入参数的地方修改对应的字段名字(估计想宰了rd吧)
  • 业务场景变化,工人的任务状态,添加了其余限制,好比任务的时间限制,任务有未开始、进行中、已过时三种状态,只有当在任务进行中的时候,才能够展现用户的状态,不然就展现未开始或者已过时,总结起来,须要下面的几种状态:this

    • 未开始
    • 已完成/未完成
    • 已过时

那么显然,你就须要修改代码的逻辑,仅仅依靠一个statusMap就不能行了。固然这里有人说了,那我把map编程一个函数:

const getUserStatus = (status, startTime, endTime) => {
  // ...do something
}

这样是否是就能够了,嗯,说的也没什么问题,那你须要去修改以前写的全部代码,传入不一样的参数,就算一开始你用的不是map而是函数,那么你的代码也须要再传入两个多余的参数,start_time和end_time。

须要解决的痛点:

  • 展示形式的分离,须要一种集中的状态处理
  • 须要传入多个参数进行判断,业务场景的变化或者字段的变化,都须要多处修改代码

最开始遇到这来那个问题的时候,我想的是怎么样可以把全部的处理集中到一块儿,天然而然就想到了面向对象,将用户的状态做为一个对象,对象具有特定的属性和对应的操做行为。

Javascript中如何编写面向对象的代码?

先睹为快,咱们看一下,上面的代码在面向对象的写法,直接使用es6的class

上面业务场景的面向对象的写法

import moment from 'moment';

class UserStatus {
  constructor(props) {
    const keys = [
      user_done_status,
      start_time,
      end_time
    ] ;
    for (let key of keys) {
      this.[`_${key}] = (props || {})[key];
    }
  }

  static StatusMap = new Map([
    [0, '未完成'],
    [1, '已完成']
  ]);

  static TimeMap = newMap([
    [0, '未开始'],
    [1, '已过时']
  ]);

  get userDoneStatus () {
    return this._user_done_status;
  }

  get isInWorkingTime() {
    const now = new Date();
    return moment(now).isBetween(moment(this._start_time), moment(this._end_time));
  }

  get isWorkStart() {
    const now = new Date();
    return moment(now).isAfter(moment(now));
  }

  get userStatus () {
    if (this.isInWorkingTime) {
      return UserStatus.StatusMap.get(this.userDoneStatus);
    } else {
      return UserStatus.TimeMap.get(+this.isWorkStart);
    }
  }
  ... ...
  // 省略其余的了
}

那么写好了上面的类,咱们应该在其余地方怎么引用呢?

// 第一步:直接讲后端传过来的信息,构造一个新的对象
const userInfo = new UserStatus(info);

// 第二步:直接调用对应的方法或者参数

return (
  <span>
    {
      userInfo.userStatus
    }
  </span>
);

之后不管业务场景如何改变这部分代码都不须要从新改写,只须要改写对应的类的操做就能够了。
这样看了比较干净的是具体的view层代码,就是简单的html和对应的数据,没有其余操做。其实这就是如何消除代码反作用的问题:将反作用隔离。当你把全部的反作用隔离以后,代码看起来干净许多,你像redux-saga就是将对应的异步操做隔离出来。

ok,看了上面的类的写法,咱们来看一下面向对象的写法应该要怎么写:

面向对象

面向对象的三大特性

  • 封装
  • 继承
  • 多态
特性 特色 举例
封装 封装就是对具体的属性和实现细节进行隐藏,造成统一的的总体对外部提供对应的接口 上面的例子就是很好的解释
继承 继承就是子类能够继承父类的属性和行为,也能够重写父类的行为 好比工人有用户状态,老板也有用户状态,他们均可以继承UserStatus这一个基类
多态 同一个行为在在不一样的调用方式下,具有不一样的行为,依赖于抽象和重写 好比工人和老板都具有一个行为那就是吃饭,工人吃的是馒头,老板吃的是海鲜,一样是吃这个行为,产生了不一样的表现形式

封装对象的几个原则

在基本的面向对象中有几个原则SOLID原则,可是这里我不想详细写了,想说一下,我在封装对象的时候会注重的几个方面

  • 基类与具体数据无关,只封装了特定的行为和属性,基类只注重抽象公共的部分
  • 类的行为对扩展是开放的,可是对于修改是不开放的(开放封闭原则),像上面的写法是存在风险的,由于生成的对象实例中的属性能够被随意的修改,我加了_,就是防止这种行为,可是最好的方式应该是使用get/set方法来对属性限制操做;对于对象的属性,必定要明确,由于js中一个是没有类型的限制不要出现下面的写法:
class Base {
  constructor(props) {
    for (let key of props) {
      this[key] = props[key];
    }
  }
}
  • 一个类只应该依赖于他继承的类,不能依赖于其余类,这样能最大限度地减小耦合

注意的问题

注意⚠️在js中必定小当心this的使用,假设有一个初始类:
初始类:

class Base {
    constructor(props) {
        this._a = props.a;
    }

    status() {
        return this._a;
    }
}

避免下面的行为:

// 方式1:
let { status } = new Base({a: 678});
status() // 会报错

而应该使用下面的写法:

//方式2:
let info = new Base({a: 678});
info.status(); //输出正确

根本缘由就是this在做怪,第一种this指向了全局做用域。

最后也是最重要的

上面的面向对象主要解决了前文提到的两个痛点,可是也不是全部的业务场景都适合面向对象,当你的代码出现了一些坏味道(代码容易、代码分散不易处理),能够考虑下面向对象,毕竟适合的才是最好的

参考资料

面向对象封装的五个原则)
五个原则比较形象的解释

相关文章
相关标签/搜索