面向对象的思考过程是一个非常优秀的设计理念。它可以独立于语言存在。如果你熟练掌握了面向对象的思考过程,那么就可以轻松地在不同的面向对象的语言之间切换。
既然函数式编程这么好,为什么这几年的发展只能算是波澜不惊,没有掀起大风浪呢?
- 首先面向对象的思考过程更加符合大家对世界的直观感受,毕竟不是每个人都是数学家
- 其次是面向对象的编程范式和函数式编程的范式并不是完全对立的,一些语言既有函数式的特点,也有面向对象的特点
历史上定义面向对象的语言拥有:封装(encapsulation)、继承(inheritance)、多态(polymorphism)的特点,我也会加入组合的特点。
开发人员面临的问题一直就是如何将新的面向对象技术与现有的系统集成起来。
包装对象是指在面向对象的类中包含其他代码。例如,可以将结构化代码包装到一个对象内部,使其行为就像对象一样。
究竟什么是对象?一个对象由两部分组成:属性及行为,对象的基本定义是一个包含了数据和行为的实体。
在面向对象的设计中,属性及行为包含在单个对象中,而在过程式或结构式设计中,属性和行为通常是分开的。
结构化编程中数据往往与程序分离,而且数据是全局的,所以在你的代码作用域之外依然可以很容易修改数据。
如果设计是恰当的,那么在面向对象模型中则不会有诸如全局数据的元素。事实上,在面向对象系统中具有很高的数据完整性。
对象包含整型和字符串之类的实体,用于表示属性,对象也包含方法,用于表示行为。在对象中,方法用于操作数据及其他行为。更重要的是,你可以控制对对象中成员(包括属性及方法)的访问。
在面向对象的术语中,数据表现为属性,行为表现为方法。限制访问具体属性和(或)方法的行为叫做数据隐藏。将属性及方法合并到同一个实体中,在面向对象中将这种方式叫做封装。
对象不应当操作其它对象的内部数据。
使用面向对象技术的程序本质上是对象的集合。
存放在对象中的数据代表了该对象的状态。在面向对象数据中,数据被称为属性。
对象的行为表示对象可以做什么。在面向对象程序设计术语中,这些行为包含在方法中,你可以通过发送消息的方式来调用方法。
使用对象最强大、最有趣的一点在于,数据与行为是一个整体,而不是与行为代码割裂。
XML 的出现并不全为了以可移植的方式表示数据。它也有一种替代方式起来让代码轻松访问数据。
取值方法和赋值方法的理念就是数据隐藏。
每个类图有三部分组成:类名、数据(属性)、行为(方法)。
请注意没必要为每个对象的每个方法都实现一个物理副本。其实,每个对象指向了同一个实现。
类是对象的蓝图。当你实例化一个对象时,你基于类来构建这个对象。
类可以认为是对象的模板或者模具。
类的数据通过属性来表示。每个类必须定义属性,用来存放该类实例化的每个对象的状态。
在任何情况下,对对象中属性的访问应该由该对象自身控制,任何一个对象都不应该直接修改其它对象的属性。
消息是对象之间的通信机制。例如,当对象A调用了对象B的一个方法,对象A向对象B发送了一个消息。对象B的响应由其返回值定义。
对象仅暴露必要的接口来和其它对象进行交互。除了如何使用该对象,其它细节都应当对其它对象隐藏起来。
数据隐藏是封装的主要部分。
为了实现数据隐藏,必须将所有属性声明为 private。属性绝不是接口的一部分。只有 public 方法是类接口的一部分。
请注意类有接口,方法也有接口。
注意在该类图中,加号(+)表示 public 访问修饰符,而减号(-)表示 private 访问修饰符。
面向对象程序设计的最强大的功能之一就是代码重用。
一个类只能有一个父类称为单继承,一个类可以有多个父类称为多重继承。
多态是一个希腊词,字面上理解为许多形状。在继承体系图中,所有的子类从它们的超类中继承接口。然而,由于每个子类是单独的实体,每个子类需要对同一个消息有单独的应答。
重载(overriding)的基本释义是子类覆盖父类中的一个实现。总之,每个类能够对同一个Draw方法返回不同的响应来绘制自己。这正是多态的意义。
如果方法被定义为abstract,子类必须提供该方法的实现。
如果方法名与类名相同,并且没有返回值,这个方法就是一个特殊的方法,称为构造函数。可以认为构造函数是类的入口,对象在这里被构造。构造函数里可以进行初始化操作和执行一些启动任务。
如果子类继承了父类的一个抽象方法,它必须提供该方法的具体实现,否则它自身也必须是个抽象类。
使用其他对象来构建或结合成新的对象,这种方式就是组合。
我想说只有两种方式来使用其他类构建新类,这两种方式就是继承和组合。我们最好用has-a术语来描述组合关系。
在设计一个系统甚至一个类之前,先思考问题本身。
培养面向对象的思考过程的良好习惯需要注意三个方面:
- 清楚接口和实现之间的区别。
- 深入理解抽象。
- 给用户提供尽可能少的接口。
当设计类时,应该向用户暴露什么、隐藏什么是非常重要的。
接口与类直接相关。终端用户通常看不到任何类,只会看到GUI或者命令行。程序员会接触类接口。
正确地设计类时要注意两部分,即接口和实现。
作为一个通用的规则,一个类的接口应该只包含需要用户知道的东西。
当设计类时最重要的考虑就是识别类的读者(或用户)。
实现细节对于用户是隐藏的。我们必须时刻牢记关于实现的一个目标,那就是修改实现不需要变动用户代码。
接口包含了调用方法及返回值的语法。
如果一个方法是公共方法,那么程序员就可以访问它,因此可以认为它是类的接口。
只提供给用户绝对需要的东西。这以为着接口要尽可能少。最好只有用户真正需要时才添加接口,不要提供超出用户需求的接口。
公共接口定义了用户可以访问什么。
从用户角度定义类至关重要,而不是从信息系统的角度定义类。
确保设计类时你向真正的用户了解了需求和设计。
我们已经确定用户是实际使用这个系统的人。那么,谁是用户?
需要从每个用户的视角来开始识别每个对象的目的以及需要做的事情。
环境限制往往都是影响因子。
刚开始,你只需要考虑如何使用这个对象,不用考虑如何构建这个对象。
很多面向对象的文章推荐每个接口模型只包含一个行为。这带给我们的问题是我们的设计究竟要抽象到哪种层次。
从技术角度来讲,任何非公共接口可以视为实现。这意味着用户不会看到具体的实现方法。
类可能有一些私有方法仅供内部使用。任何私有方法都可以视为实现的一部分,用户绝不会看到它,从而也不能访问它。
公共方法中的代码是实现的一部分,因为用户不能看到它。
理论上来说,任何对实现的修改都不应该影响用户通过接口与类的交互方式。
构造函数名称与类名相同。构造函数没有返回值。如果有返回值,编译器就不认为该方法是构造函数。
new关键字创建了Cabbie类的一个新实例,这会按需分配内存。然后会调用构造函数自身,并且可以通过参数列表传递参数。开发人员可以在构造函数内进行相应的初始化工作。
构造函数最重要的功能大概是当遇到new关键字时初始化内存分配。总之,构造函数中的代码会把新创建的对象初始化到稳定、安全的状态。
初始化属性是构造函数经常执行的功能。
如果没有为类提供一个显示的构造函数,那么类会有一个默认构造函数。请记住,无论你是否自定义了构造函数,类始终至少有一个构造函数。如果你没有提供构造函数,系统会为你提供一个默认的构造函数。
除了创建对象本身之外,默认构造函数的另一个行为是调用父类的构造函数。
在类中始终包含至少一个构造函数式一个优秀的实践。如果类有属性,最好始终在构造函数中初始化这些属性。延伸开来,无论是否在编写面向对象的代码,初始化变量总是一个优秀的实践。
通用规则是即使并不需要在构造函数中做任何事情,也应当始终提供一个构造函数。你可以提供一个不包含任何代码的构造函数,稍后再按需添加代码。尽管使用编译器默认提供的构造函数在技术上没有任何问题,但基于文档化和维护目的,这样更容易看懂你的代码。
如果你使用的是默认的构造函数,后续操作添加了另一个构造函数,那么系统不会再创建默认的构造函数。总之,只有类中没有包含任何构造函数时,系统才会添加默认的构造函数。一旦你提供了一个构造函数,系统就不再提供默认的构造函数。
重载可以让程序员重复使用相同的方法名,只要每次方法签名不同即可。方法签名包含了方法名以及参数列表。
当使用继承时,你必须知道如何构造父类。请记住,当使用继承时,也继承了父类的所有东西。因此必须熟悉父类的数据和行为。任何继承的属性都是完全可见的。然而,对构造函数的继承则是不可见的。
如果遇到new关键字,那么会分配对象,并发生以下步骤:
在构造函数中会调用父类的构造函数。如果没有显示调用父类的构造函数,那么系统会默认自动调用。
对象中的所有属性会被初始化。
执行构造函数中的其余代码。
不要依赖编译器来初始化属性。
构造函数用来确保应用程序处于稳定的状态。
优秀的实践应该是为所有属性识别一个稳定的状态,然后在构造函数中初始化这些属性为稳定的状态。
程序中有三种基本的解决方案来处理发现的问题:修复问题、通过压制来忽略问题,或以合适的方式退出运行时。
- 忽略该问题。这不是好主意!
- 检查潜在的问题,当发现问题时中止程序。
- 检查潜在的问题,捕获错误并试图修复该问题。
- 抛出异常(通常这是处理异常的最佳方式)。
不应该忽略任何已知的问题。可以让系统收拾残局并进入稍微稳定的状态,比如关闭文件和强制系统重启。
检查潜在问题,捕获错误,并试图恢复的方案远胜于简单地检查问题并中止程序的方案。
并不总能在错误第一次发生的地方就能探测到该错误。
这里的关键概念是用特定的代码块用来处理特定的异常。这既解决了尝试找出错误发生的地方的问题,也解决了在正确的地方处理该错误的问题。
在 Java 中,try 代码块中抛出了异常,catch 代码块会处理该异常。
- try 代码块会结束执行
- catch 从句会检查对应的 catch 代码块能否处理这种异常。(一个 try 代码块可能会对应多个 catch 从句)
- 如果所有 catch 代码块都不能处理抛出的异常,那么该异常会传递给最近的更高一层的 try 代码块中(如果代码中没有捕获该异常,系统最终会捕获它,结果是无法预料的,可能导致应用程序崩溃)。
- 如果有一个 catch 从句匹配上了(遇到了第一个匹配的从句),会执行 catch 从句中的代码。
- 程序会从紧挨着 try 代码块的下面的代码处恢复执行。
每个类可以实例化出多个对象。每个对象有唯一的标识和状态。这点很关键。会给每个单独构造的对象分配独立的内存。然而,一个类实例化的多个对象可以共享类中一些属性和方法,从而共享为这些属性和方法分配的内存。
构造函数是一个被类的所有实例共享的方法,这是共享方法的一个好例子。
对象有三种属性:
- 局部属性,由特定的方法拥有
- 对象属性
- 类属性
属性(和方法)存在于特定的作用域中。
关键字 this 是对当前对象的一个引用。
static 类型的变量,从该类中实例化的所有对象只会为该属性分配一块单独的内存。每个类只有一个副本,该类的所有对象共享该副本。
操作符重载允许你修改一个操作符的含义。
近代面向对象的语言(不如Java、.NET 和 Objective-C)不允许重载操作符。
与操作符重载一样,Java、.NET 和 Objective-C 的设计者认为多重继承带来的系统的复杂度超过了带来的好处,因此从语言层面消除了多重继承。Java、.NET 和 Objective-C 语言提供的接口构造能力在某些方面能弥补这一点。但 Java、.NET 和 Objective-C 不允许传统的多重继承。
接口是行为继承的一种机制,抽象类则用于实现继承。编程语言中的接口类型提供不同行为的接口,但不提供实现,而抽象类既提供接口,也能提供实现。
复杂的数据结构和对象的问题在于它们可能会包含引用。简单对引用的复制不能复制它引用的数据结构或对象。同样,当比较对象时,简单地比较两个指针只是比较了引用,而并未比较指针所指的对象。
追踪所有的引用,并对所有引用对象都创建拷贝,这种方式称为深拷贝。浅拷贝只会简单地拷贝引用,而不会深入层级。
当设计类时,你应当在类中提供一个比较功能,从而保证类的行为是预期的。
最明显的原因是类名用来识别类本身。除了简单的识别作用之外,类名必须是描述性的。选择一个合适的名称相当重要,因为类名提供了这个类的用途以及在大系统中的交互方式等信息。
把属性设置为不存在的值是非常有用的编程技术。检查变量是否为 null 可以识别该值是否正确初始化。检查属性是否为 null 也是一个优秀的编程实践。
构造函数都定义为 public,这是因为构造函数很显然是类接口的成员。如果构造函数是私有的,其它对象就不能访问它们,从而无法实例化对象。
最重要的原因是保证数据完整性以及高效调试。
赋值方法可以在某种程度确保数据的完整性。这也可以解决安全问题。因此通过取值方法和赋值方法来访问数据可以提供一种机制用于密码检查或其它验证技术。这极大地增加了数据的完整性。
如果属性是静态的,而且类为该属性提供了一个赋值方法,其它对象调用该赋值方法只会修改同一个副本。
构造函数和访问器都被定义为公共的,并且属于公共接口的一部分。对外暴露它们是因为它们是使用该类的重要方式。
我最喜爱的有关类设计指导及建议的其中一本书是《Effective C++:50 Specific Ways to Improve Your Programs and Designs》。
在设计类时最重要的问题是保持公共接口最小化。提供最小化的公共接口可以保证类尽可能地简单。
隐藏实现的原因已经阐述得非常详细了。改变类的实现不应该影响到用户,这才是设计良好的类。
但我认为把所有终端用户当做实际客户是相当重要的,而且你必须满足他们的要求。
Gilbert 和 McCarty 指出封装的最高指导原则是”所有字段都应该是私有的“。在这种方式下,其他对象无法直接访问类中的任何字段。
当设计类时,最重要的设计问题之一是如何构造类。首先并且最重要的一点是,构造函数应该把对象设置为安全的初始状态。
在包含构造函数的语言中,析构函数包括了正确的清除功能,这也很重要。
通用规则是应用程序应当绝不崩溃。当系统遭遇错误时,应当自身修复错误并继续执行,或者在不丢失用户的任何重要数据情况下友好地退出。
没有优秀的文档实践是不可能驱动出优秀的设计的。优秀的设计的最重要的方面之一是,设计类时应该小心地记录过程。
几乎没有完全隔离的类,几乎没有任何原因来构建一个不需要与其他类交互的类。
确定哪些属性和方法可声明为静态的相当重要。这些属性和方法会被类的所有对象共享。
为类、属性和方法遵循命名约定也是同一目的。有很多命名约定,你选择哪种约定并不重要,重要的是选择一个并始终遵守。当选择了一种约定后,确保当你新建类、属性和方法时,你不仅遵循了约定,而且名称具有含义。确保这些约定是有意义的,每个相关的人都能理解背后的意图。
保持命名具有描述性是优秀的开发实践,无论哪种开发范式中的都要执行这项实践。
最小化全局数据是优秀的编程风格,这并不特定于面向对象编程。全局数据在结构化开发中是允许的,但它们是危险的。
swap() 方法的作用域内需要 temp 属性。没有理由将 temp 属性放置到类级别。因此,你应该把 temp 的作用域移动到 swap() 方法的作用域中。
高度依赖其他类的行为被称为高度耦合。即如果修改一个类会强迫修改另一个类,那么这两个类则可以说是高度耦合的。
在大多数设计和编程过程中,一般都推荐使用迭代过程。基本上,这意味着不要一次性写完所有代码!用小步增长的方式来编写代码,每步都进行构建和测试。
测试人员更喜欢用迭代过程,因为他们可以在早期就参与进来。
接口的最小实现通常称为桩(stub)。
当使用完桩后,不要删除它。保留桩以便后续使用。确保用户不能看到它们。
系统可以被定义为相互交互的类。
创建优秀的设计最重要的因素是找到一个你和你的组织都感到舒服的方式并且坚持使用它。实现一个没人愿意遵循的设计没有任何意义。
通常一个稳固的面向对象的设计过程包含以下步骤:
- 进行正确的分析
- 编写工作陈述文档来描述该系统
- 通过规格说明收集需求
- 开发用户接口的原型
- 识别类
- 确定每个类的职责
- 确定类与类之间如何交互
- 创建一个高层次的模型来描述系统的构建
彻底测试软件确保绝对没有任何缺陷存在是不可能的。
在分析阶段,如果没有有效的理由来做该项目,那么可以毫不犹豫地中止该项目。
工作陈述(SOW)是描述系统的文档。
Visual Basic.NET 是一个创建原型的非常棒的环境。
我认为作为一名优秀的程序员意味着理解基本的编程逻辑,并且对写代码充满激情。
将一个现有的类包装到一个新类中,以便修改它的实现或接口。
最困难及最有趣的设计决策就是决定使用继承还是组合。
正确做法是应该始终测试新代码。每个新的继承关系使用继承而来的方法时会创建新的上下文。完整的测试策略是基于这些上下文做测试。
该概念有时被称为通用到特例,这是使用继承时的又一个重要的考虑因素。
在大型系统中,尽可能保持简单往往是最佳实践。过于精确的模型无法维持较低的复杂度。决定在设计时引入更小的复杂度或者更多的功能是一项平衡艺术。
当前还有未来的开销因素也是决策的主要因素。
对象组合的经典例子是汽车。
本书中,UML 中的聚合以带线的菱形表示,比如引擎是汽车的一部分。联合则只有一根线(没有菱形)表示。
封装是面向对象的本质,所以它是面向对象设计的基本原则之一。继承也是三个主要的面向对象概念之一。然而,继承在某种方式上实际上破坏了封装!
继承意味着对其他类的强封装,但是弱化了父类和其子类之间的封装。
多态是对继承的最优雅的使用之一。实体类自身负责实现功能。
子类不能从协议继承任何代码。因此协议用法与抽象类不是完全相同的,所以设计对象模型时要考虑这一点。
接口、协议和抽象类是代码重用的重要机制,提供了所谓契约这一功能。
面向对象的拥护者鼓吹面向对象的主要优势就是一次编写,多次重用。
创建可重用的代码的方式之一是使用框架。
框架可以实现插拔机制和重用准则。它也能让开发者最大化地重用代码,而且可以重用界面设计。
编写类或类库的人应该提供文档来介绍如何使用这些类和类库(至少我们希望他这样做)。通常这些文档代表了应用程序编程接口(API)。
我们定义契约为要求开发人员遵循API规格要求的一种机制。
如果不强制遵守,一些淘气的程序员会决定重新发明轮子来自己实现代码,而不会使用框架提供的规格说明。
实现契约的方式之一是使用抽象类。抽象类包含一个或多个没有提供任何实现的方法。
假设我们想创建一个应用程序来绘制形状。我们的目标是绘制产品设计中包含的所有类型的形状,以后还可以添加新的形状。那么必须遵循两个条件。
- 首先所有形状必须使用相同的语法来绘制自生
- 其次请记住每个类必须要响应自身的行为
向对象发送消息,不同的对象会得到不同的响应,这是多态的本质。
如果我们想让 Shape 类包含所有可能的(当前的以及以后加入的)形状代码,那么需要一些条件语句(比如 Case 语句)。这会非常杂乱,而且难以维护。
如果 Circle 继承自 Shape 但没有提供 draw() 方法,对 Circle 类的编译会失败。这是因为 Circle 没有满足 Shape 的契约。
如果 Circle 没有实现 draw() 方法,那么可以认为它自身是抽象的。那么另一个子类必须继承自 Circle 并且实现 draw() 方法。该子类则成为 Shape 和 Circle 类的实体实现。
请记住对抽象类的定义是它包含一个或多个抽象方法,这暗示了抽象类也可以提供实体方法。
这些抽象方法就是契约。
契约不适合用于组合情况(或者 has-a 关系)。
第一,当涉及用户交互的可视化接口(比如显示器)时,会广泛使用图形化用户接口(GUI)。
第二,类的接口基本上是指其方法签名。
第三,在 Objective-C 语言中,接口和实现会将代码在物理上分割为不同的模块。
第四,Java 中的接口和 Objective-C 的协议本质上是父类和子类之间的契约。
使用多个抽象类构成了多重继承。如果设计上是合理的,理论上你可以为任何类添加接口。而抽象类要求你继承自该抽象类,并且延伸至其自身所有可能的父类。
基于这些考虑,接口往往作为缺少多重继承情况下的一种替代方案。但接口并不是替代或回避使用多重继承。
接口不像抽象类,它完全不能提供任何实现。所以任何实现某个接口的类必须提供实现所有方法。
有时继承被称为实现继承,而接口被称为定义继承。
抽象类可以提供抽象方法,也可以提供实体方法,而接口只能提供抽象方法。为什么要有这样的区别呢?
- 狗是哺乳动物,所以 Dog 和 Mammal 之间的关系是继承关系
- Dog 实现了 Nameable,所以它们之间是接口关系
- 狗有头,所以 Dog 和 Head 之间是组合关系
尽管接口是继承的特殊类型,但了解特殊之处是非常重要的。理解这特殊之处是设计出强壮的面向对象系统的关键。
虽然继承时严格的 is-a 关系,但接口不是。
接口可被应用到不相关的类。你可以给狗命名,也可以给蜥蜴命名。这是使用抽象类和使用接口的关键区别。
接口指定了没有明显联系的类之间的相同行为。
所以我们可以安全地说狗是有名字的实体。这是简单但有效的证据,继承和接口都构成 is-a 关系。
首先,很多情况下开发系统时甚至并未考虑重用。其次,即使考虑到了重用,计划限制、有限的资源以及经费考虑等问题经常干扰最佳实践。
使用组合的另一个优势是可以分别构建系统及子系统,而且更重要的是这些系统可以被独立测试和维护。
- 稳定的复杂系统通常有一定的层级结构,每个系统由更简单的子系统构建而成,这些子系统又由更简单的子系统构建而成。组合适用于这条准则,即通过简单的对象来构造复杂的对象。
- 稳定的复杂系统是可分解的。
- 稳定的复杂系统往往由不同类型的子系统以不同的方式组合而成。
- 可工作的复杂系统往往是从可工作的简单系统演化而来。
作为软件设计者,组合是用于对抗软件的复杂度的非常重要的策略之一。
使用组件的主要好处就是可以使用其他开发人员(甚至是第三方供应商)构建的组件。
通常有两种组合方式:联合和聚合。联合和聚合的微小区别在于部分如何构成整体。在聚合中,通常只看到整体,而在联合中,通常看到的是组成整体的部分。
最直观的组合方式就是聚合。聚合意味着复杂的对象由其他对象构成。
聚合代表你通常看到了整体,而联合既代表整体,也代表部分。在立体音响系统中,各种各样的组件是独立的,通过插接线(连接各种各样组件的线)连接成整体。
聚合是指复杂的对象由其他对象组成。而当一个对象需要其他对象的服务时则使用联合。
最佳实践是一个领域中的对象不应当和另一个领域中的对象混合,除非有非常特殊的情况。
利用混合系统的便利性是一项设计决策。如果TV/VCR集成系统的便利性比单个组件的风险和故障更重要,那么采用混合领域则是首选的设计决策。
基数表示参与联合的对象个数,可以表示这种联合关系是可以选还是强制的。
当处理联合时最重要的问题之一是确保设计应用程序时检查可选的联合。即代码必须检查该联合是否为 null。
类图有三部分组成:类名、属性和方法(构造函数也是方法)。
属性没有签名,有类型;方法具有签名。
通过类图可以知道参数的数据类型。因为属性之前有个减号(-)前缀,声明了这些属性是私有属性。加号(+)则表示这些属性是公共的,而这是不应该的。
在 Java 中访问修饰符的默认类型是受保护类型。
当需要借助其他类来创建一个类时就是组合关系。当一个类由其他类组成时就是聚合关系(比如轮胎和汽车的关系)。当一个类需要其他类的服务时就是联合关系(比如客户需要服务器的服务)。
聚合由一个头部有一个菱形的线表示。
在UML标记中,一条单纯的线表示这种关系,线的两端没有任何形状。
XML是一种标准的机制,可以在完全不同的系统之间定义和传输数据(JSON是另一种机制)。XML和JSON提供了一种在相互独立的应用程序之间共享数据的机制。
XML提供了以多种方式传输数据的标准。通常可以认为数据能够以垂直和水平两种范式来传输。词条垂直意味着数据可以跨行业传输。
采用XML标准的另一种方式是建立水平应用程序。水平的应用程序特定于某个行业。
XML全程为扩展标记语言。你可能早已熟悉另一种标记语言,叫作超文本标记语言(HTML)。XML 和 HTML 是 SGML 的后代,SGML 是标准的通用标记语言。
然而 XML 提供了两个 HTML 不具备的优势,就是验证文档以及格式化文档。
HTML 的标签是预定义的。
存在一种叫作文档类型定义的(DTD)的文档。DTD 用于定义描述数据的标签。当创建 XML 文档时,只可以使用预定义的标签。
你不必强制使用 DTD。但使用 DTD 可以验证 XML 文档。对 XML 的唯一验证方式就是检查 XML 的格式是否正确。而使用了 DTD 的 XML 则不接受最佳猜测。如果文档结构不正确,那么会产生一个错误,该文档是不合法的。
通常使用面向对象的语言开发的应用程序可以与 XML 进行交互。
XML 文档可以将 DTD 内嵌在文档中,也可以指定一个外部的 DTD。外部的 DTD 提供了一种更强大的机制。
PCDATA 全称为解析字符数据,是从文本文件中解析字符信息的标准。
早期的一个工具叫作 XML Notepad,它和微软操作系统提供的 Notepad 很相似,有助于我们理解 XML 文档的结构。
需要使用 XML 验证器来检查合法性。
w3schools 网站的 XML 验证器是其中之一,而且简单易用。
需要指出 HTML 不会进行这种类型的检查。事实上,即使 XML 文档结构是非法的,但仍然可以用浏览器打开。
请记住 XML 主要用于定义数据,而 HTML 则基本上是一种展示机制。它们都可以用于在浏览器中展示数据。
JavaScript 对象标记(又称为 JSON)更加灵活。
- JSON 是轻量级文本数据交换格式
- JSON 与语言无关
- JSON 是”自描述的“,并且容易理解
JSON 使用 JavaScript 语法来描述数据对象,但 JSON 依然是语言和平台无关的。
JavaScript 程序可以使用内建的 eval() 函数来执行 JSON 数据并创建原生的 JavaScript 对象。
XML 和 JSON 共同目标是可以轻松解析、分享和使用包含在对象中的传输数据。很多人喜欢使用 JSON 是因为它的结构比 XML 简单,而且处理速度更快。
保存对象的状态以便以后使用,这一概念被称为持久化。我们使用术语持久化对象来定义一个不依赖与单个应用程序的对象,该对象可以被存储并稍后再次使用。
- 保存到平面文件中
- 保存到关系型数据库中
- 保存到对象数据库中
存储对象还要考虑的另一个问题是对象可以包含其他对象。
在上面 Java 序列化的例子中,方法并没有被显示保存。注意我们已经说明 Java 即用于存储对象,也用于恢复对象。事实上定义类时就限制了存储和恢复时对象的类型都必须是同一个类。因此方法自身无需保存到数据存储中。
公司如果使用对象数据库,则需要将所有数据从关系型数据库中转换到对象数据库中。这有很多缺点:
- 第一,任何做过数据库间数据迁移的人都知道这事非常痛苦的过程。
- 第二。即使数据转换成功了,也没有任何方式知道数据库工具的改变将如何影响应用程序代码。
- 第三,当发生问题时(这种情况经常发生),很难确定是数据库导致的问题还是应用程序代码导致的问题。
作为专业开发人员的最酷的现实之一是改变永无止境。
电子邮件的出现揭示了”分布式计算“这一概念。
我们已经说过C++不是真正的面向对象的编程语言,而是基于对象的编程语言。
注意任何 Web 应用都需要在客户端和服务器做验证,因为在客户端有方式可以绕过客户端验证直接向服务器发送数据,或者用户可以直接禁用客户端脚本而发送非法值。解决该问题有几个关键点需要考虑:
- 向服务器端发送信息需要更多的时间成本。
- 向服务器端发送信息会增加网络传输。
- 向服务器端发送信息会占用服务器资源。
- 向服务器端发送信息会存在潜在的错误。
基于这些原因以及其他潜在的问题,最终目标是在客户端进行尽可能多的验证。
JavaScript 和大多数脚本语言都是基于对象的。可以认为脚本语言是传统的编程范式和面向对象范式之间的桥梁。
客户端 JavaScript 通常存活在浏览器作用域中。
尽管 Java 和 JavaScript 都基于 C 语法,但他们没有什么直接关系。
很多对象类型可以直接内置到HTML文档中。网页控制器由一组预先构建的对象组成。可以使用<object>
标签来使用这些对象。<object>
标签也可以用于在浏览器中内置和启动各种音乐播放器。启动的播放器类型取决于浏览器加载的默认播放器。
对企业计算最基本的定义是它本质上是分布式计算。分布式计算文如其名,指一组分布式的计算机通过网络一起工作。分布式计算的力量在于计算机可以共享网络。
企业系统都是基于分布式对象构建的。使用分布式对象有很多优势。最大优势是理论上系统可以调用处于网络任何位置的对象。这是一种非常强大的能力,而且是当今基于互联网的业务的基石。另一个主要优势是可以通过网络中的多台服务器分发系统服务。
我们使用W3C提供的对Web服务的通用定义,即”客户端和服务器端使用基于SOAP(simple object access protocol,简单对象访问协议)标准的XML消息进行通信”。
SOAP是一项通信协议,用于通过互联网发送消息。我们可以描述SOAP为:SOAP是基于XML的用于分布式应用程序的协议。
远程程序调用(RPC)是一个通信机制,允许通过共享的网络调用其他计算机上的服务(对象)。
这种方式叫作表征状态转移,也叫作ReST。ReST 是一种无状态的协议,基本上依赖于HTTP。由于HTTP是互联网自身的基石,很大程度上可以说互联网的架构基于ReST,这通常称为RESTful架构。
软件开发的有趣之处在于,当设计软件系统时,实际上是在对现实世界系统进行建模。
设计模式归为三类,分别是创建型模式、结构型模式和行为型模式。
- 模式名称,使用一到两个词语来描述一个设计问题、对应的解决方案以及后果。
- 适用于该模式的待解决的问题,需要解释该问题的详细内容。
- 解决方案,描述了设计方案,比如类与对象之间的关系,各自的职责和协作等。
- 效果,效果是指应用该模式的结果以及利弊。
模型是应用程序对象,视图是屏幕显示,控制器则定义了用户接口如何响应用户输入。
- 创建型模式。帮你创建对象,你无需直接实例化对象。你可以根据给定的条件创建对象,这给程序带来更大的灵活性。
- 结构型模式。将一组对象组合更复杂的结构,比如复杂的用户接口或者账单数据。
- 行为型模式。定义系统中对象之间的通信方式,控制复杂程序中的流向。
创建型模式包含以下模式:
- 抽象工厂模式
- 构造器模式
- 工厂方法模式
- 原型模式
- 单例模式
请记住面向对象的重要规则之一是对象的职责由自身管理。
结构型模式用于使用一组对象来创建更复杂的结构,包括:
- 适配器模式
- 桥接模式
- 组合模式
- 装饰器模式
- 外观模式
- 轻量模式
- 代理模式
行为型模式包含以下类别:
- 责任链模式
- 命令模式
- 解释器模式
- 迭代器模式
- 中介者模式
- 备忘录模式
- 观察者模式
- 状态模式
- 策略模式
- 模板方法模式
- 访问者模式
设计模式是从有益的经验中总结出来的,反模式则来自于失败的经验。大多数软件项目最终不成功的原因都会被记载下来,最终总结为反模式。