对象与状态
学习状态与对象之间的区别。
人们认为面向对象编程(OOP)非常复杂、神秘且难以理解。但实际上,它大多相当直接。正确地使用 OOP 可以大大简化程序员的日常工作。然而,这需要比常见的 OOP 实践方式更多的思考和精力。
本课程涵盖核心内容,包括初学者必须掌握的常见 OOP 方式。如果你希望写出优雅的面向对象代码,建议开始阅读伊戈尔·布加延科(Egor Bugayenko)所著的《优雅对象》(Elegant Objects)。
类
面向对象编程假设系统中应该存在某种对象。但什么是对象呢?从我们的日常生活中可以知道,一切都可以被视为对象,比如桌上的一本书、街上行走的人,或者正在过马路的宝马 E34 车型。但如果仔细观察,宝马 E34 只是一个特定类别的对象。同一型号的每辆宝马在本质上是相同的,但它们又是不同的实例。
最直观的类的例子是珠宝的技术图纸:
我们可以从这张图中看到该珠宝产品的各种技术细节,如宽度、高度、半径等。这些细节在最终产品制造之前就已经确定了。编程语言中的类与此类似:它是一种模板或蓝图,基于这个蓝图,我们可以创建真实存在的东西。
而真实存在的东西就是我们根据这张图纸构建的实际对象,也可能是多个对象。这些对象也被称为实例或类实例。
一张图纸可以生成多个对象,类也是如此。只要有一个类,就可以基于它创建多个对象:
1class Car 2end 3 4car1 = Car.new 5car2 = Car.new 6car3 = Car.new
上面我们只有一个类,却创建了三个对象:三辆由该“图纸”生成的汽车。我们的图纸只有两行代码,是因为它是默认的空模板。目前这个类没有任何特殊之处。但在现实生活中,我们通常会给汽车添加更多属性,比如颜色、音响配置、车牌号等等。
状态
状态是 Ruby 等任何面向对象语言中的一个非常重要的概念。其他面向对象语言还包括 Java、C# 和 JavaScript。还有一些部分面向对象的语言,虽然不完全符合面向对象原则,但共享其中大部分或某些概念,例如 Go、Objective-C 等。
面向对象语言与非面向对象语言的主要区别在于前者同时具备“对象”和“状态”这两个核心概念。让我们深入探讨这一点。
回到我们之前的宝马 E34 案例。这种车型的图纸可能存放在德国某处,很可能就在慕尼黑的宝马工厂里。工厂会生产大量该型号的车辆实例。然而,每一辆车都包含许多具体部件:
- 发动机
- 挡风玻璃
- 车身
- 门
- 轮胎
甚至还有更多未列出的部分。如果这些部件脱离整车,就毫无意义——试问一辆没有车体的轮子有何用?但当所有这些组件被组装在一起时,它们就构成了一个完整的有机体,一种能够运行的复杂对象,而这个对象具有自己的状态。
多年来在德国生产的每一辆 E34 型号都属于同一类,但它们各自的状态各不相同。正是状态让一辆车区别于另一辆。那么,状态到底是什么样子的?
里程是一个典型例子。汽车本身是一个复杂的机械系统,几乎不可能有两辆不同汽车拥有完全相同的里程数。另一个定义状态的参数是油箱中的汽油量。再比如发动机当前是开启还是关闭的状态。
车主可以控制这些参数。除此之外,还有许多我们平时不会注意到的其他参数,共同构成了对象的状态。
换句话说,在面向对象语言中,一个对象就像一个活的生命体,而这个生命体拥有自己的状态。我们可以通过外部或内部的方式改变对象的状态。当我们打开车门时,是从外部改变了它的状态;而启动或关闭发动机,则是在车内进行的操作。此外,对象自身也可以主动改变状态:例如,当发动机温度过高时,风扇会自动启动。
下面我们通过一段简单的概念验证代码来演示上述内容:
1# Ruby 中的 Car 类 2 3class Car 4 5 def initialize 6 @state = :closed 7 end 8 9 def open 10 @state = :open 11 end 12 13 def how_are_you 14 puts "My state is #{@state}" 15 end 16end 17 18car1 = Car.new 19car1.how_are_you 20 21car2 = Car.new 22car2.open 23car2.how_are_you
我们创建了一个名为 Car 的新类,并用 Ruby 描绘出一个对象的结构。接着,通过 Car.new 创建该类的一个实例,并将引用赋给变量 car1。需要注意的是,car1 变量本身并不包含对象,它只是指向内存中某个位置的对象的引用。这类似于一栋多层公寓楼外的门铃,它指向某一间具体的房间。同样的规则在这里适用:一个变量可以指向同一个对象,也可能有多个变量指向同一个对象。下面我们将 car1 的值赋给 car777,此时有两个变量,但只存在一个实际对象:
1car777 = car1
然后,我们从外部向对象询问其状态:“你怎么样?” 对象会回应自己的当前状态。第一辆车报告说状态是 closed:“我的状态是 closed。” 为什么会这样?因为我们定义了 initialize 方法:
1def initialize 2 @state = :closed 3end
每当创建一个新的实例时,这个方法就会被调用。换句话说,每次执行 Car.new,都会触发 initialize 方法。尽管没人真正清楚为什么我们要用这么长的名字 initialize,而不是更简洁的 init 或 new。下面这种写法看起来更好,但实际上是错误的。我们必须使用 initialize 方法:
1class Car 2 def new 3 # ... 4 end 5end 6 7Car.new
在文档和编程书籍中,initialize 方法也被称为构造函数。在 ES6 及以上版本的 JavaScript 中,它写作如下形式:
1// ES6 及以上版本的 JavaScript 构造函数 2 3class Car { 4 constructor() { 5 console.log('hello from constructor!'); 6 } 7} 8 9let car1 = new Car();
运行上述程序,你会看到 “hello from constructor!” 的输出信息。该方法在对象初始化阶段被调用。下面是同样的代码在 Ruby 中的实现:
1# 对象初始化 2 3class Car 4 def initialize 5 puts 'hello from constructor!' 6 end 7end 8 9car1 = Car.new
这是 Ruby 中最不容易察觉的一个特性:我们调用 new 时,initialize 方法会自动被触发。
但为什么我们需要构造函数?因为我们需要用它来定义对象的初始状态。例如,当我们生产一辆新车时,希望车门、车窗和后备箱一开始都是关闭的。