将一个实例赋予它的父类变量时,其赋值语言缺省具有 Covariant(协变) 语意: 协变的语意是:当一个类型A(Animal)要接纳一个子类型B(Dog)的赋值时,尝试将B变为A的子类,系标记为+A。继承的父<-子方向为 A <- +A。 协变的语意和子类型多态的变态方向是相同的。正如上所示,Animal 是 Dog 的父类,因此将 Dog cast 成 Animal 是可以接受的。 容器类不具有类型继承关系,所以以下无法通过编译检查: 因为 MyList[Dog] 不是 MyList[Animal] 的子类,但是可以通过协变来声明他们参数之间的的关系从而声明成父子: 而 Contravariant(逆变)则将父子的关系声明为反方向: 逆变的语意是:当一个类型A(Animal)向子类型B(Dog)赋值时,尝试将B变成A的父类,标记为-A。继承的父<-子方向为 -A <- A 逆变声明通常是危险的,因为赋值语句中的类型变态的方向是从右向左。而如果赋值语句的左边(被赋值端)的类型是确定的,并且是赋值端(右侧)的子类,那不能满足类型转换条件。因为 assignment 通常允许我们做减法,可以想象将一个子类,比如 Dog 赋予它的父类 Animal 时,Dog 携带的信息等于或多余 Animal 所需要的信息,也就是说父类通常是子类的子集,因此必然可以通过子类构建出父类,所以(协变)是安全的。而反之,将父类逆变为子类则可能因为信息不足而导致失败,因此通常情况下逆变不被缺省支持: 但是作为函数的参数,逆变是受欢迎的: 当以上 transfer 被调用时: 注意我们为方法 transfer 的参数类型 T 声明它支持逆变,因此编译器将T 视为 Dog 的父类,标记为-Dog,从父到子的继承关系:-Dog <- Dog,从而允许将 Dog 逆变为 -Dog。 实际上我们并没有给 T 设定具体的边界,因此 -T 实际上指向 Dog 自己(自己是自己的父类),也可以一直逆变为 Object为止,编译器只需要寻找一个能够接纳输入值的类型即可。如果要更明确地限定 T 的类型上限只能到 Animal,可以指定它的上限 -T <: Animal,并且这样可以允许接纳更多的 Animal。 显然这是我们希望看到的。为什么呢?因为当逆变作用于函数的输入参数时,它迫使赋值端的类型升级为输入值的父类,从而允许接受它的所有子类为输入条件,这让函数变得更安全。 但是,如果我们将逆变改为协变: 编译器允许 +T,也就是 +Dog,做为Dog的子类,从而形成从父到子的继承关系:Dog <- +Dog,并尝试将 Dog 赋予自己的子类型 +Dog,但是这与赋值语句的类型转变方向(从右到左)向矛盾,因此编译器拒绝向下转换。 再来看一下函数的输出值类型的转变。与输入恰好相反,因为输出的被赋值类型(左侧)是确定的,输出类型必须确定无误地是它的子类。 这满足我们对赋值的一般印象,也是前面解释的协变发生的场景。因此输出类型参数应该满足协变。 综上所述,在定义一个容器的时候,对于参数的类型通常也应该声明为(Contravariant)逆变类型,也就是允许被赋值端的参数逆变成输入值(赋值端)的父类,这样这个容器的实现条件可以更宽松(父类具有更少的属性),比如逆变成一个 Animal 类型的输入,那么允许我们在实现的时候接受 Dog 或 Cat 或其他任何它的子类型。而输出类型为协变,这样保证输出的信息足够多(子类包含的信息比父类更多),以避免无法被左侧(被赋值侧)接受:
最近应“IT资深专业协会”的邀请做了一场有关设计原则和函数式编程的科普演讲,案例有些复杂,在这里将内容和代码简单重复一下,以便没有听明白的协友可以复习。 需求分析 案例是一个假设的电商系统的用户信息管理接口,需求大致为以下三点: 设计一个电商系统的用户信息管理子模块(ProfileService),满足新用户注册、登陆、和个人信息管理,包括头像、密码、住址等; 用户信息管理模块必须能够支持来自订单系统(OrderService)的调用,提供必要的相关功能子集。 用户信息管理模块必须能够支持来自客服系统(HelpDeskService)的调用,提供必要的相关功能子集。 先大致梳理一下订单系统到用户系统的 use case: 用户可以直接注册/登陆/或取回密码,也可以先下单(order),然后在提交订单的时候再注册/登陆,并且可以添加/修改寄送地址,因此 OrderService 需要能够访问用户的 Profile。基于这样的需求,我们得到以上 use case,并根据 use case 的功能,得到以下 ProfileService 及其接口的类图设计: 接口隔离 我们得到了一个初步的版本,但是这个版本是有缺陷的。很显然它没有考虑接口的应用。 对于订单模块 OrderService 和负责登陆认证的 AuthService 模块,ProfileService 应该提供不同的功能集合,因此我们需要将大而全的接口分割成多个不同的小接口及其组合,以便为不同的业务模块提供不同的外观,因此我们需要重新考虑接口的设计以满足对接口隔离的要求: 我们从 IProfileService 中分离出两个小的接口:ICustomerService 和 ISecurityService,让它们分别作为 OrderService 和 AuthService 的依赖项,同时 ICustomerService 和 ISecurityService 又共享了注册/登陆功能,这些更基础的功能项被放在更基础的 IRegisterService 中。自此,我们为项目成功提供了隔离接口,让每个模块彼此保持了低耦合性。目前为止来说,传统面向对象架构都还能够很好地胜任。 项目演进 现在让我们来添加第三个 HelpDeskService 模块,先来看一下这个模块的 use case: Help desk 模块相比 AuthService 而言,出于安全考虑,我们不打算让它能替用户开户,更不能替用户登陆。但是它可以在用户忘记密码的时候协助用户发送密码重设邮件,因此它可以访问 ResetPassword 功能,而我们假设这个能力是 OrderService 没有的。同时它又比 AuthService 有更丰富的用户信息管理能力,比如添加或修改用户的寄送地址等,这部分功能则与 OrderService 共享。因此它看上去更像是 AuthService 和 OrdereService 的部分子功能的集合。也就是说我们需要从 ICustomerService 和 ISecurityService 中屏蔽掉(来自最基本的 IRegisterService 的)部分功能,然后再分离出一些更小的元素重新组合出新的集合,假设这个集合叫 IHelpService,于是我们再次重构接口得到以下类图: 重新设计的接口组合,我们首先从最基本的层面拆解了 IRegisterService,得到了四个更小粒度的接口,分别是 IPasswordReset、IProfileReadOnly、IRegisterService 和 IAddressService 四个基本接口,然后在这四个接口的基础上,我们从新组合得到 IHelpService、ICustomerService 和 ISecurityService 这三个分别对应 Help desk、订单和认证的功能接口,以及基于以上接口的最上一层 IProfileService 模块的总接口。 以上设计基本上达到了接口设计的目的,它保持了接口隔离。ICustomerService 和 ISecurityService 从客户端的角度看完全没有发生变化,但是我们成功新增了 IHelpService 接口,其中包含了分离出来的部分函数,隔离了不需要的函数。 虽然我们达到了设计目标,但是感觉似乎有一些不对劲。仔细品味一下,我们在添加 Help desk 接口的时候,明显比前两个接口要费劲的多,我们几乎重构了之前的所有接口,而结果也让类图中的元素看上去增加了许多,这会不会是个问题呢? 问题及思考 经过重新的梳理,我们发现以上这些接口虽然能够让我们完整地遵守接口隔离原则,只暴露必要的功能给相应的客户端模块。但是很显然相比在没有加入 Help desk 之前的设计,接口逻辑的复杂度已经提升了很多。这是因为当三个或三个以上的模块彼此产生联系的时候,它们之间的关系不是以线性的方式,而是以曲线方式递增的。我们每增加一个新的模块,它都有可能和已经存在的所有模块发生交集,并且在最坏的情况下可能导致重构所有有关联的接口模块的继承关系。由此可见,接口隔离原则虽然很重要,但是它给工程带来的设计成本的增加,在随着项目规模的升级,会越来越可观,很快就会达到难以承受的高度。成本是我们在实际的工程中必须要解决的首要问题,否则它将严重影响质量。 另一个方面,我们之前的分析一直聚焦在接口上,暂时地忽略了实现层面的问题。从第一个版本开始,我们只是将所有的功能实现都集中在 ProfileService 这一个大类(class)中,它实现了 IProfileService 接口,是所有功能最终的提供者。虽然从接口层面我们不必关心这一点,但是在实际项目中,我们几乎一定会考虑代码解藕,而不仅仅是接口的解藕。因此,我们有必要思考一下 ProfileService 的实现应该怎样适应接口的变化。 如何实现功能 class 这个问题其实和第一个问题有一定的关联。我们设想一下,带来第一个问题的根本是因为接口隔离原则着眼的是功能的组合,而不是功能本身,因此如果我们在最极端的情况下,将每单个功能都以一个单一接口来表达,然后基于一个一个零散的实现来组合隔离接口。这好比,与其靠从其它车辆上拆卸零件来组装新的汽车,为何不一开始就提供最原子化的零件来组装汽车呢?也就是说,如果我们抛弃“继承”,直接以单个函数为对象来实现我们的类(特质),然后再通过“混入”(mix in),来实现不同的接口组合,那么就能够实现模块的数量和复杂度的线性关系。因此从对功能的实现上引入“蛋糕模型”,而不是接口继承模型来设计我们的功能组合将明显有利于成本控制。 那么蛋糕模型对我们的传统设计是否构成挑战呢?首先,当我们为每个方法都设立一个对应的接口时,编码量会明显地增加,只是相比不可控地增加有所减小,至少它是线性的,你可以基于此做出预算了。其次,让我们以这样一个例子来看一下具体的实现:假设我们将实现 addAddress(Int profileId, Address address) 这个函数,它的代码大致如下: 在添加新的地址之前,我们需要先根据 profile id 来获得相应的 profile 引用。传统的继承模型中,我们可以通过父类提供的 getProfile 方法来获得 profile 的引用,但是现在我们使用的是蛋糕模型,很显然当前上下文中没有 getProfile 方法可供调用。它来自蛋糕的另一块切片,两个切片之间没有继承关系!如果我们使用 new 在这里加入一个实体变量来指向 getProfile,那么我们可以想象在整个工程中,类似的操作将累积起惊人的内存消耗和无端的运行开销,并且它也不是代码管理的最佳实践,因此这条路显然也走不通。 我们似乎陷入了另一个迷宫:虽然借助蛋糕模型暂时控制了成本不成比例上涨的问题,但是照成了另一个更难解决的技术难题。如何让不同的,没有继承关系的类彼此发生调用关系呢?这成为了我们要解决的第二个难题。当然,这也是传统架构遗留下来的问题,因为传统架构没有为我们提供突破此障碍的解决方案。自此,我们不得不重新审视我所选择的开发工具,采用更全面的函数式编程来解决以上工程和技术难题。 函数式解决方案 我将采用 Scala 来重构以上设计。首先要声明,我们将在不牺牲接口隔离原则的前提下,以可控的成本来解决技术上的挑战。以 HelpService 接口为例: 打完收工…… 是的,结束了,以上就是实现 HelpService 接口的全部(如果神奇的 “???” 也算的话)代码。让我来解释一下: trait(特质)是一个融合了 Interface 和 Abstract class 的,函数式风格的“接口”,通过 trait,我们避免了为无数的函数提供同样多数量的 Interface,这极大地节约了代码量。并且它的编写成本和单个函数相差无几,这是我们保证工程成本的关键,基于此我们可以得到更可承受的成本预算。同时,它提供了比函数,甚至比接口和抽象基类更强大的能力,它是函数式语言实现蛋糕模型的核心。在 Scala 中,我们甚至可以在 trait 中通过“自类型”来让没有关系的两个 trait […]
TSet[A] 是一个可以参与 STM 中事务可变集合。 创建一个 TSet 创建一个空的 TSet: 或创建具有指定值的 TSet: 或者,您可以通过具有值的集合来创建 TSet: 如果提供了重复项,则取最后一个。 将一个值加入 TSet 可以通过以下方式将新元素添加到集合中: 如果集合已经包含元素,则不会进行任何修改。 从 TSet 中删除元素 从 TSet 中删除元素的最简单方法是使用 delete 函数: 同样,可以删除满足所提供函数参数的每个元素: 或者,您可以保留所有与函数参数相匹配的元素: 请注意,retainIf 和 removeIf 与 filter 和 filterNot 具有相同的目的。分别命名它们的原因是要强调其本质上的区别。也就是说,retainIf 和 removeIf 都是破坏性的,调用它们会修改集合本身。 TSet 的并集 Set A 和 Set B 的并集表示属于 Set A 或 Set B 或两者都属于的元素集。使用 A union B 函数来修改 A 集合(取得两者的并集)。 TSet 的交集 Set A 和 Set B 的交集指的是同时属于 A 和 B 的元素集。使用 A intersect B 函数修改 A 集合(来取得交集)。 TSet 的差集 Set A 和 Set B 的差集是包含于集合 A但,集合 B 中没有元素。使用 A diff B 函数来获得。The difference between sets A and B is the set containing elements of set A but not in B. Using A diff B method modifies set A. 转换 TSet 的元素 transform(A => A) 函数允许基于集合中的每个元素计算一个新值: 以下可以压缩 TSet: 上面示例得到的结果集只有一个元素。 请注意,transform 的作用与 map 相同。对其进行不同命名的原因是要强调其本质上的区别。也就是说,transform 是破坏性的,调用它会修改集合本身。 可以通过 transformM 效果化地映射元素: fold 可以使用指定的二元运算折叠 TSet 的元素: 元素可以通过 foldM 有效折叠:The elements can be folded effectfully via foldM: 对 TSet 中的元素执行side-effect foreach 用于对集合中的每个元素执行副作用: 检查 TSet 的成员 检查元素是否存在于 TSet 中: 将 TSet 转换为 List 可以按以下方式获取集合元素的列表: TSet 的大小 集的大小可以通过以下方式获得:
TRef[A] 是可以参与 STM 事务的对不可变值的可变引用。其可变引用可以在事务内检索和设置,并被强制保证原子性,一致性并与其他事务的隔离。 TRef 在 STM 内存中发生改变时,使用了低级机制来创建事务。 创建一个 TRef 在事务内部创建TRef: 或者在事务内创建一个TRef,然后立即提交该事务,这使您可以存储并传递一个值引用。 从 TRef 中取出值 从一次事务中取回结果值: 或多次事务提交后取回值: 给 TRef 设置一个值 设置该值将覆盖现的引用内容。 在单个交易中设置值: 或(在)多次交易(中多次设置): 更新 TRef 中的值 update(A => A) 函数允许根据 TRef 中旧的值计算新的值。 在单次事务中更新值: 或(在)多次事务(中多次更新): 修改 TRef 中的值 modify(A => (B, A)): B 函数和 update 相似,但是它允许返回一些(B 类型的)信息。 在单个事务中修改值: 或(在)多次事务(中多次修改): 使用例子 这是使用 TRef 在两个纤程之间传递值的例子: 在此示例中,我们为发送者和接收者创建并提交两个事务引用,以便能够提取它们的值。在接下来的步骤中,我们创建一个原子事务,仅在发送者帐户中有足够的可用余额时才更新两个帐户。然后,我们(fork)以异步运行它。在接下去的纤程中,我们暂停它的执行直到发件人余额发生变化(在这种情况下达到零为止)。 最后,我们提取两个帐户的新值并将其合并为一个结果。 ZTRef 和 Ref[A] 类似,TRef[A] 实际上是 ZTRef[+EA, +EB, -A, +B] 类型的别名, ZTRef[+EA, +EB, -A, +B] 是一个多态的事务性引用,支持 ZRef 所提供的所有运算。有关多态引用的更多讨论,请参见 ZRef.
TQueue[A] 是一个可以参与 STM 事务的可变队列。 创建一个 TQueue 创建一个具有指定容量的空的有界 TQueue: 创建一个空的无容量限制的 TQueue: 将元素加入一个 TQueue 将元素放入一个 TQueue: 如果队列未满,则指定的元素将被成功添加到队列中。否则,它将等待队列中的空插槽位。 另外,您可以使用一个元素列表来填充队列: 从 TQueue 中取回元素 您可以从队列中取回第一个元素,如下例: 如果队列为空,它将阻塞等待您所期待的元素。 可以通过使用 poll 方法来避免此阻塞行为,该方法将返回一个元素(如果存在)否则返回 None: 取回队列的前n个元素: 可以按以下方式获取队列的所有元素: TQueue 的大小 可以按以下方式获取队列中元素的个数:
TPromise 是一个可以设置一次,并且可以参与 STM 事务的可变参考。 创建一个 TPromise 创建一个 TPromise: 结束一个 TPromise 成功完成 TPromise: 得到一个失败的 TPromise: 另外,您也可以使用 done 组合器,并通过传递 Either[E, A] 来完成 Promise: 设置它的值后,之后任何尝试对其进行设置的操作都会返回 false。 从一个 TPromise 中得到值 如果 Promise 已经完成,则返回结果,否则返回 None: 或者,您可以(阻塞)等待 Promise 的完成并将值返回:
TPriorityQueue[A] 是一个可以参与 STM 事务的可变队列。一个 TPriorityQueue 中包含了类型为 A 的带有顺序定义的值。与 TQueue 不同,take 返回的是最高优先级(指定顺序中的第一个)值,而不是队列中的第一个值。当从队列中取出时,不保证共享相同优先级的元素的顺序。 创建一个 TPriorityQueue 您可以使用 empty 函数创建一个空的 TPriorityQueue: 请注意,TPriorityQueue 的创建使用了隐式 Ordering。默认情况下,take 将返回指定顺序中第一个的值。例如,在按时间排序的事件队列中,最早的事件将被首先取出。如果您想要不同的行为,可以使用自定义的 Ordering。 您还可以使用 fromIterable 或 make 构造函数创建一个使用指定元素初始化的TPriorityQueue。fromIterable 构造函数采用 Iterable,而 make 构造函数采用可变参数元素序列。 向 TPriorityQueue 添加元素 您可以使用 offer 或 offerAll 方法将元素添加入 TPriorityQueue。如果您要同时向队列添加多个元素,则 offerAll 方法会更加高效。 从 TPriorityQueue 中获取元素 使用 take 从 TPriorityQueue 中获取一个元素。take 在语义上会阻塞,直到队列中至少要取一个值为止。您还可以使用 takeAll 立即获取队列中当前的所有值,或使用 takeUpTo 立即获取队列中指定数量的元素。 您也可以使用 takeOption 方法从队列中获取第一个值(如果不存在也不会被挂起),或者使用 peek 方法观察队列中的第一个元素(如果存在)而不将其从队列中删除。 有时,您想要对队列的当前状态进行快照而不修改它。为此,toChunk 组合器或其变体 toList 或 toVector 非常有用。这些函数将返回一个不可变的集合,该集合由当前队列中的所有元素组成,而队列的状态保持不变。 TPriorityQueue 的大小 您可以使用 size 方法检查 TPriorityQueue 的大小:
TMap[A] 是一种可以参与 STM 事务的可变映射。 创建一个 TMap 创建一个空的 TMap: 或创建具有指定值的 TMap: 或者,您可以通过提供的元组集合来创建 TMap: 将键值对存入一个 TMap 可以通过以下方式将新的键值对添加到 map 映射: 在 map 中添加条目的另一种方法是使用 merge: 如果该键不存在于 map 中,则其行为类似于简单的 put 方法。否则,使用提供的函数将现有值与新值合并。 从 TMap 中删除元素 从 TMap 中删除键值对的最简单方法是使用采用 delete 删除某个键: 同样,它可以删除满足函数参数的每个键值对: 或者,您可以保留所有与函数参数匹配的键值对: 请注意,retainIf 和 removeIf 与 filter 和 filterNot 具有相同的目的。但是分别命名它们的原因是要强调其本质上存在的区别。也就是,retainIf 和removeIf 都是破坏性的,调用它们会修改原集合。(而 filter 和 filterNot 只是返回新集合而不修改原集合) 从 TMap 中读取 可以通过以下方式获取与键关联的值: 或者,如果 map 映射中不存在该键,则可以提供默认值: 转换 TMap 中的条目 函数 transform((K, V) => (K, V)) 可以用于为 map 映射中的每一个条目计算新的值: 请注意,它也可以用来压缩 TMap: TransformM 可以用于效果化地映射条目: 函数 transformValues(V => V) 可以为 map 中的每个值计算一个新值: 可以通过 transformValuesM 效果化地处理 map 中的这些值: 请注意,transform 和 transformValues 的用途与 map 和 mapValues 相同。 之所以分别命名它们的原因是要强调其本质上的区别。也就是说,transform 和 transformValues 都是破坏性的,调用它们可以修改原集合。 fold 使用指定的两个关联运算来遍历折叠 TMap 中的元素: foldM 可以效果化地折叠原属: 对 TMap 键值对执行side effect计算 foreach 用于对映射中的每个键值对执行 side-effect 计算: 检查 TMap 中的成员 检查键值对是否存在于 TMap 中: 将 TMap 转换为 List 可以通过以下方式转换成元组列表: 可以按如下方式获得键列表: 可以按以下方式获得值列表:
TArray 是可以参与 STM 事务的可变引用的数组。 创建一个 TArray 创建一个空的 TArray: 或创建具有指定值的 TArray: 或者,您可以通过指定的集合来创建 TArray: 从 TArray 读取值 可以通过以下方式获得数组的第 n 个元素: 访问不存在的索引会引发 ArrayIndexOutOfBoundsException 异常并中止事务。 更新 TArray 中的值 可以按照以下步骤更新数组的第n个元素: 可以通过 updateM 效果化地更新数组的第n个元素: 更新不存在的索引会引发 ArrayIndexOutOfBoundsException 并中止事务。 转换 TArray 的元素 transform(A => A) 函数可以为数组中的每一个元素计算新值: 可以通过 transformM 效果化地映射元素: fold 通过两个指定的运算遍历折叠 TArray 中的元素: 可以通过 foldM 效果化地进行遍历折叠: 对 TArray 元素执行 side-effect 运算 foreach 用于对数组中的每个元素执行 side-effect 运算: