Week 10
OOP与FP的程序分解 OOP Versus Functional Decomposition
- 在函数式编程中,我们将一个完整的程序分解成数个小函数,每个函数使用其参数完成一些特定操作
- 在面向对象编程中,我们一个程序分解成一个个类,每一个类中的方法对一些数据执行一些行为,这些数据即是其所在类表示的
- OOP和FP实际是将同一个事情以两种完全不一样的方式做出来
- OOP通过数据组织程序
- FP通过功能组织程序
- 具体孰优孰劣,取决于个人在实现特定项目下的思路,哪种合适就用哪个
- 代码布局很重要,但是代码之间的联系是一个多维结构,因此很难完美地设计程序布局
- 如果以一个矩阵关系来描述,那么每一行是一类数据,每一列是一类方法,那么OOP就是按行组织程序,FP就是按列阻止程序
加入操作或变化 Adding Operations or Variants
-
对于已有的代码矩阵关系,如果新加入一个数据或者一个操作,FP和OOP的解决方式不同
-
对函数式编程
- 加入新的操作十分容易——只需要额外在矩阵关系上添加一列,增加一个新的函数
- 加入新的数据就比较麻烦——增加一行,就影响的所有既有列,需要修改所有的既有函数
- 对于ML,其类型系统可以在未使用通配符的情况下,避免漏下额外的类型
-
对面向对象变成
- 加入新的数据非常简单——添加一个子类型
- 加入一个新操作需要修改原有类,但是在一些静态类型OOP语言中,可以在超类中生命该方法,那么就能够有一个像ML的机制避免漏下部分类型的实现
-
整体来讲,在不改变原有代码的情况下,函数式编程可以加入新的操作,而面向对象编程可以加入新的数据
-
FP可以在预先计划好的情况下添加新的数据:让类型构造器更加有扩展性,让函数能够对这些扩展执行操作
-
OOP可以在预先计划好的情况下添加新的操作,使用访问者设计模式(Visitor Pattern),将操作定义为访问者类,让对象执行之(使用双重分发,Double Dispatch)
-
让软件更具有扩展性非常重要,同时又很困难
- 如果希望未来加入新操作,使用FP
- 如果希望未来加入新数据,使用OOP
- 两者都要?比较麻烦(Scala语言)
-
扩展性是一把双刃剑
- 为后期修改做准备将利于代码的可复用性
- 会让代码的可读性变差
- 通常会有一些机制限制语言的扩展性
使用函数式分解二元方法 Binary Methods with Functional Decomposition
- 函数式分解可以很好地解决一个操作涉及到多种类型数据作为参数的情况
- 当一个函数可以接受多种类型的数据,如果接受位为两个(两个参数),那么会形成一个有关各类型参数的二维表格,此时称这个函数为二元方法
- 针对上诉各类数据参数情况表格完成函数式问题分解,每一个空格即一种情况
双重绑定 Double Dispatch
- 使用OOP方法解决上述二元方法的分解问题
- 一种直观的方法,就是在其中一种类型中定义对于所有类型的加法辅助方法
- 这样需要对另外一个对象进行类型判断,不是纯粹的OOP
- 双重绑定,再调用了第一个对象的加法辅助方法
add_value
之后,再调用另外一个对象的方法,告诉另外一个对象当前对象是什么(通过调用不同的方法,传入self
) - 就相当于,在每个对象中有一个加法辅助方法,这个方法不会判断另外一个对象的类型,而是直接调用另外一个对象的用于当前对象(我们已知这个对象是什么类型)的加法方法
- 第一个对象中有一个方法
add_value
- 第二个对象钟有一个方法
addc1
- 在第一个对象的方法中调用
v2.addc1
- 以告诉另外一个对象本对象是什么类型(这个比较容易的值)的方式,解决当前对象不知道另外一个对象是什么类型的问题
- 第一个对象中有一个方法
- 在这个问题上,函数式编程更胜一筹
多方法 MultiMethods
- 对于上面这种实现技巧,OOP实际上还有一种多方法设计特性,即只在每种类型中定义三个
add_value
方法,只是每个方法接受的参数是指定类型的,运行时会因为参数类型的不同执行到指定的方法中 - 这种特性称之为多方法或者多重绑定
- 基本思想:
- 允许多方法重名
- 不仅需要知道哪个对象被调用方法,同时还需要知道接受的实例是哪个类
- 使用在消息接收者和参数上的动态绑定决定调用哪个具体的方法
- 一个小问题:考虑到子类型,确定具体的调用方法可能会比较复杂
- ruby并不适用多方法
- 对于传入方法的参数的信息没有具体限制
- 不允许重名函数——只存在覆盖或替换
- 对Java/C#/C++,其使用的是静态重载机制,而非多方法
多重继承 Multiple Inheritance
- 一个类具有多个超类的情况
- 限制多个超类的原因
- 语义有时很复杂
- 静态类型检查更加困难
- 让高效的语言实现更加困难
- 子类和超类可能具有歧义
- 直接超类、子类
- 间接超类、子类
- 单继承,类的分层结构是一棵树
- 节点是类
- 父结点是直接超类
- 允许任意多个子节点
- 多重继承,分层结构不再是一棵树,但仍不允许环路存在(有向无环图)
- 多重继承的一些疑问
- 如果多重继承的两个分支均声明了同一个方法,那子类的方法来自于哪一个分支(
super
方法将调用哪一个实现会变得很复杂,这里需要引导式的重新调用如Z::super
) - 如果两个分支的共同超类定义了一个方法,其中一个分支进行了重载,那么最终的子类继承了哪一个分支(可以像上一个方法那样解决之)
- 如果共同超类定义了一个域,那么最终子类继承一份还是两份改域的拷贝
- 如果多重继承的两个分支均声明了同一个方法,那子类的方法来自于哪一个分支(
混型 Mixins
-
混型,一个方法的集合(仅是一个集合)
-
混型次于一个类,不能够实例化
-
具有混型的语言,一般允许继承一个超类,但可以包含多个混型
-
其语义为,将一个混型纳入到一个类中,那么就是将其拥有的方法纳入到这个类中(避免了代码的直接复制)
- 按照混型包含到类定义中的顺序扩展或覆盖方法
- 比助手方法更强大,因为混型中的方法可以使用
self
的方法,这些方法并没有定义在混型中
-
定义方法和类定义相似,只是用
module
关键字 -
查找规则
- 查找一个对象
obj
的方法m
,则先查找类定义,再查找混型,然后在查找直接超类,再查找直接超类的混型,依此类推 - 对于实例变量,混型方法可以访问之,但是这并不是一种好的代码风格,因为可能会因为名字冲突造成意外后果
- 查找一个对象
-
ruby中两类最受欢迎的混型
- 比较器
Comparable
:使用<=>
定义了<, >, ==, !=, >=, <=
,相等返0,大于返1,小于返-1 - 枚举器
Enumerable
:使用each
定义迭代器
- 比较器
-
混型很擅长使用已经定义好的自定义方法来实现一些通用方法
-
混型并不能完全替代多继承(不能解决使用同名实例变量的情况)
接口 Interfaces
- 接口是一个与混型很大不同的事物——有关于静态类型,对于ruby这样的动态类型完全不适用
- 在Java中,类(Class)是一个类型(Type)
- 方法的参数都有对应类型
- 相应地,有子类型等概念,和对待超类与子类类似
- 接口是一个类型,但不是一个类
- 只有方法的声明(签名),没有方法的定义(这一点不同于混型)
- 不能在接口上使用
new
(这一点和混型相同)
- 实现一个接口
- 可以继承于一个类,但是可以实现任意数量个接口
- 这意味着这个类需要正确地实现接口中的所有方法
- 这个类就成为了接口的一个子类型
- 可以继承于一个类,但是可以实现任意数量个接口
- 多重接口
- 接口不提供方法或者域
- 接口可以根据接口调用方法而无视具体类别,更加灵活
- 但是动态类型本身就已经比类型系统灵活了——在动态语言里,接口完全没有必要
抽象方法 Abstract Methods
- 明确要求的覆盖
- 一些类可能希望任何继承于其的子类都覆盖一些方法,而超类本身在于保持一些公共的域和方法
- ruby的方法
- 不在超类中定义这些方法
- 在子类中加入
- 创建超类类型将会导致”方法丢失错误“
- 对于静态类型
- 上述方法是无法通过类型检查的
- 一种方法:会抛出错误的实现
- 更好的方法:使用类型检查器管理之
- 抽象方法:类型检查层面的需要的覆盖,给出签名,要求子类实现,同时禁止对超类的实例化
- 编译时检查错误
- 告知读者其编码者意图
- 保持原有语言能力
- 将代码传递到另外的代码中
- 抽象方法和动态分发是OOP的从子类传递代码到超类的方式
- 高阶函数即FP的传递代码的方式
- C++中没有接口
- 如果同时拥有抽象方法和多继承,接口没有必要
- 使用一个全是抽象方法的基类代替接口
- 多继承代替多接口