原文: The Four Pillars of Object-Oriented Programming

JavaScript 是一种多范式语言,可以按照不同的编程范式进行编写。编程范式本质上就是你在写代码时遵循一些规则,以帮助你解决一个特定的问题。

这就是四大支柱的含义。它们是软件设计原则,帮助你编写整洁的面向对象的代码。

面向对象编程的四大支柱是:

  • 抽象
  • 封装
  • 继承
  • 多态

让我们仔细看看这些支柱。

面向对象编程中的抽象

抽象的意思是把实现的细节隐藏在某个东西里面——有时是一个原型,有时是一个函数。因此,当你调用这个函数时,你不需要确切地了解它在做什么。

如果你必须了解一个大代码库中的每一个函数,你将永远无法编码。要花几个月的时间才能读完它。

你可以通过抽象化某些细节来创建一个可重用的、简单易懂的、容易修改的代码库。让我给你举个例子:

function hitAPI(type){
	if (type instanceof InitialLoad) {
		// 实施示例
	} else if (type instanceof NavBar) {
		// 实施示例
	} else {
		// 实施示例
	}
}
这完全没有被抽象

你能从这个例子中看到,你如何准确地实现你的自定义用例所需要的东西吗?

你需要的每一个新的 API 都需要一个新的 if 块,以及它自己的自定义代码。这不是抽象的,因为你需要为你添加的每一个新类型担心实现。这不是可重用的,而且是一个维护的恶梦。

像下面这样的方法怎么样?

hitApi('www.kealanparr.com', HTTPMethod.Get)

你现在只需向你的函数传递一个 URL 和你想使用的 HTTP 方法就可以了。

你不必担心这个函数如何工作。它已经被解决了。这极大地有利于代码重用!同时也使你的代码更容易维护。

这就是抽象的意义所在。在你的代码中找到相似的东西,并提供一个通用的函数或对象来服务于多个地方/多个关注点。

这里还有一个很好的抽象的例子:想象一下,如果你正在创建一台机器,为你的用户制作咖啡,可能有两种方法:

如何在有抽象的情况下创建按钮

  • 有一个标题为“制作咖啡”的按钮

如何在没有抽象的情况下创建按钮

  • 有一个标题为“烧水”的按钮
  • 有一个标题为“向水壶中加入冷水”的按钮
  • 有一个标题为“向干净的杯子中加入一勺咖啡粉”的按钮
  • 有一个标题为“清洁任何脏杯子”的按钮
  • 以及所有其他的按钮

这是一个非常简单的例子,但第一种方法将逻辑抽象到机器中去了,第二种方法迫使用户了解如何制作咖啡,并且基本上是自己制作。

下一个支柱向我们展示了一种我们可以实现抽象的方法,即使用封装

面向对象编程中的封装

封装的定义是“将某物封闭在或像封闭在一个胶囊中的行为”。移除对部分代码的访问并使之成为私有的东西正是封装的意义所在(很多时候,人们把它称为数据隐藏)。

封装意味着你代码中的每个对象都应该控制自己的状态。状态是你的对象的当前“快照”,例如键、你的对象上的方法、布尔属性等等。如果你要重置一个布尔值或从对象中删除一个键,它们都是对你状态的改变。

限制你的代码中哪些部分可以访问。如果不需要的话,让更多的东西无法访问。

在 JavaScript 中,私有属性是通过使用闭包来实现的。下面是一个例子:

var Dog = (function () {

	// 私有
	var play = function () {
		// 执行代码
	};
    
	// 私有
	var breed = "Dalmatian"
    
	// 公有
	var name = "Rex";

	// 公有
	var makeNoise = function () {
 		return 'Bark bark!';
	};

 	return {
		makeNoise: makeNoise,
		name: name
 	};
})();

我们做的第一件事是创建一个立即被调用的函数(Immediately Invoked Function Expression,简称 IIFE)。这创建了一个任何人都可以访问的对象,但隐藏了一些细节。你不能调用 play,也不能访问 breed,因为我们没有在最后的对象中用 return 暴露它。

上面这个特殊的模式被称为揭示模块模式(Revealing Module Pattern),但它只是一个你如何实现封装的例子。

我想更多地关注封装的思想(因为它比仅仅学习一个模式更重要)。

反思一下,多想想如何能把你的数据和代码藏起来,把它分开来。模块化和明确责任是面向对象的关键。

我们为什么要喜欢私有?为什么不把所有的东西都变成全局的呢?

  • 很多不相关的代码将通过全局变量成为相互依赖/耦合的对象。
  • 如果名字被重复使用,你很可能会覆盖这些变量,这可能会导致错误或不可预测的行为。
  • 你很可能最终会产生意大利面条式的代码——很难推理出是什么在读写你的变量和改变状态。

封装可以通过将长行代码分离成较小的独立函数来应用。将这些函数分离成模块。我们把数据隐藏在一个没有其他需要访问的地方,并显示出需要的东西。

这就是封装。将你的数据绑定到某个东西上,无论是类、对象、模块还是函数,并尽你所能保持它的私有性。

面向对象编程中的继承

继承让一个对象获得另一个对象的属性和方法。在 JavaScript 中,这是通过原型继承完成的。

主要的好处是可重用性。我们知道,有时多个地方需要做同样的事情,而且除了一个小部分之外,它们需要做的事情都是一样的。这就是继承可以解决的问题。

每当我们使用继承时,我们都会努力使父代和子代的代码具有高关联度。例如, Bird 类型是否从 DieselEngine 类型中延伸出来?

保持你的继承简单易懂,可以预测。不要因为有一个你需要的方法或属性而从完全不相关的地方继承。继承并不能很好地解决这个特殊问题。

使用继承时,你应该需要大部分的功能(你不一定需要所有的功能)。

开发人员有一个原则,叫做里氏替换原则(Liskov Substitution principle)。它指出,如果你能在使用子类(我们称之为 ChildType)的任何地方使用父类(我们称之为 ParentType)——并且 ChildType 继承自 ParentType——那么你就通过了测试。

你不能通过这个测试的主要原因是,如果 ChildType 从父类移除了一些东西。如果 ChildType 删除了它从父类继承的方法,就会导致 TypeError,即出现未定义的东西,不符合你的期望。

image-146
箭头看起来好像走错了方向,但 Animal 是基础--父代

继承链是一个术语,用来描述从基础对象的原型(其他东西都是从它继承的)到继承链的“终点”(最后继承的类型——上面例子中的 Dog)的继承流。

尽力保持你的继承链清晰且合理。在使用继承时,你很容易出现编程反模式(Fragile base anti-pattern)——当你的基础原型被认为是“脆弱的”,因为你对基础对象做了一个“安全”的改变,然后开始破坏你所有的子对象。

面向对象编程中的多态

多态意味着“以多种不同形式出现的条件”。这正是第四个也是最后一个支柱所关注的——同一继承链中的类型能够做不同的事情。

从上一个图中,我们可能有一个叫做 Animal 的基本原型,它定义了 makeNoise。然后,从这个原型延伸出来的每个类型都可以覆盖,做他们自己的自定义工作。就像这样。

// 让我们创建一个 Animal 和 Dog 的例子
function Animal(){}
function Dog(){}

Animal.prototype.makeNoise = function(){
	console.log("Base noise");
};

// 我们编码的大多数动物有 4 个,如果需要的话,这可以被重写
Animal.prototype.legs = 4;

Dog.prototype = new Animal();

Dog.prototype.makeNoise = function(){
	console.log("Woof woof");  
};

var animal = new Animal();
var dog = new Dog();

animal.makeNoise(); // Base noise
dog.makeNoise();    // Woof woof- 这个被覆盖了
dog.legs;           // 4!被继承了

Dog 扩展自 Animal,可以利用默认的 legs 属性。但它也能自己实现制造自己的噪音。

多态的真正力量在于共享行为,并允许自定义重写。

总结

我希望这篇文章解释清楚了什么是面向对象编程的四大支柱,以及它们是如何帮助我们写出更整洁更高效的代码的。

如果你喜欢这篇文章并想看到更多文章,可以在 Twitter 上关注我。