封装的目的是将信息隐藏。一般而言,我们讨论的封装是封装数据和封装实现。这一节将讨论更广义的封装,不仅包括封装数据和封装实现,还包括封装类型。

本文主要包含以下知识点:

  • 封装数据
  • 封装实现
  • 封装类型
  • 封装变化

封装数据

在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 private、public、protected 等关键字来提供不同的访问权限。

JavaScript 并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 publicprivate 这两种封装性。

下面是一个 JavaScript 中实现封装的示例:

  1. var Computer = function (name, price) {
  2. this.name = name;
  3. var _price = price; // 封装 _price 属性,被封装的属性采用_开头是一个不成文的规定
  4. // 内部方法可以访问到 _price 的值
  5. this.showPrice = function () {
  6. console.log(`这台电脑的价格为${_price}元`)
  7. }
  8. }
  9. Computer.prototype.showSth = function () {
  10. console.log(`这是一台${this.name}电脑,价格为${this._price}元`);
  11. }
  12. var apple = new Computer("苹果", 12000);
  13. console.log(apple.name); // 苹果
  14. console.log(apple._price); // undefined
  15. apple.showSth(); // 这是一台苹果电脑,价格为undefined元
  16. apple.showPrice(); // 这台电脑的价格为12000元

在上面的代码中,我们通过函数作用域的方式来封装了属性 price,因为没有挂在 _this 对象上面,所以外部是无法访问到该变量值的。

封装实现

上一节描述的封装,指的是数据层面的封装。

有时候我们喜欢把封装等同于封装数据,但这是一种比较狭义的定义。

封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。

从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。

封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。

封装实现细节的例子非常之多。拿迭代器来说明,迭代器的作用是在不暴露一个聚合对象的内部表示的前提下,提供一种方式来顺序访问这个聚合对象。

我们编写一个 each 函数,它的作用就是遍历一个可迭代对象,使用这个 each 函数的人不用关心它的内部是怎样实现的,只要它提供的功能正确便可以。

  1. // 为 Array 原型上封装了一个 each 方法
  2. Array.prototype.each = function (callback) {
  3. // this 指向调用 each 方法的数组
  4. for (let i = 0; i < this.length; i++) {
  5. callback(this[i]);
  6. }
  7. }
  8. let arr = [1, 2, 3, 4, 5];
  9. // 对于外部来讲,不关心 each 具体的实现
  10. // 只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现
  11. arr.each(function (i) {
  12. console.log(i);
  13. });

修改 each 的实现,对外部调用没有任何影响,如下:

  1. // 为 Array 原型上封装了一个 each 方法
  2. Array.prototype.each = function (callback) {
  3. // this 指向调用 each 方法的数组
  4. for (let i of this) {
  5. callback(i)
  6. }
  7. }
  8. let arr = [1, 2, 3, 4, 5];
  9. // 对于外部来讲,不关心 each 具体的实现
  10. // 只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现
  11. arr.each(function (i) {
  12. console.log(i);
  13. });

在上面的代码中,即使 each 函数修改了内部源代码,只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现的改变。

封装类型

封装类型是静态类型语言中一种重要的封装方式。

一般而言,封装类型是通过抽象类和接口来进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。例如我们之前所书写的 Java 代码:

  1. // 抽象类,用于隐藏具体对象的类型信息
  2. // 所以可以看作是对对象类型的封装
  3. abstract class Animal {
  4. abstract void makeSound(); // 抽象方法
  5. }
  6. // 鸡类继承 Animal 类
  7. class Chicken extends Animal {
  8. public void makeSound() {
  9. System.out.println("咯咯咯");
  10. }
  11. }
  12. // 鸭子类继承 Animal 类
  13. class Duck extends Animal {
  14. public void makeSound() {
  15. System.out.println("嘎嘎嘎");
  16. }
  17. }
  18. class AnimalSound {
  19. // 接受 Animal 类型的参数
  20. public void makeSound(Animal animal) {
  21. animal.makeSound();
  22. }
  23. }
  24. public class Test {
  25. public static void main(String args[]) {
  26. AnimalSound animalSound = new AnimalSound();
  27. Animal duck = new Duck();
  28. Animal chicken = new Chicken();
  29. animalSound.makeSound(duck); // 嘎嘎嘎
  30. animalSound.makeSound(chicken); // 咯咯咯
  31. }
  32. }

在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使如工厂方法模式、组合模式等模式诞生的原因之一。

当然在 JavaScript 中,并没有对抽象类和接口的支持。JavaScript 本身也是一门类型模糊的语言。在封装类型方面,JavaScript 没有能力,也没有必要做得更多。

封装变化

从设计模式的角度出发,封装在更重要的层面体现为封装变化。

《设计模式》一书中共归纳总结了 23 种设计模式。从意图上区分,这 23 种设计模式分别被划分为创建型模式结构型模式行为型模式

image.png

拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。

通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。

-EOF-