面向对象范式
面向对象是一种编程方式,与电子设备的演变方式有着惊人的相似之处。本页将解释过去的难题,以及这些难题是如何通过组件(在电子设备中)和程序中的对象解决的。
最早的电子设备是一个组件网络。如果你看一下老式收音机的内部,你会发现所有东西都是连接在一起的。只有少数东西可以断开:电源插头,可能还有外部扬声器。有时这些设备还会附带一个电气图,其中详细地显示了所有内容。你可以花几个小时看这样的图,发现图中各种各样的功能,比如无线电接收、放大、音调控制等等。
最早的计算机程序也具有相同的结构。用汇编语言或 Basic 编写的程序很容易跨越数百甚至数千行代码。它必须这样做,因为所有细节都必须在一个代码文件中提供。
当然,在那些日子里,如果开发人员设法用一套不错的功能编写了一个能工作的程序,他真的会感到很自豪,但维护起来却很困难。如果你想要另一个程序,你必须从头开始编写。重用之前程序的代码并非完全不可能(你可以复制一些子程序),但并不容易。
因此,在电子设备和程序中,都必须对庞大的结构进行组织。如果你打开一台现代的收音机,你会发现它的内部与第一代设备截然不同。如今你会发现的是组件。这些组件通常看起来像是带有一个基本功能的小型电路板。因此,你可能有一个前置放大器组件(在立体声设备中可能有两个,甚至在环绕声系统中更多),音调控制组件,电源组件等等。这些组件很容易更换,因为它们是通过插头连接的。
通常,一家工厂不会多年生产一种复杂的产品,而是生产一整系列产品,这些产品都是由相同或类似的组件组成的。事实上,工厂通常不自己生产所有组件,而是由其他工厂生产的。所有这些都是因为这些组件具有一个定义的功能,并遵循标准。个人电脑甚至更进一步:它们的设计方式允许用户随时对其进行升级。
为了使组件能够互换,需要一些必要条件。所有与组件的电气通信都必须通过插头进行。这样的插头定义了一个广义功能,例如“声音输出”,使连接耳机、扬声器、录音系统或任何其他“声音处理器”成为可能,而不论其内部结构如何。事实上,内部结构只对组件本身很重要,不应该打扰使用这些组件来构建设备的设计人员。
计算机程序的演变方式与电子设备相同。可重用的组件被称为对象,所有通信都应该通过的插头被称为接口。
“接口”这个词可以有多种含义。许多计算机语言允许你定义一个接口。这样的定义就是一个最小接口,或者说是连接到它的任何东西都应该支持的最低限度。任何对象都可以有多个这样的接口,就像任何电子元件都可以有多个插头一样。很多插头都是众所周知的,并且有文档记录(声音插头、USB 插头、墙壁插座电源插头等等),同样,接口也经常有文档记录,尤其是在它们与外部库一起提供的情况下。编程中的一件好事是,接口是由命名函数和你可以传递给它们的参数定义的。仅仅给函数一个描述性的名称就可以解释一个接口的可能性,即使它没有附带任何其他文档。
“接口”这个词有时也用来指代所有公开的函数集,或者所有插头的完整集合。
执行一个基本功能也会让人联想到责任的概念。例如,一个墙壁插座负责提供一定的电压。如果电压过高,你连接到它的任何东西都可能损坏。但插座的类型表明了应该提供的功率。一个 220 伏插座与一个 380 伏插座看起来完全不同。有些设备会检查电压,例如用保险丝,但它们不应该这样做。
同样,你的对象也可以被认为与外部世界有一个契约:如果你提供给它有效的输入,它将正确地执行其功能并提供正确的输出。一些编程语言允许你指定特定函数的需求以及在满足需求的情况下保证的内容。这些保证的理论被称为契约式设计。
这种契约在面向对象语言中尤其有用,在面向对象语言中,任何复杂结构都是由对象处理的。例如,当你将一个路径传递给一个文件时,你可以声明文件必须存在的需求。或者,当传递数据库连接时,一个方法可以要求连接是打开的,并且事务已启动。同样,一个方法可以确保一个文件存在(在它向其中写入数据之后)或者数据库事务已关闭。因此,这种契约有助于将调用对象和被调用对象之间的责任分开。
但一个对象仍然可能遇到它无法自行解决的问题(例如,网络连接突然断开)。在这种情况下,一个对象可以将问题升级到周围的应用程序。这通常是通过异常完成的,这将在后面解释。
由于一个对象执行一个功能,它也可以单独测试。你可以构建一个小的测试台,将各种有效和无效的输入馈送到对象,并测试它是否产生正确的输出,以及它是否在应该升级的时候升级。这些测试台被称为单元测试。
如果任何对象都可以信赖于做好自己的工作,那么这样的对象也可以信赖其他对象做好更细粒度的任务。这意味着,任何有责任的对象都可以将部分责任委托给另一个对象。例如,一个 SettingsStorage 对象可以将设置写入文件,但将所有文件处理委托给一个 TextFile 对象。或者,在另一个例子中,一个 Backend 对象可以指示一个 Database 对象执行一个查询,并将结果发送到 ObjectRelationalMapper 对象,以便它可以构建一个很好的对象结构。这可以使代码非常清晰,因为 ObjectRelationalMapper 对象不必自己“挖掘”数据。
在一个电子设备中,所有组件通常都必须在设备通电时存在。但计算机程序在运行时生成对象。为了能够做到这一点,许多编程语言允许你定义要创建的对象的类型及其功能。这种对象的“设计”被称为类。简单地说:类是对象的蓝图。程序员编写一个类,程序使用它来构建对象。
如果你能够精确地定义一个对象的责任,你也可以测试它。单元测试是专门测试其他对象责任的对象。单元测试通常在程序的正常运行期间不处于活动状态,但可以从开发环境中激活,或者使用某种“自检”选项激活。
使用已完成的应用程序测试类的所有细节极其困难。一些测试难以组织(例如断开的连接、不匹配的权限、查询中的语法错误等),但您希望了解程序的内部机制在所有这些条件下都能可靠地工作。这就是单元测试的用武之地。单元测试测试被检查对象是否能够执行其任务,同时也测试它在无法执行任务时是否会升级。单元测试允许程序员测试系统中最基本、最底层的对象。但单元测试也让程序员对这些对象充满信心。当然,完美的代码是不存在的,因此总是存在遗漏测试的可能性。因此,单元测试会随着被测代码的演变而演变。
单元测试极其重要的一个领域是程序员必须修复一些代码时。如果这段代码被单元测试覆盖,程序员只需运行测试以查看修复是否成功,并且没有产生任何意想不到的副作用(或“错误”)。
异常
[edit | edit source]异常通常也是对象,但它们用于传达问题。如果一个对象无法履行其职责,它会通过构建一个包含问题描述的新对象并将其“抛出”到周围代码中来升级。周围代码可以“捕获”这些异常并决定如何处理它。
多态
[edit | edit source]多态为您提供了以不同的抽象级别定义对象的機會。例如,您可以定义一个 WebWidget 类,该类可以输出网页的一部分并处理一些提交的输入。从这个相当抽象的类,您可以派生一个 TextBoxWebWidget,它专门在网页上绘制一个文本框并检索一段文本。更抽象的类被称为 *超类*,更具体的类被称为 *子类*:在本例中,WebWidget 类是 TextBoxWebWidget 的超类,而 TextBoxWebWidget 类是 WebWidget 类的子类。也可以说 TextBoxWebWidget 类 *继承* 自 WebWidget 类。
这种多态有一个特殊之处:您可以将变量定义为更抽象的形式或更具体的形式。如果您将变量或参数定义为更抽象的形式,您始终可以传递更具体的形式。因此,您可以将各种小部件传递给生成完整网页的代码,每个小部件都会执行该特定小部件所需的动作,而不会让网页构建代码困扰所有差异。
一个超类可以有多个子类,但在大多数面向对象的语言中,一个子类只能从一个超类派生。注意,面向对象理论中没有任何东西禁止这种“多重继承”,它只是因为语言设计者发现它对自己或语言用户来说更方便。
回到组织
[edit | edit source]当您阅读有关职责和继承的信息时,您可能会感觉到面向对象的代码就像人一样组织。的确如此。更准确地说,代码就像公司或其他结构化组织一样组织。
继承和专业化
[edit | edit source]许多组织都有通用功能和专门功能。例如,任何警察都可以逮捕,但有专门的警察负责解决谋杀案、解决毒品案件、在人群中阻止醉酒的人成为问题,或确保没有人溺水。类似的结构存在于医生和其他职业中。在警察和医生方面,这些人也很容易被识别。因此,您可以将接口或类比作一种制服:每种制服都会传达佩戴者能做什么,而制服也带来了责任。
职责、技能和升级
[edit | edit source]当公司里的人给仓库打电话说“请寄一个新的笔记本电脑”时,人们相信仓库员工有足够的技能来完成这项工作。但这并不意味着仓库员工必须亲自完成任务。他可以将其分配给同事,或者自己去取笔记本电脑,但让其他人把它送到请求者那里。这并不重要:工作完成了,请求者不必知道怎么做。只有在出现问题时,情况才会升级(例如,当笔记本电脑缺货时),可能会有从公司外部获取笔记本电脑或稍后送货的选项。无论如何,没有人会指望仓库员工从砍树开始,做纸张,然后做笔记本电脑。这绝对 *不是* 他的职责。同样,显示数据库中值的物体通常不负责打开连接、构建查询等等。它将这些任务外包给其他对象,或者其他对象将显示任务外包给我们的对象。
面向对象原则
[span>edit | edit source]在面向对象理论中,出现了一些原则,试图使维护成为可能,而不会引入错误。在实践中,完美地遵循这些原则非常困难,但它们有助于避免在现有软件中引入新的错误。
开闭原则
[edit | edit source]“软件应该对扩展开放,对修改关闭。”
如果您只能扩展软件,则无法触及现有部分,您只能在扩展中引入错误。在实践中,这是通过多态完成的:您可以编写一个扩展现有类的新的类,尽可能使用所有现有函数,并且只覆盖新的函数。
重构
[edit | edit source]不是真正的“原则”,而是一种实践。重构听起来像是完全无用的东西:重构是在不改变软件行为的情况下改变软件。换句话说,重构程序后,您将得到一个与原始程序完全相同的程序。这听起来可能完全没用,但事实并非如此。
如果您应用了开闭原则,您就可以放心,您不会在现有软件中引入新的错误,但您仍然在软件中添加了一些东西。许多这些添加的修复可能看起来很丑陋,并且在维护中是一个负担。因此,在某个时刻,您将不得不重新组织结构以使其再次变得逻辑和合理。或者相反:您甚至可能无法在不打开结构进行此类修复的情况下向软件添加修复。
但是您如何知道行为是否已保留?这就是单元测试的用武之地。单元测试检查代码的行为,因此任何行为变化都应该在单元测试中可见。
请注意,开闭原则和重构实践相互补充。首先保持结构和现有代码完好无损,只添加功能,然后保持功能完好无损,只修改结构,您将有最大机会防止代码中出现错误。
一位维基书作者认为此页面应该拆分为具有更窄子主题的较小页面。 您可以通过将此大页面拆分为更小的页面来提供帮助。请务必遵循 命名策略。将书籍划分为更小的部分可以提供更多的重点,并允许每个部分做好一件事,这有利于所有人。 |