Erosvan 2019-11-04
面向对象并不是针对一种特定的语言,而是一种编程范式。但是每种语言在设计之初,都会强烈地支持某种编程范式,比如面向对象的Java,而Javascript并不是强烈地支持面向对象。
任何一名开发人员,在编写具体的代码的时候,不应该为了套用某种编程范式,而去编写代码和改造代码。任何编写方式的目的是:
在我的日常工作中,最不想做的的就是两点:
因为这两种方式,会让代码冗余,而且不易维护。为什么?
因为相同的代码,具备相同的逻辑,也就是具备相同的业务逻辑场景,如果场景一旦改变,你将会改变两处代码。
ok,到这里,我们来讲一个具体的业务场景。
场景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> );
这样的写法,看起来没啥问题,但是可读性是很差的,主要体现在两点:
可能有的人会说,这样把上面的代码单独抽离出一个文件,也没什么问题,状态也是比较集中的,嗯,这种说法也没什么问题,单独提取一个文件,用作处理用的状态,是一种常见的抽象方法。但是可能会遇到下面集中情况,就会让你很难受:
业务场景变化,工人的任务状态,添加了其他限制,比如任务的时间限制,任务有未开始、进行中、已过期三种状态,只有当在任务进行中的时候,才可以展示用户的状态,否则就展示未开始或者已过期,总结起来,需要下面的几种状态:
那么显然,你就需要修改代码的逻辑,仅仅依靠一个statusMap就不能行了。当然这里有人说了,那我把map编程一个函数:
const getUserStatus = (status, startTime, endTime) => { // ...do something }
这样是不是就可以了,嗯,说的也没什么问题,那你需要去修改之前写的所有代码,传入不同的参数,就算一开始你用的不是map而是函数,那么你的代码也需要再传入两个多余的参数,start_time和end_time。
最开始遇到这来那个问题的时候,我想的是怎么样能够把所有的处理集中到一起,自然而然就想到了面向对象,将用户的状态作为一个对象,对象具备特定的属性和对应的操作行为。
先睹为快,我们看一下,上面的代码在面向对象的写法,直接使用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原则,但是这里我不想详细写了,想说一下,我在封装对象的时候会注重的几个方面
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指向了全局作用域。
上面的面向对象主要解决了前文提到的两个痛点,但是也不是所有的业务场景都适合面向对象,当你的代码出现了一些坏味道(代码容易、代码分散不易处理),可以考虑下面向对象,毕竟适合的才是最好的