封装的理解

背景

最近在学习 canvas 的使用,并写了个 demo,就是做一个 K 线图,支持提示、拖拽、缩放,也发布为 npm 包:echarts-for-abc
效果如图所示
pic.1708325742371
写代码过程中,封装是必不可少的一部分,所以想以这个为例子来讲一下封装。

目的

封装的目的,是为了减少代码量。

定义

万物皆对象,对象具有属性和行为(方法),对象公共属性和行为的提取就是封装。

理念

封装的前提,必定跟场景相关联的,也就是说,先有场景,再有封装。封装前,先把场景下的属性具象化出来,再考虑提取他们的共性。如果跳过这一步直接思考他们的共性,这很容易出现思考偏差。

举个例子:一个学校里面,有老师、学生、校长等各角色,不同角色有自己的特点和行为,比如学生有学号,老师有职位等级、类型,他们也有共同的特性,比如拥有性别、年龄等特点,用类(class)来表示各个角色。

将学生和老师用代码抽象化表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student {
constructor(studentNumber, sex, age) {
this.studentNumber = studentNumber; // 学号
this.sex = sex;
this.age = age;
}
}

class Teacher {
constructor(rank, sex) {
this.rank = rank; // 等级
this.sex = sex;
this.age = age;
}
}

发现学生和老师都有性别和年龄,那就可以提取出来,所以可以很容易用Person封装起来,

1
2
3
4
5
6
class Person {
constructor(sex, age) {
this.sex = sex;
this.age = age;
}
}

然后就可以使用extends让子类(学生)继承父类(Person)的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Student extends Person {
constructor(studentNumber, sex, age) {
super(sex, age);
// super() 表示执行父类构造函数,相当于Person.prototype.constructor.call(this), 用于继承父类Person的属性
// 在这里,相当于this.sex = undefined; this.age = undefined
// 传入sex,age 相当于 this.sex = sex; this.age = age;

this.studentNumber = studentNumber;
}
}

const student1 = new Student('330311221', '男', 15);
// student1 = { age: 15, sex: "男", studentNumber: "330311221" }

class Teacher extends Person {
constructor(rank, sex, age) {
super(sex, age);
this.rank = rank;
}
}

const teacher1 = new Teacher('高级教师', '女', 35);
// teacher1 = { age: 35, sex: "女", rank: "高级教师" }

封装颗粒度

封装颗粒度表示函数的拆分程度,是否越细越好呢,其实不是。
在项目中,如果组件拆的过于细,可能会导致父组件的参数向子组件一层层传递时,会出现遗漏,传错等问题。如果拆的太粗,会导致难以复用、难以维护等问题,那么怎样的颗粒度大小才算合适呢?封装的颗粒度大小,取决于不同场景下的偏向性考虑。

考虑偏向性

封装偏向性可以分为两种,偏应用还是偏底层。

偏应用:这个封装只适用你这个特定的场景
偏底层:具有独立性,颗粒度更小,可以脱离特定场景,就像工具方法,如 Lodash 工具库

工具方法:是一个纯函数,传入参数,返回结果。也就是说不要在工具方法内部获取外部变量,要作为参数传入。

考虑哪种偏向性,取决于场景,所以进入“写一个 K 线图 demo”的场景,来聊一聊封装。

举例说明

可以看到图中的蜡烛出现了 10 次,那么如何封装渲染蜡烛的这个函数呢?是封装单个蜡烛合适还是一串蜡烛合适?

如果作为单个蜡烛的出现,我们只需要知道蜡烛横坐标、蜡烛宽度、颜色以及四个点的纵坐标就能绘制出蜡烛,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 绘制蜡烛
* @param {number} abscissa 蜡烛横坐标
* @param {number} topPointY 最高点纵坐标
* @param {number} bottomPointY 最低点纵坐标
* @param {number} secondPointY 第二个点纵坐标
* @param {number} thirdPointY 第三个点纵坐标
* @param {number} candleW 蜡烛宽度
* @param {string} candleColor 蜡烛颜色
*/
function renderCandle(
abscissa,
topPointY,
bottomPointY,
secondPointY,
thirdPointY,
candleW,
candleColor,
) {
const halfCandleW = candleW / 2;

// 绘制蜡烛上影线
ctx.beginPath();
ctx.moveTo(abscissa, topPointY);
ctx.lineTo(abscissa, secondPointY);
ctx.closePath();
ctx.stroke();

// 绘制蜡烛下影线
ctx.beginPath();
ctx.moveTo(abscissa, bottomPointY);
ctx.lineTo(abscissa, thirdPointY);
ctx.closePath();
ctx.stroke();

// 绘制蜡烛实体(中间矩形部分)
ctx.beginPath();
ctx.moveTo(abscissa - halfCandleW, secondPointY);
ctx.rect(
abscissa - halfCandleW,
secondPointY,
candleW,
thirdPointY - secondPointY,
);
ctx.fillStyle = candleColor;
ctx.fill();
}

renderCandle(50, 88, 50, 44, 33, 20, 'red');

pic.1708325756628
这个函数的封装偏向性就属于偏底层封装,它具有独立性,颗粒度更小,复用率更高,就像在 Echarts 图标库里,有多个应用到单个蜡烛的地方,就可以使用这个工具函数,你只需要传入相关参数即可。

但在我这个练习 demo 中,单个蜡烛并没有独立出现的场景,我考虑偏应用封装,而且在渲染蜡烛之前我需要处理数据源、判断涨跌以及蜡烛颜色的情况,所以将这些处理逻辑和绘制蜡烛封装为一个函数,不管在阅读还是使用上都更为方便,我的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 绘制一串蜡烛
* @param {array} data 数据源
* @param {number} candleW 蜡烛宽度
*/
function renderCandles(data, candleW) {
// 将数据源转换为绘制蜡烛所需要的纵坐标集合
const dataYAxisPoint = tranPriceToOrdinate(data);
const halfCandleW = candleW / 2;

for (let i = 0, candleLength = dataYAxisPoint.length; i < candleLength; i++) {
const { heightPrice, lowPrice, openingPrice, closingPice } =
dataYAxisPoint[i];
let abscissa = xAxisTickPointX(i),
topPointY = heightPrice,
bottomPointY = lowPrice,
secondPointY,
thirdPointY,
candleColor;

if (closingPice < openingPrice) {
// 涨
candleColor = 'red';
secondPointY = closingPice;
thirdPointY = openingPrice;
} else {
candleColor = 'green';
secondPointY = openingPrice;
thirdPointY = closingPice;
}

// 绘制蜡烛上影线
ctx.beginPath();
ctx.moveTo(abscissa, topPointY);
ctx.lineTo(abscissa, secondPointY);
ctx.closePath();
ctx.stroke();

// 绘制蜡烛下影线
ctx.beginPath();
ctx.moveTo(abscissa, bottomPointY);
ctx.lineTo(abscissa, thirdPointY);
ctx.closePath();
ctx.stroke();

// 绘制蜡烛实体(中间矩形部分)
ctx.beginPath();
ctx.moveTo(abscissa - halfCandleW, secondPointY);
ctx.rect(
abscissa - halfCandleW,
secondPointY,
candleW,
thirdPointY - secondPointY,
);
ctx.fillStyle = candleColor;
ctx.fill();
}
}

总结

  • 封装的理念为:把场景下的属性和方法具象化,提取共性
  • 封装的颗粒度大小,取决于不同场景下的偏向性考虑
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×