写在前面
本文是《重构,有品位的代码》系列第四篇文章,将向大家介绍常用的封装手法对于重构的意义,以及其对于我们代码的提升。正如你所知道的,分解模块时的重要标准是对外界隐藏内部细节,而数据结构正是不可对外的核心秘密,此时可以使用封装记录或封装集合进行处理。还有更多封装手法,在本文中将逐一介绍…
前情回顾:
什么是封装
看到封装二字,我们通常会问什么是封装,函数和类是不是封装呢?答案是肯定,类和函数就是封装。在《JavaScript 高级程序设计(第4版)》中第80页提到:
函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。
简单的,就是通过将零散的语句代码放在函数的花括号中,作为函数体,有参数可以传入参数,最后通过函数名进行调用即可运行代码。
举个栗子:
let curDiv = document.getElementsByIdTagName("div")[0];
let p = document.create("p");
body.style.background = "green";
p.innerText = "添加的新内容";
curDiv.appendChild(p);
我们看到,这段代码每次被执行都会造成资源浪费,且会被同名变量覆盖。为此我们考虑到进行函数封装,可以随时调用。如下:
function createTag(newColor,newText){
let curDiv = document.getElementsByIdTagName("div")[0];
let p = document.create("p");
body.style.background = newColor;
p.innerText = newText;
curDiv.appendChild(p);
}
createTag("green","添加的新内容")
经过函数封装后的代码的复用性更高,可以达到按需执行的目的,避免全局变量污染。
常用的封装手法
在平时开发中,经常会在代码中使用到封装,但是并不知道是做了什么封装,现在我们将常用的封装手法进行总结如下:
- 封装记录
- 封装集合
- 以对象取代基本类型
- 以查询取代临时变量
- 提炼类
- 内联类
- 隐藏委托关系
- 移除中间人
- 替换算法
1. 封装记录
记录型结构是多数编程语言提供的一种常见特性,能够直观地组织存在关联的数据,让数据可以作为有意义的单元传递。简单的记录型结构的缺陷,在于需要清晰区分“记录中存储的数据”和“通过计算得到的数据”。
记录型结构有两种类型:
- 需要声明合法的字段名字
- 随便用任何字段名字。通常由语言库自身实现,通过类的形式提供,因此这些类也被称为散列、映射、散列映射、字典以及关联数组等。
通常的,对持有记录的变量进行封装变量*,并将其封装到函数中。而后,创建类将记录进行包装,将记录变量的值替换成该类的实例,再在类上定义访问函数get用于返回原始记录。新建函数返回该类的对象,而非原始数据记录。对于该记录的每处使用点,可以将原先返回记录的函数调用替换为返回实例对象的函数调用。
{
id:2021219001,
name:"wenbo",
height:"172cm",
weight:"65kg"
}
对于上面的json数据进行处理,可以将功能进行简单的封装。
class Person{
constructor(data){
this._name = data.name;
this._height = data.height;
this._weight = data.weight;
}
set name(arg){
this._name = arg.name;
}
get name(){
return this._name;
}
set height(arg){
this._height = arg.height;
}
get height(){
return this._height;
}
set weight(arg){
this._weight = arg.weight;
}
get weight(){
return this._weight;
}
}
2. 封装集合
通常的,在封装集合时常犯错误:只对集合变量的访问进行封装,但依然让取值函数返回集合本身,这使得集合的成员易被直接修改,封装类无法介入。
为避免在这种情况,可以在类上提供一些修改集合的方法,可以是添加和删除方法,使得对集合的修改都得通过类。
原始代码:
class Person{
get courses(){
return this._courses;
}
set courses(aList){
this._courses = aList;
}
}
重构代码:
class Person{
get courses(){
return this._courses.slice();
}
addCourse(aCourse){}
removeCourse(aCourse){}
}
3. 以对象取代基本类型
在开发初期,常常使用简单的数据项来表示简单的情况,但后期业务发展,数据项变得庞大且不便管理。此时可以创建新类进行简单数据类型的包装,再进行业务逻辑的封装。
具体的,对于未被封装的变量先要进行封装处理,并为这个数据值创建简单的类。而类的构造函数应该保存这些数据值,并对其提供对应的取值函数和设值函数。
4. 以查询取代临时变量
临时变量的一个作用是保存某段代码的返回值,以便在函数的后面可以使用它。临时变量可以允许引用之前的值,可以避免代码的重复计算,同时需要进一步抽取成函数方便复用。
当然,以查询取代临时变量手法只适用于处理某些类型的临时变量(只被计算一次且后面不再被修改的变量)。但是,在复杂代码中变量会被多次赋值,这样变量也就会被计算多次,那么应该把这些计算代码一块提炼到查询函数中。
具体的,先对变量进行检查在使用前是否完全计算完毕,检查计算它的那段代码是否每次都能得到同样的值。若变量目前不是只读的,但是可以改造成只读变量,则先需要对变量进行改造。将为变量赋值的代码段提炼成函数,再应用内联变量手法移除临时变量。
原始代码:
class Oreder{
constructor(quantity, item){
this._quantity = quantity;
this._item = item;
}
get price(){
let basePrice = this._quantity * this._item.price;
let discountFactor = 0.98;
if(basePrice > 1000) discountFactor; -= 0.03;
return basePrice * discountFactor;
}
}
重构代码:
class Oreder{
constructor(quantity, item){
this._quantity = quantity;
this._item = item;
}
get basePrice(){
return this._quantity * this._item.price;
}
get discountFactor(){
let discountFactor = 0.98;
if(basePrice > 1000) discountFactor -= 0.03;
return discountFactor;
}
get price(){
return tthis.basePrice * this.discountFactor;
}
}
5. 提炼类
在开发中有个约定俗称的规定:一个类应该有个清晰的抽象,根据功能需求进行拆分成独立的类。当某些数据和某些函数总是同时出现,某些数据经常同时变化甚至彼此相依,表示应当将这些代码进行抽取成子类。
通常的,在进行提炼类时需要先理清代码逻辑、分离责任,根据代码处理的业务功能进行抽取成子类。
原始代码:
class Person{
get officeAreaCode(){
return this._officeAreaCode;
}
get officeNumber(){
return this._officeNumber;
}
}
重构代码:
class Person{
get officeAreaCode(){
return this._phoneNumber.areaCode();
}
get officeNumber(){
return this._phoneNumber.number();
}
}
class PhoneNumber{
get areaCode(){
return this._areaCode;
}
get number(){
return this._number;
}
}
6. 内联类
内联类其实就是提炼类的反向重构,如果某个类的作用比较小,没有单独存在的理由,那么就可以将其作为另外的重要类的组成部分(子类)。
通常的,对于内联类中创建待内联类的函数,新创建的所有函数直接委托到待内联类。并修改待内联类中public方法所有的引用点,将其调用内联类对应的委托方法,待内联类中的函数与数据全部定义在内联类中。
原始代码:
class Person{
get officeAreaCode(){
return this._phoneNumber.areaCode();
}
get officeNumber(){
return this._phoneNumber.number();
}
}
class PhoneNumber{
get areaCode(){
return this._areaCode;
}
get number(){
return this._number;
}
}
重构代码:
class Person{
get officeAreaCode(){
return this._officeAreaCode;
}
get officeNumber(){
return this._officeNumber;
}
}
7. 隐藏委托关系
对于好的模块化设计而言,封装是最关键特征之一,可以使得各个模块和系统其它模块的联系减少。
正如在某些客户端可以先通过服务对象的字段得到另一个受委托的对象,要通过调用受委托类的函数就必须知道它们之间的委托关系,可以避免受委托类修改接口导致不可用。
// 原始代码
manager = aPerson.department.manager;
//重构代码
manager = aPerson.manager;
class Person{
get manager(){
return this.department.manager
}
}
8. 移除中间人
在隐藏委托关系中,封装受委托对象的代价是,每当客户端要使用受委托类的新特性时,你就必须在服务端添加一个简单委托函数。但是随着受委托类的功能越来越多,转发核函数也增多,让人脑阔疼,这样服务类就成了工作人,这样为啥不直接调用受委托类。
通常的,先为受委托类创建取值函数get,对于每个委托函数,让其客户端转为连续的访问函数调用。其实就是隐藏委托关系的反向重构。
// 原始代码
manager = aPerson.manager;
class Person{
get manager(){
return this.department.manager
}
}
//重构代码
manager = aPerson.department.manager;
9. 替换算法
在进行算法替换时,需要对大型的原始函数进行深入理解并进行拆分成小函数进行处理,保证待替换的算法被抽取到独立的函数中,这样只需要测试此部分算法的行为即可。
对于每个委托关系中的函数,在服务对象端建立一个简单的委托关系,调整客户端令它只调用服务对象提供的函数。如果将来不再有任何客户端需要使用delegate(受委托类),即可移出服务对象中的访问函数。
原始代码:
function foundPerson(people){
for(let i = 0; j < people.length; i++){
if(people[i] === "wenbo"){
return "wenbo";
}
if(people[i] === "shunshun"){
return "shunshun";
}
if(people[i] === "xiaofan"){
return "xiaofan";
}
}
return "";
}
重构代码:
function foundPeron(people){
const students = ["wenbo","shunshun","xiaofan"];
return people.find(p=>students.includes(p) || "");
}
小结
本篇文章介绍了封装的定义概念和常见的9种封装方法,能够帮助我们的代码变得更加有品位,更加整洁,远离坏味道。
参考文章
《重构──改善既有代码的设计(第2版)》
写在最后
我是前端小菜鸡,感谢大家的阅读,我将继续和大家分享更多优秀的文章,此文参考了大量书籍和文章,如果有错误和纰漏,希望能给予指正。
更多最新文章敬请关注笔者掘金账号一川萤火和公众号前端万有引力。## 写在前面