面向对象思想
# 程序设计思想的发展历程
面向机器:使用机器语言编程,例如 0000 代表 Load 操作,0001 代表 Store 操作。后来有了汇编语言编写的程序,例如使用 Load 代表 0000,使用 Store 代表 0001。
面向过程:使用高级编程语言不用再关注与机器本身操作相关的事情,而是专注于解决具体问题,这样大大提高了开发效率。面向过程编程是一种以过程为中心的编程思想,也可称之为"面向记录"编程思想。它强调流程化、线性化、步骤化的思考方式,主要是把解决问题的步骤分析出来,然后用函数实现这些步骤,在需要的时候按顺序调用函数。面向过程的编程思想具有效率高、流程明确的优点,但缺点是代码重用性低,扩展能力差,维护难度比较高。
结构化程序设计: 是"面向过程"方法的改进,结构化程序设计(structured programming)是进行以模块功能和处理过程设计为主的详细设计的基本原则。结构化程序设计是过程式程序设计的一个子集,它对写入的程序使用逻辑结构,使得理解和修改更有效更容易。
第一次软件危机:解决办法是结构化程序设计。
第二次软件危机:解决办法是面向对象程序设计。
提示
软件危机:落后的软件生产方式无法满足迅速增长的计算机软件需求,从而导致软件开发与维护过程中出现一系列严重问题的现象。
# 面向对象的主要思想
分而治之。
高内聚低耦合。
封装变化。
# 面向对象思考
面向对象概念
学习面向对象的概念与学习使用面向对象语言进行编程有着巨大差异,理解这一点很重要。因此,在学习使用面向对象的开发环境之前,先学习基本的面向对象概念至关重要。与其直接学习一门编程语言,不如把时间花在学习面向对象的思考过程上。显然,我坚定地认为,熟悉面向对象的思考过程优先于学习编程语言或建模语言。
软件行业的技术变迁非常快,而概念则是逐步演进的。面向对象的思想也是如此。
实现细节对于用户是隐藏的。我们必须时刻牢记关于实现的一个目标,那就是修改实现不需要变动用户代码。可能你看起来有些困惑,但该目标是设计问题的核心所在。
对象
对象是面向对象程序中的基础材料,使用面向对象技术的程序本质上是对象的集合。
对象的基本定义是一个包含了数据和行为的实体。将属性和方法合并到同一个实体中,在面向对象中这种方式叫作封装。面向对象编程的最大优势是数据和对数据的操作都被封装在一个对象中。在面向对象术语中,数据被称为属性。存放在对象中的数据代表了该对象的状态。对象的行为表示对象可以做什么。
结构化编程中数据往往与程序分离,而且数据是全局的,所以在代码作用域之外依然可以很容易地修改数据。这意味着对数据的访问是失控的,并且不可预测(因为很多函数都可以访问全局数据)。而且,由于你无法控制谁能访问数据,因此测试和调试将变得更加困难。对象通过将数据和行为组合到一个完整的包中解决了这些问题。
限制访问具体属性和方法的行为叫作数据隐藏。取值方法和赋值方法的理念就是数据隐藏。通常,一个对象不应当操作其他对象的内部数据。因为其他对象不应该直接操作另一个对象中的数据,而取值方法和赋值方法提供了对对象数据的访问控制。在任何情况下,对对象中属性的访问应该由该对象自身控制,任何一个对象都不应该直接修改其他对象的属性。使用对象的一个显著好处是对象无须暴露它的所有属性和行为。在出色的面向对象设计(至少通常认为是好的设计)中,对象仅暴露必要的接口来和其他对象进行交互。除了如何使用该对象,其他细节都应当对其他对象隐藏起来----基本上是一个"需要知道"的基础。封装是基于对象既包含属性也包含行为这一事实定义的。数据隐藏是封装的主要部分。
从概念层面讲,对象是拥有某种责任的抽象。从规格层面讲,对象是一系列可以被其他对象使用的公共接口。从语言实现层面讲,对象封装了代码和数据。
类
类是创建对象的模板。当对象被创建时,我们看到对象被实例化。简单地说,类是对象的蓝图。你实例化一个对象时,你基于类来构建这个对象。
从用户角度设计类而不是从信息系统的角度设计类是至关重要的。
面向对象设计的基本单位是类。培养面向对象思考过程的良好习惯需要注意三个方面:清楚接口和实现之间的区别,抽象地思考,给用户提供尽可能少的接口。正确地设计类时要注意两部分:即接口和实现。
超类,也称为父类(有时候也叫基类),包含了继承自它的所有类的公共属性和行为。
子类,也称为孩子类(有时被叫作衍生类,扩展类),是超类的扩展。
接口
在大多数面向对象的语言中,访问修饰符指定为 public 的方法属于接口。为了实现数据隐藏,必须将所有属性声明为 private。属性绝不是接口的一部分。只有公共方法是类接口的一部分。将属性描述为 public 破坏了数据隐藏这一理念。
呈现给终端用户的服务构成了接口。最佳实践中,只呈现给用户他们需要的服务。
如果接口设计得当,实现的修改不需要对用户代码做任何改变。
得到最终的接口始终是一个迭代的过程。
继承
我想说只有两种方式来使用其他类构建新类,它们就是继承和组合。我们之前已经讨论过了继承关系是 is-a 关系的原因,而组合关系可以称为 has-a 关系。
抽象
现在,抽象和重用的联系在哪里?请你自问这两个场景哪个更具重用性,是抽象的还是非抽象的?我们来更进一步简化问题,以下哪个短语更具重用性?是"送我到机场"还是"右转,右转,然后再左转,左转,左转"。显而易见,第一个短语更具重用性。你可以在任何城市中使用它,只要你钻进一辆出租车并且想去机场。第二个短语只在特定的情况下可行。因此,抽象接口"带我去机场"更具通用性。可重用的面向对象设计针对芝加哥、纽约或克利夫兰的实现是不同的。
面向对象与编程语言
各种面向对象编程语言相互有别,但都能看到它们对面向对象三大机制的支持:"封装、继承、多态"。封装:隐藏内部实现。继承:复用现有代码。多态:改写对象行为。
任何一个严肃的面向对象的程序员,都需要系统地学习面向对象的知识,单纯从编程语言上获得面向对象知识,不能够胜任面向对象设计和开发。通过面向对象编程语言认识到的面向对象,并不是面向对象的全部,甚至只是浅陋的面向对象。
面向对象的本质问题:我们为什么要使用面向对象?我们应该怎样使用三大机制来实现好的面向对象?我们应该遵循什么样的面向对象原则?
如何设计好的面向对象?遵循一定的面向对象设计原则。熟悉一些典型的面向对象设计模式。
如何从设计原则到设计模式?
针对接口编程,而不是针对实现编程:客户无需知道所使用对象的特定类型,只需要知道对象拥有客户所期望的接口。
优先使用对象组合,而不是类继承。类继承通常为白箱复用,对象组合通常为黑箱复用。继承在某种程度上破坏了封装性,子类父类耦合度高。而对象组合则只要求被组合的对象具有良好的定义的接口,耦合度低。
封装变化点:使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次之间的松耦合。
使用重构得到模式:设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。没有一步到位的设计模式。
# 面向对象的设计原则
- 单一职责原则(Simple Responsibility Pinciple,SRP):一个类应该有且仅有一个引起变化的因素。【一个类的定义】
- 开闭原则(Open-Closed Principle,OCP):对扩展开放,对修改封闭。面对新需求,对程序的改动是通过增加新代码来实现的,而不是修改现有的代码。【目标】
开闭原则是面向对象编程的核心原则。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用和灵活性好。开发人员应该仅对程序中呈现出频繁变化的那部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。
对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者.EXE文件,都无需改动。
- 里氏替换原则(Liskov Substitution Principle,LSP):任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受影响时,基类才能真正的被复用,而衍生类也能够在基类的基础上增加新的行为。里氏替换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。【继承后的重写】
- 依赖倒转原则(Dependence Inversion Principle,DIP):高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。简单的说就是要求对抽象(接口)进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。【依赖抽象】
依赖倒置是面向对象设计的标志,使用哪种语言编写程序并不重要,编写时如果考虑的都是针对抽象编程,而不是针对细节编程,即程序中的所有依赖关系都是终止于抽象类或者接口,那就是面向对象的设计,反之就是过程化的设计了。
- 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。【功能拆分】
- 迪米特法则(Least Knowledge Principle,LKP):迪米特法则又叫做最少知识原则,就是说一个对象应当对其它对象有尽可能少的了解,不和陌生人说话。【类与类的交互原则】
在类的结构设计上,每一个类都应当降低成员的访问权限。也就是说一个类包装好自己的private状态,不需要让别的类知道的字段和行为就不要公开。需要公开的字段,通常用属性来体现。
迪米特法则的根本思想是强调了类之间的松耦合。
类之间的耦合越弱越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说信息的隐藏促进了软件的复用。
- 合成复用原则(Composite/Aggregate Reuse Principle,CARP):合成复用原则要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。优先使用对象的合成复用将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。【复用的最佳实践】
# 面向对象的三大特征
封装:如何组织类或模块,让封装的类或组件,尽量只负责一个领域的工作。
继承:复用方式之一,概念形成统一,通过继承可以管理多个概念。
多态:一个行为的不同做法,目标一致,实现的方式不同。
# 类与类的六大关系
耦合关系从弱到强
依赖(Dependency)
关联(Association)
聚合(Aggregation)
组合(Composition)
泛化(Generalization)
实现(Realization)
详细说明
依赖关系(Dependency)
简单的理解,依赖就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是类B的变化会影响到类A。比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。表现在代码层面,为类B作为参数类A在某个方法中使用。在UML类图设计中,依赖关系用由类A指向类B的带箭头虚线表示。

关联关系(Association)
关联体现的是两个类之间语义级别的一种强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。当一个类知道另一个类的时候可以用关联关系。表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量。在UML类图设计中,关联关系用由关联类A指向被关联类B的带箭头实线表示。

聚合关系(Aggregation)
聚合是关联关系的一种特例,它体现的是整体与部分的关系,即 has-a 的关系。聚合体现的是一种弱拥有关系。此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。比如计算机与CPU、公司与员工的关系等。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,聚合关系以空心菱形加实线表示。(菱形指向整体)

组合关系(Composition)
组合也是关联关系的一种特例,这种关系比聚合更强,也称为强聚合。体现了一种强拥有关系。它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束,比如人和人的大脑。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,组合关系以实心菱形加实线表示。

继承关系(Generalization)
继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。在UML类图设计中,继承用一条带空心三角箭头的实线表示,从子类指向父类,或者子接口指向父接口。

实现关系(Realization)
实现指的是一个class类实现interface接口(可以是多个)的功能,实现是类与接口之间最常见的关系。在UML类图设计中,实现用一条带空心三角箭头的虚线表示,从类指向实现的接口。

如何区别聚合、关联、组合和依赖关系
关联是朋友关系,依赖是临时朋友关系、聚合是弱拥有关系、组合是强拥有关系。
# 业务分析基本方法
识别对象:从需求中找出参与的对象有哪些?
识别对象最简单的方法就是在需求中找名词。
分配职责:为每个对象分配相应的职责。
分配职责最简单的方法就是在需求中找动词,并发掘潜在职责。
建立交互:确定对象之间的调用接口,完成交互。
继承→