封装的目的是将信息隐藏。一般而言,我们讨论的封装是封装数据和封装实现。这一节将讨论更广义的封装,不仅包括封装数据和封装实现,还包括封装类型。
本文主要包含以下知识点:
- 封装数据
- 封装实现
- 封装类型
- 封装变化
封装数据
在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 private、public、protected 等关键字来提供不同的访问权限。
但 JavaScript 并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 public 和 private 这两种封装性。
下面是一个 JavaScript 中实现封装的示例:
var Computer = function (name, price) {
this.name = name;
var _price = price; // 封装 _price 属性,被封装的属性采用_开头是一个不成文的规定
// 内部方法可以访问到 _price 的值
this.showPrice = function () {
console.log(`这台电脑的价格为${_price}元`)
}
}
Computer.prototype.showSth = function () {
console.log(`这是一台${this.name}电脑,价格为${this._price}元`);
}
var apple = new Computer("苹果", 12000);
console.log(apple.name); // 苹果
console.log(apple._price); // undefined
apple.showSth(); // 这是一台苹果电脑,价格为undefined元
apple.showPrice(); // 这台电脑的价格为12000元
在上面的代码中,我们通过函数作用域的方式来封装了属性 price,因为没有挂在 _this 对象上面,所以外部是无法访问到该变量值的。
封装实现
上一节描述的封装,指的是数据层面的封装。
有时候我们喜欢把封装等同于封装数据,但这是一种比较狭义的定义。
封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。
封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。
封装实现细节的例子非常之多。拿迭代器来说明,迭代器的作用是在不暴露一个聚合对象的内部表示的前提下,提供一种方式来顺序访问这个聚合对象。
我们编写一个 each 函数,它的作用就是遍历一个可迭代对象,使用这个 each 函数的人不用关心它的内部是怎样实现的,只要它提供的功能正确便可以。
// 为 Array 原型上封装了一个 each 方法
Array.prototype.each = function (callback) {
// this 指向调用 each 方法的数组
for (let i = 0; i < this.length; i++) {
callback(this[i]);
}
}
let arr = [1, 2, 3, 4, 5];
// 对于外部来讲,不关心 each 具体的实现
// 只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现
arr.each(function (i) {
console.log(i);
});
修改 each 的实现,对外部调用没有任何影响,如下:
// 为 Array 原型上封装了一个 each 方法
Array.prototype.each = function (callback) {
// this 指向调用 each 方法的数组
for (let i of this) {
callback(i)
}
}
let arr = [1, 2, 3, 4, 5];
// 对于外部来讲,不关心 each 具体的实现
// 只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现
arr.each(function (i) {
console.log(i);
});
在上面的代码中,即使 each 函数修改了内部源代码,只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现的改变。
封装类型
封装类型是静态类型语言中一种重要的封装方式。
一般而言,封装类型是通过抽象类和接口来进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。例如我们之前所书写的 Java 代码:
// 抽象类,用于隐藏具体对象的类型信息
// 所以可以看作是对对象类型的封装
abstract class Animal {
abstract void makeSound(); // 抽象方法
}
// 鸡类继承 Animal 类
class Chicken extends Animal {
public void makeSound() {
System.out.println("咯咯咯");
}
}
// 鸭子类继承 Animal 类
class Duck extends Animal {
public void makeSound() {
System.out.println("嘎嘎嘎");
}
}
class AnimalSound {
// 接受 Animal 类型的参数
public void makeSound(Animal animal) {
animal.makeSound();
}
}
public class Test {
public static void main(String args[]) {
AnimalSound animalSound = new AnimalSound();
Animal duck = new Duck();
Animal chicken = new Chicken();
animalSound.makeSound(duck); // 嘎嘎嘎
animalSound.makeSound(chicken); // 咯咯咯
}
}
在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使如工厂方法模式、组合模式等模式诞生的原因之一。
当然在 JavaScript 中,并没有对抽象类和接口的支持。JavaScript 本身也是一门类型模糊的语言。在封装类型方面,JavaScript 没有能力,也没有必要做得更多。
封装变化
从设计模式的角度出发,封装在更重要的层面体现为封装变化。
《设计模式》一书中共归纳总结了 23 种设计模式。从意图上区分,这 23 种设计模式分别被划分为创建型模式、结构型模式和行为型模式。
拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。
通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。
-EOF-