类型类 V.S. 桥接模式:撞衫不可怕,谁丑谁尴尬

缘起

设计模式是建立在编程语言层面之上的,从某种角度上看,模式是以应用场景为导向,在编程语言的基础设施之上构建的最佳设计的范本,其价值在于可以作为模版应用于同类场景中。而反过来,语言基础设施的改变必然会影响到上层的设计模式,设计模式和编程语言相互影响,新的编程范式会催生新的设计模式,而成熟的设计模式会引导在语言在基础层面上进行进化从而直接支持!这类的案例是非常多的,比如:Java语言原生支持原型模式、观察者模式,Scala里的Object是原生的单态,等等。本文将要探讨的是在Scala语言中存在的一种语言特性:类型类。同时,本文还将会讨论一个与类型类在用意上非常接近的设计模式:桥接模式,讨论两者的异同,但是请不要被我开篇的立论所误导,两者是否互为等价物,需要我们抽丝剥茧慢慢分析。本文原文出处: http://blog.csdn.net/bluishglc/article/details/50894843 严禁任何形式的转载,否则将委托CSDN官方维护权益!

认识“类型类”

类型类是Scala中的一项语言特新,为了能更好的说明它的设计初衷,在介绍它之前我们先来看一个传统的面向对象的设计,你一定非常熟悉:

一个糟糕的例子

package com.github.scala.typeclasses

import scala.math.{Pi, sqrt}

object PolymorphicDemo {
    trait Shape {
        def area:Double
    }

    case class Circle(radius:Double) extends Shape {
        def area:Double = 2*Pi*radius
    }

    case class EQLTriangle(side:Double) extends Shape {
        def area:Double = (sqrt(3)/4)*side*side
    }

    val shapes = Vector(Circle(2.2), EQLTriangle(3.9), Circle(4.5))

    def a(s:Shape) = f"$s area: ${s.area}%.2f"

    val result = for(s <- shapes) yield a(s)

    def main(args: Array[String]) {
        println(result)
    }

}

上述代码设计了一个Trait: Shape,所有的Shape都有一个计算面积的方法,所以这个方法被定义在了Trait中。在它的两个派生子类:Circle和EQLTriangle中都实现了计算面积的方法:area. 这个例子来自《Atomic Scala》一书,书中解释说:想把计算面积的方法抽离出来独立的演变,于是引入的“类型类”的实现版本:

package com.github.scala.typeclasses

import scala.math.{Pi, sqrt}

object TypeClassDemo {

    trait Calc[S]{
        def area(shape:S):Double
    }

    case class Circle(radius:Double)

    implicit object CircleCalc extends Calc[Circle] {
        def area(shape:Circle) = 2*shape.radius*Pi;
    }

    case class EQLTriangle(side:Double)

    implicit object EQLTriangleCalc extends Calc[EQLTriangle] {
        def area(shape:EQLTriangle) = (sqrt(3)/4)*shape.side*shape.side
    }

    def a[S](shape:S)(implicit calc: Calc[S])= f"$shape area: ${calc.area(shape)}%2.2f"

    def main(args: Array[String]) {
        //you can only use a(shape) to get area not shape.area
        //this is not so "OO"
        println(a(Circle(2.2)))
        println(a(EQLTriangle(3.9)))
        println(a(Circle(4.5)))
    }
}

如果仅从学习语法的角度看,这个例子是OK的,但是这个例子会严重误导读者对于类型类用意的理解。首先,你会发现,在这个示例中没有了Circle和EQLTriangle的共同基类:Shape,实际上它们也不能有共同基类Shape,因为Shape的area是虚函数,一旦继承,Circle和EQLTriangle就必须要实现,而这个例子要演示的是让类型类去实现。这个例子没能很好地演示“在两个维度上独立演化”,反而把原类族的继承关系给摸去了。实际上,作为一个反面教材,这个案例很好的证明了一个事实:类型类不是用来重写一个或多个类的已有方法的,而只能是给它们添加新的面向这个类型自身的某种具有共性的操作。

一个“稍好”的例子

让我们接着看一个稍好的应用类型类的例子:

package com.github.scala.typeclasses

import scala.math.{Pi, sqrt}

object TypeClassDemo2 {

    trait Car

    trait Transmissive[C]{
        def shift(Car:C)
    }

    case class Bus(seats:Int) extends Car

    implicit object ATBus extends Transmissive[Bus] {
        def shift(car:Bus) = println(s"This is a Bus, seats: ${car.seats} , now shift automatically!");
    }

//    //Error: ambiguous implicit values:
//    implicit object MTBus extends Transmissive[Bus] {
//        def shift(car:Bus) = println(s"This is a Bus, seats: ${car.seats} , now shift manually!");
//    }

    case class Truck(load:Double) extends Car

    implicit object ATTruck extends Transmissive[Truck] {
        def shift(car:Truck) = println(s"This is a Truck, load: ${car.load} T, now shift automatically!");
    }

//    //Error: ambiguous implicit values:
//    implicit object MTTruck extends Transmissive[Truck] {
//        def shift(car:Truck) = println(s"This is a Truck, load: ${car.load} T, now shift manually!");
//    }

    def shift_func[C](car:C)(implicit t: Transmissive[C]) = t.shift(car)

    def main(args: Array[String]) {
        shift_func(Bus(10))
        shift_func(Truck(1))
    }
}

在这个例子中, 我们试图来展示:类型类可以将一个已存在的类族向另一个维度上演变的能力。Car和Bus/Truck是主类族,类型类Transmissive试图从变速方式上对Car类族进行独立的演化,而附加上去的行为就是:shift(换挡)。之所以说这是一个“稍好”的例子,一方面它能够印证类型类将一个已存在的类族向另一个维度上演变的能力,但同时我们也看到另一个尴尬的地方,那就是代码中注释掉的部分,因为如果不注释掉,会出现编译错误,因为针对同一个类型引入了两个隐式对象,编译器在隐式解析时不知道要使用那一个。注释掉的代码试图将Car类族向另一个维度上进去进行演变,但是它失败了,在后续的桥接模式的探讨中,我们会继续探讨这个问题。

桥接模式?

类型类很容易让人联想到桥接模式,因为两者都有能让一组类型在另一种维度上演化的能力。但是两者的差异其实是很大的。在对比之前我们还是有必要来简单回顾一下桥接模式。

上图是《设计模式》一书中给出的桥接模式的案例,该书对于桥接模式的解释是将抽象部分与它的实现部分分离,使它们都可以独立地演化,在现实世界里,很多事物根据不同的属性和行为特征都有可以按照不同的维度进行分类,一般分两种典型的场景:

  • 一种是多个子类之间的概念是平行的,比如咖啡有大杯、小杯之分,又有摩卡、卡布奇诺之分。
  • 一种是多个子类之间有内容和概念上的重叠, 那么需要我们抽象共同的部分,把变化的部分抽象成类族独立的去演化

以上两种场景中的任何一种都可以使用桥接模式轻松地解决。那么问题来了,“类型类”能解决上述问题吗?在回答问题之前,让我们先定义两个概念,我们知道既然类型类和桥接都有让两个类族独立演化的能力,为了准确的描述,我们把两个独立演化的维度分别称为主维度和副维度(在Bridge的示意图中,Window是主维度,WindowImp是副维度,在我们的类型类示例代码中,Car是主维度,Transmissive是副维度)

现在来回答前面的问题:

  • 针对第一种场景

    • 如果副维度是一个单一概念(不存在两层以上的继承,也不存在并行的第二种实现),则可以使用类型类,而且使用类型类在语法实现更多加简洁优雅,比如我们前面例子中,仅仅只针对汽车类族在“自动档”维度上做的延展工作一样(当然如前所说这不是一个最好的例子)。
    • 但一旦副维度上出现了两种以上的实现,就会出现二意性的隐式解析错误,所以就不能使用类型类了。
  • 针对第二种场景,类型类更无能为力了。

对决

以下我们把这两者放到一起,做一个细致的比较:

  • “用意”不同

类型类可以针对一个类族进行嫁接,但并不限于一个类族,它可以针对任何类嫁接,这一点很重要,类型类的立意是围绕一个主题或者说特性,为所有涉及到这些主题或特性的类“添加”或者说“绑定”面向这个主题或特性的“实现”!而桥接模式总是针对 一个类族在另一个维度上进行演变。

  • “粒度”不同

类型类并不是在目标类型的目标方法上添加或修改逻辑,它也没有这样的能力,它的目标是针对目标类型“附加”新的操作!这是类型类区别于桥接模式的一个显著特点。

  • “维度”不同

桥接模式必定有两个独立演化的类族,如果两个类族各有两个实现类的话,那么理论上会产生2X2=4种对象组合,而对于类型类来说,它自身(副维度)是没有自主的演化体系的,就像我们的关于Car的实例代码一样,你只能针对一个具体的汽车类施加一种换挡操作,在“变速模式”这个维度上,它只能是单一的,如果有两个以上的变种,你就不能再使用类型类了。

  • “嫁接”方式不同。

对桥接模式而言,主维度中的具体类一定会依赖到(作为一个字段被引入)副维度上的具体类,这是桥接模式将两种维度嫁接在一起的方式。而对于类型类来说,是通过将副维度的基类泛型化,在副维度的具体类继承时指定了要嫁接的主维度的对应类型,两个维度的嫁接需要通过一个使用了隐式参数的“嫁接函数”(如本例中的a)来实现的。虽然从语法上看,像println(a(Circle(2.2)))这样在求图像面积时没有出现第二维度的具体类,直观上完成了嫁接,但是不完美的地方在于:你不能使用println(Circle(2.2).area)这样更自然更OO的方式去调用,这是非常让人遗憾的。但尽管如此,类型类的定义和声明方式还是非常优雅的。

小结

跳出主/副维度的圈子,类型类设计的初衷不是和桥接模式进行PK,类型类的立意更加倾向于创建一个独立于任何其他类族的功能系(只针对某项功能或特征进行建模),然后可能有多种类型都需要这个功能或特征,此时是类型类的典型应用场景。实际上,类型类并没有真得把一组类型在另一个维度上演化,它所做的就是针对某个类型,附加了一些行为,然后利用隐式参数,使得调用时看上去像是被嫁接到目标类型上一样。


时间: 2024-10-12 21:29:45

类型类 V.S. 桥接模式:撞衫不可怕,谁丑谁尴尬的相关文章

Java设计模式应用——桥接模式

性能管理系统中,数据产生后需要经过采集,汇聚,入库三个流程,用户才能查询使用. 采集可以是snmp采集,也可以是ems采集:汇聚可以使storm汇聚,也可以是spark汇聚:入库可以是hdfs入库,也可以是mppdb入库. 针对不同场景,我们可以灵活选择不同的采集,汇聚,入库方式.这种一个功能需要多种服务支持,每种服务又有不同类型的实现,使用桥接模式再适合不过. (注:这里仅仅是桥接模式的例子,实际应用中,采集.汇聚.入库时异步执行的,他们之间通过消息通信) 桥接模式,顾名思义,就是把每种服务看

C#设计模式--桥接模式

0.C#设计模式--简单工厂模式 1.C#设计模式--工厂方法模式 2.C#设计模式--抽象工厂模式 3.C#设计模式--单例模式 4.C#设计模式--建造者模式 5.C#设计模式--原型模式 6.C#设计模式--设配器模式 7.C#设计模式--装饰器模式 8.C#设计模式--代理模式 9.C#设计模式--外观模式 设计模式: 桥接模式(Bridge Pattern) 简单介绍: 桥接模式(Bridge Pattern):桥接模式的用意是将抽象化(Abstraction)与实现化(Impleme

js --桥接模式

定义: 将抽象部分与它的实现部分分离,使他们都可以独立的变化. 也就是说,桥接模式里面有两个角色: - 扩充抽象类 - 具体实现类 在写桥接模式之前,想在写一下关于抽象的理解.我觉得抽象这个概念过于抽象,而不易于理解. 抽象的概念: 从具体事物抽出.概括出它们共同的方面.本质属性与关系等,而将个别的.非本质的方面.属性与关系舍弃,这种思维过程,称为抽象. ---[百度百科] 在自然语言中,很多人把凡是不能被人们的感官所直接把握的东西,也就是通常所说的“看不见,摸不着”的东西,叫做抽象. 比如:物

Java设计模式菜鸟系列(十七)桥接模式建模与实现

转载请注明出处:http://blog.csdn.net/lhy_ycu/article/details/40008711 桥接模式(Bridge): 把事物和其具体实现分开(抽象化与实现化解耦),使他们可以各自独立的变化.假设你的电脑是双系统(WinXP.Win7),而且都安装了mysql.oracle.sqlserver.DB2这4种数据库,那么你有2*4种选择去连接数据库.按平常的写法,咱要写2*4个类,但是使用了桥接模式,你只需写2+4个类,可以看出桥接模式其实就是一种将N*M转化成N+

大话设计模式_桥接模式(Java代码)

合成/聚合复用原则:尽量使用合成/聚合,尽量不要使用类继承. 桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立变化. 解释:即一个系统可以有多种分类实现,把没种分类独立出来,让他们可以独自变化,减少他们之间的耦合. 简单描述:1个Abstraction类,持有一个Implementor的引用,其方法中调用此Implementor引用的对应方法 大话设计模式中的截图: 代码例子: Abstraction类: 1 package com.longsheng.bridge; 2 3 publi

合成/聚合复用原则,桥接模式

问题: 方式一, 方式二, 存在问题: 继承带来的麻烦,无论是哪种方式,一旦功能增多.品牌增多,增长不可控的无限变大.增加一个品牌,增加m个软件类+1个品牌类:增加一个软件,增加n(品牌个数)软件个类. 对象的继承关系在编译时就定义好了,所以无法在运行时改变从父类继承的实现. 子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化. 当需要复用子类时.如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换.这种依赖关系限制了灵活性,并最终限

C#设计模式(10)——桥接模式

1.桥接模式介绍 桥接模式用于将抽象化和实现化解耦,使得两者可以独立变化.在面向对象中用通俗的话说明:一个类可以通过多角度来分类,每一种分类都可能变化,那么就把多角度分离出来让各个角度都能独立变化,降低各个角度间的耦合.这样说可能不太好理解,举一个画几何图形的例子:我们画的几何图形可以按照形状和颜色两个角度的进行分类,按形状分类,分为圆形.长方形.三角形,按照颜色分类分为蓝色图形.黄色图形和红色图形,而形状和颜色都是可以添加的,比如我们也可以添加五角星形状,颜色可以添加一个绿色.如果按继承来实现

Javascript设计模式理论与实战:桥接模式

桥接模式将抽象部分与实现部分分离开来,使两者都可以独立的变化,并且可以一起和谐地工作.抽象部分和实现部分都可以独立的变化而不会互相影响,降低了代码的耦合性,提高了代码的扩展性. 基本理论 桥接模式定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化.桥接模式主要有4个角色组成:(1)抽象类(2)扩充抽象类(3)实现类接口(4)具体实现类根据javascript语言的特点,我们将其简化成2个角色:(1)扩充抽象类(2)具体实现类怎么去理解桥接模式呢?我们接下来举例说明 桥接模式的实现 理解桥

读书笔记_java设计模式深入研究 第六章 桥接模式

1,桥接模式:将抽象部分与实现部分分离,使他们可以独立变化.桥接模式可以实现的是不同方式的组合完成不同的功能,方式和功能完全分离,使得相互不影响. 2,UML模型: 3,简单代码实例: /** * * @(#) IPost.java * @Package pattern.chp06.bridge.simple * * Copyright ? JING Corporation. All rights reserved. * */ package pattern.chp06.bridge.simpl