聊聊设计模式中的那些坑 —— 桥接模式

先简单地介绍一下“桥接模式”:

用一个(抽象化的)接口将实体中的属性独立出来,以保持这个属性的变化独立于宿主,因为这个接口在两个定义(宿主和属性)之间起到桥接的作用,故名“桥接模式”,比如下文中的 Name 接口就是起到桥接 Party 类和 Name 属性之间的“桥”。

桥接模式给对象的灵活性带来了一些变化,它使得对象的某些部分的实现不再完全依赖父类,相反它可以从非继承关系中获的某些好处。就以文中的例子,我们先来看看当它以正面形象运行的时候是什么样的:

package pro.wangji;

interface Name{
    String toString();
}

class OrganizationName implements Name{
    String name;
    public OrganizationName(String name){
        this.name = name;
    }
    @Override
    public String toString(){
        return name;
    }
}

class IndividualName implements Name{
    String firstName;
    String lastName;
    public IndividualName(String firstName, String lastName){
        this.firstName = firstName; this.lastName = lastName;
    }

    @Override
    public String toString(){
        return lastName + ", " + firstName;
    }
}

class Party {
    Name name;
}

class Organization extends Party {}

class Individual extends Party{}

public class BridgePattern {
    public static void main(String[] args) {
        Organization organization = new Organization();
        organization.name = new OrganizationName("ABC Company");

        Individual individual = new Individual();
        individual.name = new IndividualName("Gates", "Bill");

        System.out.println(String.format("Organization name is \"%s\"",  organization.name));
        System.out.println(String.format("Individual name is \"%s\"",  individual.name));    }
}

很显然我们会得到预期的输出结果:

Organization name is "ABC Company"
Individual name is "Bill, Gates"

但是需要注意的是“桥接模式”同样是恶魔的制造者,我们只需要对主代码做非常小的一点改动你就会发现问题:

public class BridgePattern {
    public static void main(String[] args) {
        Organization organization = new Organization();
        organization.name = new OrganizationName("ABC Company");

        // Exchange organization and individual name wouldn't causes error.
        organization.name = new IndividualName("Gates”, "Bill");
        individual.name = new OrganizationName("ABC Company");

        System.out.println(String.format("Organization name is \"%s\"",  organization.name));
        System.out.println(String.format("Individual name is \"%s\"",  individual.name));
    }
}

我们“不小心”交换了传入参数的类型,原本应该传入 OrganizationName的地方实际上传入了IndividuaName,相反也是。而编译器居然完全没有发现这个错误,结果我们的到了完全相反的输出结果:

Organization name is "Gates, Bill"
Individual name is "ABC Company"

为什么会这样?编译器为什么无法发现这个问题呢?难道是编译器开发者的疏忽吗?并不是的,其实这就是编译器做出的正确选择,虽然它不符合我们的预期。要理解这个“错误”,我们还是要从OO的最基础原理中去寻找答案。

OO 允许我们适应不同类型的最根本依据是“多态”,多态是 OO 三大特征中的一个,多态的主要体现是“重载”和“重写”,它们解决的核心问题是对象如何正确地寻找到目标。以 Java 语言规范为例,当一个对象被初始化的时候,它会对对象做“重载解析”(overload resolution)或称 “静态分派”,所谓 “静态” 指的是一个对象在声明的时候的类型,也被称为“静态类型”,在这个例子中也就是 “Name”类型,而等号后面的实例类型,OrganizationName 或 IndividualName,被称为“运行时类型”(runtime type)或“实际类型”(Actual type),顾名思义,实际类型在运行时决定,两者最大的不同是运行时类型可能会在运行时发生变化,但是这种变化不会影响静态类型,也就是说,你可以先给 organization.name 赋予 OrganizationName 类型值,然后再改变成 IndividualName 类型也不会导致程序运行的任何不舒服(而你舒服不舒服就不好说了)。

因为静态类型的判定属于编译期的事,而实际类型是属于运行期的事,编译器对OO 的多态依据是由静态类型决定的,也就是说是静态类型,而不是实际类型决定了重载解析时的变量类型!并且,在 Java 语言中,因为属性是不支持重写的,也就是说如果这个问题发生在属性 Name 上,我们希望通过重写来解决问题也是不可能的,也就是说指望经典多态理论来解决这个问题基本上不可行。

以下重写无法通过编译:

class Organization extends Party {
    @Override
    OrganizationName name;
}

综上所述,桥接模式其实是一个很危险的模式,这种危险性在于它的类型不安全性,那么有没有办法解决这个问题呢?答案当然是有的,就是“泛型”,泛型允许我们在编译期指定运行期类型的范围:

object GenericTest {
    sealed trait Namne
    final case class OrganizationName(name: String) extends Name
    final case class IndividualName(firstName:String, lastName: String) extends Name

    sealed trait Party[S <: Name] {
        def name: S
    }
    trait Organization extends Party[OrganizationName]
    trait Individual extends Party[IndividualName]
}

如此一来就避免了类型的不确定性,可以放心地使用 Organization 和 Individual了。

泛型并不排斥多态,甚至泛型弥补了经典多态的缺憾,产生了泛型多态的概念,丰富了多态的领域,它让语言变得更加安全,但是这种加强也导致了语言发展过程中的一次强烈的地震,这场地震的结果是将编程语言的世界一分为二,各自走上不同的道路:在一方面尽可能利用泛型来加强静态类型的约束力,让语言逐渐趋向静态类型,静态类型语言一个最明显的标志是逐渐淘汰了 cast(特指 downgrade cast)指令的使用,而对cast 指令的支持最初被认为是OO多态很重要的一个特征(自行Google cast 的四种模式中的 dynamic cast相关内容),Scala语言算的上是静态类型语言的巅峰之作;而另一方面有些语言则希望极大化地强调运行时侧的类型变化特征,这产生了语言的另一个分支:“动态类型语言”的出现,典型的动态类型语言包括 Python、JavaScript 等。当然也有语言尝试兼取两者优点,比如typescript 等。有关动态类型语言,以及它和泛函编程的渊源的内容有很多,基于时间和篇幅不在这里啰嗦,以后再单独介绍。但是可以提一下今天的 Java 也实现了对动态类型语言的支持,只不过这个支持出现的很晚,一直到 JVM7 才从虚拟机层面支持,而语言层面的支持则一直到 Java 8 发布以后。这个过程罕见地经过两个主版本的迭代,前后历时8年,影响深远。

Leave a Reply
Your email address will not be published.
*
*

BACK TO TOP