设计模式概论:从历史到现代的演进
第1章:设计模式概论:从历史到现代的演进
Christopher Alexander与建筑模式语言的启示
要理解软件设计模式的本质与价值,必须追溯其思想根源。Christopher Alexander,这位出生于1936年的奥地利裔英国建筑师,在1977年出版的《A Pattern Language》以及1979年出版的《The Timeless Way of Building》中,系统性地提出了一种前所未有的建筑设计方法论——模式语言(Pattern Language)。这套理论的核心观点在于:优秀建筑的诞生并非依赖于天才建筑师的神秘灵感,而是通过一系列可描述、可重复、可传授的“模式”来实现的。Alexander认为,在任何人类聚居的环境中,都存在大量反复出现的设计问题——门廊的尺度、窗户的位置、楼梯的坡度、庭院的布局——而这些问题中的每一个,都存在一个最优或接近最优的解决方案,这个解决方案能够在特定的上下文(Context)中解决特定的问题(Problem),同时产生相应的后果(Consequence)。
Alexander的模式语言包含253个模式,从“独立建筑”(Autonomous House)这样的宏观规划模式,到“窗户位置”(Windows on Two Sides of a Room)这样的微观细节模式,构成了一个从城市设计到室内装饰的完整体系。这些模式并非孤立的教条,而是一个相互关联的网络:每个模式都与其他模式相连接,大模式包含小模式,小模式支撑大模式。Alexander用一种独特的描述结构来阐述每个模式,每个模式都遵循统一的格式:首先说明该模式适用的上下文条件,接着陈述在该上下文中反复出现的问题,然后详细描述解决这个问题的方法,最后阐明采用该方案后产生的后果。这种四段式的结构后来被软件行业直接继承和发展,形成了今天我们所熟悉的模式描述范式。
Alexander的核心洞察在于:模式和模式语言的本质是一种“活的语言”——它不是一套静态的规则汇编,而是一种能够激发创造性解决方案的词汇表和语法规则。正如自然语言允许人们说出无限多的句子一样,模式语言允许设计师解决无限多的具体设计问题,同时保持解决方案之间的一致性和连贯性。Alexander在《The Timeless Way of Building》中写道:“当你使用模式语言时,你所创造的东西将与你所知道的模式一样好;当你不知道模式语言时,你所创造的东西将与你不知道的东西一样糟糕。”这句话不仅适用于建筑领域,更精准地预言了软件设计模式在软件开发中的核心价值。
从建筑到软件:设计模式的诞生
软件行业对设计模式的关注,最初源于Christopher Alexander的工作在建筑领域的巨大成功。Ward Cunningham和Kent Beck等软件先驱在1987年的OOPSLA(Object-Oriented Programming, Systems, Languages & Applications)会议上发表了论文《Using Pattern Languages for Object-Oriented Programs》,首次尝试将Alexander的模式语言思想引入软件设计领域。他们开发了一个名为“Hypertalk”的系统,并在此过程中应用了五个建筑模式来指导用户界面设计。这次尝试虽然规模有限,但它开创了软件行业系统研究设计模式的先河。
此后,James Coplien、Richard Helm、Ralph Johnson等研究者开始在不同场合讨论模式的价值和应用。1994年,在美国德克萨斯州奥斯汀市召开的OOPSLA会议上,四个名字开始被联系在一起:Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides。他们在这次会议上组织了第一次设计模式研讨会,分享各自在软件开发实践中积累的模式经验。同年,这四位作者(后来被业界称为"Gang of Four",即GoF)在Addison-Wesley出版社出版了《Design Patterns: Elements of Reusable Object-Oriented Software》一书,这部著作迅速成为软件工程领域的里程碑式作品,彻底奠定了设计模式作为软件工程核心主题的地位。
GoF的这本书并非凭空创造,而是建立在深厚的实践基础之上。Erich Gamma当时在瑞士联邦理工学院(ETH Zürich)从事软件研究工作,同时参与了一个C++标准模板库的早期实现项目。Richard Helm和Ralph Johnson都是伊利诺伊大学厄巴纳-香槟分校(UIUC)的计算机科学教授,长期从事面向对象技术的研究。John Vlissides则来自IBM T.J. Watson研究中心,专注于软件架构和模式的研究。这四位作者的背景涵盖了学术研究、工业实践和工具开发等多个领域,他们的合作本身就是一次理论与实践深度融合的尝试。
《Design Patterns》全书分为两大部分:前半部分是理论阐述和方法论指导,后半部分则是23个具体模式的详细分析。这23个模式并非作者的凭空想象,而是他们从众多成熟的面向对象系统中精心提炼出来的。其中既有来自Smalltalk环境的设计经验,也有来自ET++(一个早期的C++应用框架)的实践总结,还有来自Boehm的《Software Engineering Economics》等其他来源的研究成果。作者们在撰写本书时,采用了“考古学”的方法:首先研究大量现有的成功系统,找出其中反复出现的设计结构和解决方案,然后分析这些结构解决了什么问题、在什么上下文中有效、产生了什么后果,最终将它们提炼为可描述、可传授的模式。
模式的核心特征:问题-解决方案-上下文的三角结构
理解设计模式的关键,在于把握其独特的三元结构。每一个设计模式都包含三个核心要素:问题(Problem)、解决方案(Solution)和上下文(Context)。这三个要素之间存在着紧密的关联关系,共同构成了模式的完整语义。
问题要素描述了在软件设计中反复出现的一个特定困难或挑战。这个困难不是指具体的实现细节,而是指在特定情境下设计者面临的结构性矛盾。例如,“一个对象依赖于另一个对象,但需要避免它们之间的紧耦合”就是一个典型的问题描述;“如何在不修改客户端代码的前提下增加新功能”描述了另一种常见的设计困境;“怎样确保对象的一致创建”则指向了对象创建层面的问题。问题描述的关键在于其通用性——它必须能够跨越具体的技术栈、应用的业务领域和项目的规模大小,在尽可能广泛的场景中描述同类困难。
上下文要素界定了问题的适用范围和发挥作用的前提条件。上下文不仅包括技术环境(如编程语言特性、运行时环境、框架约束),还包括项目特征(如团队规模、开发周期、维护周期)、组织因素(如代码所有权的划分、发布流程的约束)以及时间因素(如技术的演进阶段、社区的接受程度)。例如,某些模式在动态类型语言和静态类型语言中的适用性完全不同;某些模式在小型团队中能够发挥巨大价值,但在大型组织的政治环境中可能遭遇阻力。准确理解上下文是正确应用模式的前提——将一个在特定上下文中有效的模式盲目移植到另一个上下文中,往往会导致适得其反的效果。
解决方案要素描述了解决所述问题的具体方法,包括参与其中的类或对象的结构、它们之间的职责划分和协作关系、以及各自的行为分配。解决方案不是简单的代码片段或类图,而是一套经过实践检验的设计决策的组合。理解解决方案的关键在于把握其背后的设计意图——为什么这样设计而不是那样设计?这种设计决策在哪些方面取得了平衡?它牺牲了什么来换取什么?优秀的模式描述会清晰地阐述这些设计权衡,而不仅仅是展示最终的类图结构。
这三个要素之间的关系可以用一个三角结构来形象地表示:上下文是基础,定义了模式适用的场景范围;问题是在特定上下文中涌现出来的设计困难;解决方案则是针对这个困难的具体设计手段。上下文和问题共同决定了什么样的解决方案是有效的,而解决方案又会反过来影响上下文——它在解决问题的同时,也可能引入新的约束或改变原有的上下文条件。
模式与原则之间的关系也是理解模式的重要维度。软件设计原则(如单一职责原则、开放封闭原则、里氏替换原则、依赖倒置原则、接口隔离原则等SOLID原则,以及DRY原则、KISS原则、YAGNI原则等)是指导设计决策的高级准则,它们回答的是“为什么”的问题;而设计模式是这些原则在特定情境下的具体实现,它们回答的是“怎么做”的问题。原则是抽象的、通用的,它们指导模式的设计但本身不是模式;模式是具体的、可操作的,它们是原则在实践中的具体化。例如,“针对接口编程,而不是针对实现编程”这一依赖倒置原则,在策略模式(Strategy Pattern)中通过定义抽象的策略接口并让具体策略实现该接口来体现,在状态模式(State Pattern)中通过定义抽象的状态接口并让具体状态实现该接口来体现,在装饰器模式(Decorator Pattern)中通过定义抽象的组件接口并让具体组件和装饰器都实现该接口来体现。同一个原则可以体现在多种不同的模式中,而同一个模式也可以体现多个不同的设计原则。
GoF 23模式的分类体系
GoF在《Design Patterns》一书中提出的23个模式,构成了软件设计模式领域最经典、最核心的知识体系。这23个模式按照其目的(Purpose)可以被划分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)和行为型模式(Behavioral Patterns)。这种三分法基于模式处理问题的主要关注点:创建型模式处理对象创建机制相关的问题,结构型模式处理对象组合方式相关的问题,行为型模式处理对象交互与职责分配相关的问题。
创建型模式:对象创建的艺术
创建型模式关注的是对象创建的机制,它们抽象了对象实例化的过程,使得系统在创建对象时不必关心对象的具体类型,而是通过统一的接口或机制来获取所需的对象实例。创建型模式的核心价值在于:将“对象是怎样创建的”这一细节从使用对象的代码中分离出来,从而提高系统的灵活性和可维护性。
GoF定义了5个创建型模式。单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这个模式在需要控制资源访问、协调跨系统行为、维护全局状态等场景中非常有用。例如,应用程序的配置管理器、数据库连接池、日志记录器等都常常采用单例模式实现。然而,单例模式也因其带来的全局状态和隐式依赖而备受争议,在现代软件架构中需要谨慎使用。
工厂方法模式(Factory Method Pattern)定义了一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法模式将对象的创建延迟到子类中进行,使得系统能够在不修改现有代码的情况下引入新的产品类型。这种模式在框架设计中尤为有用——框架定义创建对象的接口,而具体的产品类则由应用程序来提供。
抽象工厂模式(Abstract Factory Pattern)提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式适用于需要创建一组相互关联的对象集合的场景,例如在一个支持多种视觉主题的GUI框架中,不同的主题需要协调一致的各种UI组件。
建造者模式(Builder Pattern)将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。这个模式在构建过程复杂、需要创建不同表示的对象、或对象创建的参数很多但大部分是可选的情况下特别有用。经典的例子包括StringBuilder、DocumentBuilder等。
原型模式(Prototype Pattern)通过复制现有的实例来创建新实例,而不是通过类的构造函数。原型模式在创建对象的成本较高(例如需要复杂的初始化过程或需要访问外部资源)、而复制现有实例更为高效的场景中非常有用。原型模式还需要解决深拷贝和浅拷贝的问题,这在实现时需要特别小心。
结构型模式:对象组合的智慧
结构型模式关注的是如何将类或对象组合成更大的结构,以实现新的功能或满足新的需求。这些模式主要处理对象之间的关系,通过组合、继承、委托等手段来构建灵活的系统架构。结构型模式的核心价值在于:它们提供了一套经过实践检验的组合技术,使得系统能够以可控的方式增长和变化。
GoF定义了7个结构型模式。适配器模式(Adapter Pattern)将一个类的接口转换成客户端期望的另一个接口,使得原本接口不兼容的类可以合作无间。适配器模式有类适配器和对象适配器两种实现方式:类适配器通过多重继承实现,对象适配器通过组合实现。适配器模式常用于集成第三方库、改造遗留系统、或者在系统演进过程中处理接口演变。
桥接模式(Bridge Pattern)将抽象部分与它的实现部分分离,使它们都可以独立地变化。桥接模式的核心思想是用组合关系代替继承关系,从而避免类爆炸的问题。当一个类存在两个或多个独立变化的维度时,桥接模式特别有用——例如,图形界面系统中的形状(圆形、矩形、三角形)和渲染方式(Windows风格、Mac风格、Web风格)就是两个独立变化的维度。
组合模式(Composite Pattern)将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。组合模式在需要处理树形结构数据、实现文件系统、设计图形界面、处理组织架构等场景中非常有用。组合模式的关键在于为容器对象和叶子对象定义统一的接口,使得客户端可以透明地处理单个对象和组合对象。
装饰器模式(Decorator Pattern)动态地给对象添加一些额外的职责,就增加功能来说,装饰器模式比生成子类更加灵活。装饰器模式通过组合而非继承的方式,在运行时为对象添加行为,这与继承相比具有更大的灵活性。Java I/O库中的FilterInputStream、FilterOutputStream就是装饰器模式的经典应用。
外观模式(Facade Pattern)为子系统中的一组接口提供一个统一的接口,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式在需要为一个复杂的子系统提供简单接口、降低客户端与子系统之间的耦合度、或者需要层次化子系统结构等场景中非常有用。
享元模式(Flyweight Pattern)运用共享技术有效地支持大量细粒度的对象。享元模式通过共享尽可能多的对象状态来减少内存占用,适用于需要创建大量相似对象的场景。字符对象的处理(字符串池)、图形对象的渲染(棋盘游戏的棋子)、文档编辑器的字符等都是享元模式的典型应用。
代理模式(Proxy Pattern)为其他对象提供一种代理以控制对这个对象的访问。代理模式在需要控制对对象的访问、实现延迟初始化、实现远程调用、实现访问日志、实现访问缓存等场景中非常有用。代理模式有多种变体,包括远程代理、虚拟代理、保护代理、缓冲代理、智能引用代理等。
行为型模式:对象交互的协调
行为型模式关注的是对象之间的交互和职责分配,它们定义了对象之间如何通信、如何协调工作、如何分担责任。行为型模式是面向对象设计中最为丰富的领域,因为它们触及了软件设计的核心问题——如何组织对象之间的协作。
GoF定义了11个行为型模式,数量最多,涉及的问题也最为广泛。责任链模式(Chain of Responsibility Pattern)使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。责任链模式将请求沿着处理者链传递,直到有一个处理者处理它为止。这种模式在需要动态构建处理链、需要将请求发送者和多个潜在的处理者解耦等场景中非常有用。
命令模式(Command Pattern)将请求封装成对象,从而使得可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。命令模式将发出请求的对象和执行请求的对象解耦,使得系统可以添加新命令而不影响现有代码。命令模式在实现撤销/重做功能、实现事务性操作、实现异步任务队列、设计宏命令等场景中非常有用。
解释器模式(Interpreter Pattern)给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。解释器模式为某种语言定义一个语法表示,并提供一个解释器来处理这个语法。虽然解释器模式在某些特定领域(如配置文件解析、正则表达式处理)中有应用,但由于它需要为每条语法规则实现一个类,当语法规则复杂时会导致类数量急剧膨胀,因此使用范围相对有限。
迭代器模式(Iterator Pattern)提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。迭代器模式将遍历聚合对象的责任抽象为独立的迭代器对象,使得聚合对象的内部表示可以变化而不会影响客户端代码。迭代器模式在几乎所有支持集合遍历的编程语言和框架中都得到了广泛支持。
中介者模式(Mediator Pattern)用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式在需要将对象之间的网状交互关系转变为星形交互关系、或者需要集中管理对象之间的通信逻辑等场景中非常有用。
备忘录模式(Memento Pattern)在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。备忘录模式在需要实现撤销功能、需要保存对象的历史状态、或者需要在对象之间传递状态等场景中非常有用。
观察者模式(Observer Pattern)定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会收到通知并自动更新。观察者模式是发布-订阅模式的一种实现,在GUI框架、事件处理系统、消息中间件、MVC架构等场景中得到了广泛应用。观察者模式的关键在于定义Subject和Observer之间的协议,以及处理观察者列表的动态管理。
状态模式(State Pattern)允许对象在其内部状态改变时改变它的行为,看起来好像对象修改了自己的类。状态模式将对象的状态封装为独立的状态类,使得对象可以在不同状态之间切换而不影响客户端代码。状态模式在处理复杂的状态机、实现工作流引擎、设计游戏状态管理等场景中非常有用。
策略模式(Strategy Pattern)定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。策略模式使得算法可独立于使用它的客户端而变化。策略模式在需要支持多种算法或规则、支持运行时选择算法、需要消除大量条件判断等场景中非常有用。
模板方法模式(Template Method Pattern)定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。