松本行弘的程序世界 2 面向对象
编程和面向对象的关系
计算机只会高速运算,到底是最大程度地发挥计算机的能力还是扼杀它的能力都取决于我们编写的程序。
颠倒的构造
程序员要夺得主动权。
主宰计算机的武器
为了能够主宰计算机,必须以计算机的特性和编程语言作为武器。
怎样写程序
编程风格 算法 数据结构 设计模式 开发方法
面向对象的编程方法
smalltalk为面向对象编程语言之母。
面向对象的难点
面向对象编程语言中最重要的技术是“多态性”。
多态性
多态就是可以把不同种类的东西当做相同的东西处理。
操作对象是三个箱子,分别是盖着盖子的箱子、加了锁的箱子、系着彩带的箱子。在编程中,“打开箱子”的命令,称之为消息;打开不同箱子的具体操作,称之为方法。
具体的程序
调用box_open这个方法,根据参数(箱子的种类)的不同做相应的处理。
但是如果增加种类代码就要重写,修改的地方越来越多,追加箱子的种类就会变得非常困难。
修改程序,实现真正的多态。“.”可以理解为“给前面的式子的值发送一个open消息”。
多态性的优点
首先,各种数据可以统一地处理。关注要处理什么,而不是怎么处理。
其次,根据对象选择方法,程序内部不会冲突。减轻程序员的负担。
再次,新数据简单的追加可以实现,不需要改动以前程序,具备了扩展性。
数据抽象和继承
面向对象编程的三原则:多态性,数据抽象,继承。别称:多态性:动态绑定,数据抽象:信息隐藏或封装。
面向对象的历史
simula的“发明”
面向对象编程思想起源于瑞典20世纪60年代后期发展起来的模拟编程语言Simula。以前,表示模拟对象的数据和实际的模拟方法是互相独立的,需要分别管理。
Smalltalk的发展
smalltalke的开发宗旨是“让儿童也可以使用”,吸收了Simula的面向对象思想,且独居一格。不仅如此,还有一个很好的图形用户界面。使得世人开始了解面向对象编程的概念。
Lisp的发展
许多重要的面向对象的概念都是从Lisp的面向对象功能中诞生的。
和c语言的相遇
C++是直接受到了simula语言的影响而没有受到smalltalk多大影响。
Java的诞生
java语言放弃了和C语言的兼容性,并增加了Lisp语言中一些好的功能。此外,通过Java虚拟机(JVM),java程序可以不用重新编译而在所有操作系统中运行。
Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
复杂性是面向对象的敌人
软件开发的最大敌人是复杂性。人类的大脑无法做台复杂得处理,记忆力和理解力也是有限的。
在计算机性能这么高的今天,人们为了找到迅速开发大规模复杂软件的方法,哪怕牺牲一些性能也在所不辞。
结构化编程
如果不考虑黑盒内部的处理,系统复杂性就可以降低到人类的可控范围内。针对程序控制流的复杂问题,结构化编程采用看限制和抽象化的武器解决问题。
数据抽象化
数据抽象是数据和处理方法结合。对数据内容的处理和操作,必须通过实先定义好的方法来进行。数据和处理方法结合起来成为了黑盒子。
比如说栈。
有了数据抽象,程序处理的数据就不再是单纯的数值或者文字这些概念性的东西,而变成了人脑容易想象的具体事物。而代码的“抽象化”则是把想象的过程“具体化”了。
雏形
同样的对象大量存在时,为避免重复,可以采用两种方法管理对象。
- 原型。用原始对象的副本来作为新的相同的对象,JS用的原型。
- 模板,称为类。
跟原型不同,面向对象编程语言的类和对象有明显区别。对象又称作实例。
找出相似的部分来继承
类的增多会用到很多性质相似的类。这就违背了我们的DRY(Don’t Repeat Yourself)原则。把这些相似的部分汇总到一起就好了。
继承就是这种方法。子类继承父类所有方法,如有需要可以增加新的方法,也可以重写父类方法。
Ruby跟多数编程语言一样,一个子类只能有一个父类,即单一继承,C++、Lisp等编程语言中,一个子类可以有多个父类,这称为“多重继承”。
多重继承的缺点
继承的原本目的实际上是逐步细化。所以为解决抽出类中相似部分的问题并不完全准确。
为什么需要多继承
一个程序员也可能是一个作家。
多重继承和单一继承不可分离
单一继承的特点:
- 继承关系单纯,有利有弊
多重继承的特点:
- 很自然的做到了单一继承的扩展。
- 可以继承多个类的功能。
单一继承可以实现的功能,多重继承都可以实现,但是类之间的关系变得复杂。这是多重继承的一个缺点。
goto语句和多重继承比较相似
多重继承导致的问题:
- 结构复杂化
- 优先顺序模糊
- 功能冲突
解决多重继承的问题
继承作为抽象化的手段,是需要实现多重继承功能的,如果一个类只允许抽出一个功能,那么限制就太多了。
受限制的多重继承,这个解决或改善多重继承问题的方法出现了,它在Java中被称为接口(interface),在Lisp或Ruby中是Mix-in。
静态语言与动态语言的区别
编程语言可以分为静态语言和动态语言两种。像Java这样规定变量和算式类型的语言称为静态语言。
在静态语言中,不能给变量赋不同类型的值,会导致编译错误,不通过执行就可以发现类型不匹配是静态语言的一个优点。
如果只能给一个变量赋值同类对象,就不可能根据对象的类自动选择合适的处理方式(多态性)。
静态语言的特点
当给一个类变量赋值时,既可以用这个类的对象来赋值,也可以用这个类的子类对象来赋值。这样就可以实现多态性。
但是这时,如果定义了父类变量,赋值子类对象,变量不能调用子类特有的方法。
动态语言的特点
动态语言允许调用没有继承关系的方法。静态语言中只能调用有继承关系的方法,数组、哈希表和字符串都能调用的方法,只能是在它们共同的父类(恐怕是Object)中定义。
在静态语言中,如果要调用类层次中平行类的方法,那么必须要有一个可以表现这些对象的类型。如果没有这个类型,可调用的方法是十分有限的。由此我们看到静态语言中某种形式的多重继承是不可少的。
静态语言与动态语言的比较
静态语言即使不通过执行也可以检查出类型是否匹配。但是逐个来定义算式和变量的类型又会使程序变得冗长。只有包含继承关系的类才会具有多态性。
对于动态语言来说,静态语言显得限制过多,灵活性差。程序中有没有错误只有执行了才会知道。程序中没有类型定义,这样程序会变得很简洁。只要方法名一样,这些对象可以以相同的方式去处理。这样生产效率会大大提高,这种宽松的编程机制称为Duck Typing(鸭子类型检测)
继承的两种含义
继承包括两种含义,一种是“类都有哪些方法”,也就是说这个类都支持些什么操作,即规格的继承。
另一种是“类中都用了什么数据结构什么算法”,也就是实现的继承。
静态语言中这两者的区别很重要,动态语言中区别规格的继承和实现的继承意义不大。即使没有继承关系,方法也可以自由地调用。
java对两者有很明确的区分,实现的继承用extends来继承父类,规格的继承用implements来指定接口。
类是用来指定对象实现的,而接口只是指定对象的外观(都有哪些方法)。类的继承是单一的,implements可以指定多个接口。接口对实现没有任何限制,也就是说,接口可以由跟实现的继承没有任何关系的类来实现。
接口的缺点
为了解决多重继承的问题,人们允许了规格的多重继承,但是还是不允许实现多重继承。
继承实现的方法
和静态语言Java不同,动态语言本来就没有继承规格这种概念。动态语言需要解决的就是实现的多重继承。
Lisp、Perl和Python都提供了多重继承功能,这样就不存在单一继承的问题了。
从多重继承变形而来的Mix-in
Mix-in是降低多重继承复杂性的一个技术,最初是在Lisp中开始使用的。按以下规则来限制多重继承:
- 通常的继承用单一继承
- 第二个以及两个以上的父类必须是Mix-in的抽象类。
Mix-in类是具有以下特征的抽象类。
- 不能单独生成实例
- 不能继承普通类
积极支持Mix-in的Ruby
1 | 我理解的接口的作用:接口是一种协议,满足同一接口实际上是告诉计算机你们是满足了同一样协议,你们是有关系的,java以这种方式间接实现了多态。当然可以实现多个接口,抽象为不同的“种类”,这样就相当于某种意义上的”多继承“。 |
两个误解
- 对象是对现实世界中具体物体的反应,继承是对物体分类的反应。(误)
- 多重继承是不好的。Mix-in不错。(误)
澄清:如果多继承用的不好就会出问题,Mix-in只不过是实现多重继承的一个技巧而已。
面向对象的编程
不管过去怎样,现在面向对象最好的理解是,面向对象编程是结构化编程的延伸。
随着软件的复杂化,开发越来越复杂,Dijkstra提倡把程序控制限制为(1)顺序(2)循环(3)分支 使得程序变得简单且容易理解。面向对象的设计方法是在结构化编程对控制流程实现了结构化后,又加上对数据的结构化。
面向对象编程,封装(黑盒)和多态(减少分支处理)是提高生产率的技术。
结构化编程通过整理数据流,提高程序的生成效率和可维护性。同样,面向对象编程通过对数据结构的整理,提高了程序的生产效率和可维护性。
对象的模板=类
类是吧数据黑盒化的工具,由于对类内部的操作都是通过类的方法来实现的,所以内部数据结构即使在以后发生变化,对外部也没有影响。
利用模板的手段=继承
类是模块,继承就是利用模块的方法。传统的面向对象编程语言是一下子把规格和实现都继承下来,在最近,有的是把这两种继承分开了。比如java里的接口就是规格继承。
多重继承不好吗
单一继承的类之间的关系是很单纯的树结构。但是对于多重继承而言,类之间的关系却是复杂得网状结构。
静态语言中可以实现多态性只是局限于拥有共通父类的对象。
为了解决这个问题,静态面向对象编程语言的代表C++支持多重继承。java也可以通过接口来支持规格的多重继承。1
2
3
4**静态类型语言**: 是指在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型,某些具有类型推导能力的现代语言可能能够部分减轻这个要求.
**动态类型语言**: 是在运行时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。
**强类型语言**: 是一旦变量的类型被确定,就不能转化的语言。实际上所谓的貌似转化,都是通过中间变量来达到,原本的变量的类型肯定是没有变化的。
**弱类型语言**: 则反之,一个变量的类型是由其应用上下文确定的。比如语言直接支持字符串和整数可以直接用 + 号搞定。当然,在支持运算符重载的强类型语言中也能通过外部实现的方式在形式上做到这一点,不过这个是完全不一样的内涵
动态编程语言也需要多重继承
动态编程语言没有类型检查,从这方面来说没有理由用多重继承,但是动态编程语言肯定需要多重继承。
实现共享可以通过多个对象的组合(composition)和委托(delegate)来做到。
驯服多重继承的方法
多重继承可能引发的问题:
- 类关系复杂化
- 继承功能名字重复
最初的问题是因为类关系从简单的树结构变成了复杂的网状结构。
父类的优先级并不明确,多重继承设计的一个有效的技巧是Mix-in。用Mix-in做多继承设计时,从第2个父类开始的类要满足以下条件。
- 不能单独生成实例的抽象类。
- 不能继承Mix-in以外的类。
抽象类和接口的对比
mix-in的例子
通过对功能的分离,多重继承就可以由单一继承加上Mix-in类来实现。利用Mix-in就可以同时享有单一继承的单纯性和多重继承的共有性。
对于名字重复问题(如函数名),多重继承编程语言都有自己的应对方法,大致分为以下3种:
- 给父类定义优先级
- 把重复的名字替换掉
- 指定使用类的名字
ruby中多重继承的实现方法
Mix-in
java实现多重继承的方法
接口。。
Duck Typing诞生之前
静态是指程序执行之前,从代码中就可以知道一切。程序静态的部分包括变量、方法的名称和类型以及控制程序的结构等等。
相对于静态,动态是指在程序执行之前有些地方是不知道的。程序动态的部分包括变量的值、执行时间和使用的内存等等。
通常情况下、程序本来就是不被执行就不知道结果的,所以在一定程度上说程序都具有动态特性。因此,严格说静态和动态之间的界限是很微妙的。
为什么需要类型
动态的类型是从Lisp中诞生的
动态类型在面向对象中发展起来了
对象保存着有关自己种类的信息,某个变量可以用各种类型的数据来赋值,这两点是多态这一面向对象重要特性的必要条件。
动态类型和静态类型的邂逅
20世纪80年代,面向对象编程语言主流是包含动态数据类型的语言,但是在21世纪的今天,使用最广泛的面向对象编程语言是具有静态类型的java和c++。
受simula很大影响的c++引入了子类对象可以看成是父类对象这个原则,对象也采用了静态类型。
根据这个原则,在编译时就可以知道变量或算式的类型,又可以根据执行时的数据类型自动选择合适的处理,从而同时具备了静态类型的优点和动态类型的多态性。
静态类型的优点
- 最大的优点是在编译时能够发现不匹配的错误。
- 如果明确指定了数据类型,在编译时可以用到的信息就很多,利用这种信息可以在编译时对程序做优化,提高程序执行速度。
- 在读程序时提高理解度,IDE也可以自动补充。
问题:
- 不指定类型就写不了程序,数据类型只是一些辅助信息,并不是程序本质。有的类型声明仅仅是为了满足编译器的要求。程序规模也因为数据类型的定义而变大。
- 灵活性问题。静态类型本身限制了给某个变量只能赋值某种类型的对象,这种限制可能成为妨碍将来变化的枷锁。
动态类型的优点
- 源代码简洁,提高生产力。
- 会不会更难以理解,更突出程序处理的实质,程序函数相差几十倍并不少见,理解起来反而简单。
- 会不会运行更缓慢,同样的处理,在大多数情况下,静态类型编程语言运行得要快些。这是因为动态类型程序执行时要做类型检查。另外,静态类型的编程语言大都是通过编译把程序源代码转换成可以直接执行的形式,而动态类型的编程语言大多是边解释源代码(转换成内部形式)边执行,这种编译型处理和解释型处理的区别也是影响程序执行速度的愿意之一。
- 灵活,灵活性的关键是Duck Typing。
- 最大的缺点是不执行就检测不出错误。
只关心行为的Duck Typing
If it walks like a duck and quacks like a duck,it must be a duck.(走起路来像鸭子,叫起来也像鸭子,那么他就是鸭子)。
根本不考虑一个对象属于什么类,只关心它有什么样的行为(它有哪些方法)。
动态语言用Duck Typing的概念设计时需要遵循的原则最低限度是避免明确的类型检查。
避免明确的类型检查
克服动态类型的缺点
- 执行时才能发现可以用完备的单元测试来解决,如果能严格实行完备的单元测试的话,即使没有编译时的错误检查,程序的可靠性也不会降低。
- 读程序时可用到的线索少这一点可以通过完整的文档来解决。可以在源代码中同时写文档,减轻维护文档的负担。
- 运行速度慢,随着计算机性能的提升已经不再重要,现在的程序开发中,程序的灵活性和生产力更为重要。
动态编程语言
现在我们对程序开发生产力的要求越来越高,也就是说,要在更短的时间内开发出更多的功能。
尽快着手开发,快速应对需求变更的开发方式变得越来越重要。
在这种快速开发模式中,Duck Typing所代表的执行时的灵活性就非常有用。Ruby、Python、Perl和PHP等优秀的动态类型编程语言,因为它们在执行时所具有的灵活性而越来越受到人们的关注。
元编程
元编程是对程序进行编程的意思。
元编程
利用元编程可以动态生成类的方法,而且重要的是你可以自己编写生成过程,不支持元编程的编程语言实现这样的功能是很麻烦的,要么需要扩展语言的语法,要么用宏定义等预处理方法来实现。
反射
元编程的反射(reflection),在编程语言中它是指在程序执行时取出程序的信息或者改变程序的信息。
Ruby彻底实现了对程序的动态操作。
元编程的例子
Ruby使用Delegator这个库实现了委托功能,调用对象d的方法时可以转变为调用a的方法。我们还可以给这个对象增加特意方法来改变它的部分行为,这就大大扩展了它的应用范围。
使用反射功能
分布式Ruby的实现
Delegator将被调用的方法直接委派到其他对象,这一功能在很多领域都有应用。如dRuby。
dRuby是通过网络来调用方法的库。dRuby可以生成服务器上存在的远程对象(Proxy),Proxy的方法调用可以通过网络执行。
调用的方法在服务器上的远程对象中执行,执行结果可以通过网络返回。这和java的RMI(Remote Method Invocation)功能比较相似。但是,利用Ruby的元编程功能,不用明确定义接口,也可以通过网络调用任一对象的方法。
C++和Java的远程调用是用IDL(Interface Definition Language)等语言来定义接口的,自动生成的存根(stub)必须编译和连接。和这些相比,Ruby的元编程更简单。
数据库的应用
在数据库领域,元编程也很有用。
web应用程序框架Ruby on Rails(也称为Rails或RoR)中也应用了元编程。具体地说是在与数据库关联的类库(ActiveRecord)中,利用元编程简单的把数据记录定义为对象。
由于元编程功能让我们可以获取类名,在执行时增加方法。元编程的功能使得Rails被称赞为生产效率高的Web应用程序框架。
输出xml
手工写XML是很麻烦的,利用Ruby块功能则可以很方便的处理。
元编程和小编程语言
元编程功能可以运用到DSL领域,DSL是针对特定领域强化了功能的小规模编程语言。通过DSL用户可以强化应用程序的功能或者定制一些功能。
声明的实现
Ruby的方法可以读取或改变程序自身的状态,利用普通的方法调用,可以实现其他编程语言中声明所完成的工作。
上下文相关的实现
instance_eval方法接受块作为参数,把调用对象置换成self来执行块。
单位的实现
词汇的实现
针对特定领域,如果用Ruby来定义需要的类和方法,那么就可以认为Ruby是这个领域的专用语言。这些类和方法可以称为这个领域的词汇。
层次数据的实现
适合DSL的语言,不适合DSL的语言
Ruby是非常适合DSL的语言。
首先定义好DSL用的小语言,然后编译成C++、java等目标语言。编译器经常用到Ruby这种具有优秀文本处理的语言。
另一种实现DSL方法的例子是解释器。比如开发应用程序时从设计到实现是很复杂的,可以利用固定的语法和类库函数来读取程序。具体方法是用XML、DOM等XML处理类库来解释小语言语法。这是Java应用程序的配置文件曹勇XML的原因之一。通过XML文件,不用每次编译Java程序就可以改变配置,定制程序的行为。
这种用法可以把XML称为Java界的DSL,或是Java应用程序的脚本语言。1
2脚本语言又被称为扩建的语言,或者动态语言,是一种编程语言,用来控制软件应用程序,脚本通常以文本(如ASCII)保存,只在被调用时进行解释或编译。
早期的脚本语言经常被称为批处理语言或工作控制语言。一个脚本通常是--解释运行--而非编译。虽然许多脚本语言都超越了计算机简单任务自动化的领域,成熟到可以编写精巧的程序,但仍然还是被称为脚本。