设计模式是一个通过定义、使用、测试去解决特定问题的方法,是针对软件设计中在给定条件下会重复性发生的问题而提出的一种通用性的可重用解决方案,设计模式不是可以直接转化为代码的完整设计,它是用于描述在不同情况下解决问题的通用方案。
设计模式通过提供经过验证的行之有效的开发范式加快开发过程,预防重大的隐患问题,提高代码可读性。
这里主要讨论GoF所提出的23种设计模式,可将其分为三种类型:
注重完成对象的实例化,相比于直接实例化对象,根据实际情况选择合适的设计模式完成对象的实例化,可以为复杂的业务场景带来更高的灵活性。 创造型设计模式主要包括以下几种:
结构型设计模式用于指导我们完成对代码的结构划分,如此,代码结构会更加清晰,更易理解,也提高了软件的可维护性。 结构型设计模式主要包括以下几种:
行为型设计模式主要用于定义对象之间的通信与流程控制,主要的设计模式都非常注重优化对象之间的数据交互方式。 行为型设计模式主要包括以下几种:
恰当使用设计模式能够提高代码的复用性,但是由于复用性往往会引入封装与间接调用,这些会降低系统性能,增加代码复杂程度。因此,除非设计模式能够帮助我们完成代码的实现或者后续的维护工作,否则没有必要去引入设计模式。 学习设计模式的关键并不在于学习设计模式本身,而是在于识别应用场景与潜在的风险,并将设计模式用之有道,这般,设计模式才能算作得心应手的工具。 在没有必要的情况大可不必去使用设计模式,因为设计模式有可能会牺牲代码的简洁性,而且滥用设计模式多会引入新的问题却没有解决原来的问题。 保持代码的整洁,模块化和可读性,同时不要让各类之间过度耦合。
创造型设计模式主要关注的是类的实例化,也就是说体现的是对象的创建方法,利用这些模式,我们可以在适当的情况下以适当的形式创建对象,创造型设计模式通过控制对象的创建来解决设计中的问题。 创造型设计模式主要包含以下子类别:
主要完成对象创建,并将对象中部分内容放到其他对象中创建。
主要完成类的实例化,并将类中的部分对象放到子类中创建,此类模式在实例化过程中高效地利用了继承机制 创造型设计模式主要包含以下5种具体的设计模式:
提供一个用于创建相关对象或相互依赖对象的接口,无需指定对象的具体类
将复杂对象的构建与其表示相互分离,使得同样的构建过程可以创建不同的表示
允许在子类中实现本类的实例化类
使用一个原型实例来指定创建对象的种类,然后通过拷贝这些原型实现新对象的创建
确保某个类在系统中仅有一个实例,并提供一个访问它的全局访问点
对象创造型设计模式 | 类创造型设计模式 |
---|---|
抽象工厂设计模式 | 工厂方法设计模式 |
生成器设计模式 | |
原型设计模式 | |
单例设计模式 |
工厂方法的作用是创建对象,用来从一组实现特定逻辑的类中实例化某个对象。
抽象工厂模式相比于工厂方法模式的抽象层次更高。这意味着抽象工厂返回的是一组类的工厂。与工厂方法模式类似(返回多个子类中的一个),此方法会返回一个工厂,而这个工厂会返回多个子类中的一个。简单来说,抽象工厂是一个工厂对象,该对象又会返回若干工厂中的一个。 工厂模式是创造型模式的典型示例。抽象工厂设计模式是工厂方法模式的扩展,从而使我们无须担心所创建对象的实际类就能够创建对象。抽象工厂模式扩展了工厂方法模式,允许创建更多类型的对象。
抽象工厂模式的主要优点之一是它屏蔽了这些具体类的创建方法。实际应用的类名称不需要再让客户端(将客户端与具体类解耦)知道。由于具体类是屏蔽的,因此我们可以在不同的工厂(实现方法)之间进行切换。
生成器模式,能够从简单的对象一步一步生成复杂的对象。生成器模式是一种用来逐步构建复杂对象并在最后一步返回对象的创造型模式。 构造一个对象的过程是通过泛型实现的,以便它能够用于对同一对象创建不同的表示形式。
生成器模式隐藏了产品构建过程中的内部细节。各个生成器之间都是相互独立的。这提高了代码的模块化,并使其他的生成器更方便地创建对象。因为每个生成器都能够逐步创建对象,这让我们能够很好地对最终产品进行掌控。
在应用程序的整个生命周期中,对象只有一个实例的时候,就会使用单例设计模式。单例类总是在第一次被访问时完成实例化,直至应用程序退出之前,都只会使用同一个实例。单一实例创建策略:我们通过限制构造函数(通过设置其为私有)从而限制单例类的实例化。之后在定义类时包含一个该类的静态私有对象,以便创建单例类的实例。 在单例模式中,最棘手的部分是对单一实例的实现和管理。 在单例模式的定义过程中,有两点需要注意的地方:
单例模式中的主动实例化和被动实例化(饿汉、懒汉) 线程安全的单例:双重同步锁、静态变量、枚举
在应用程序的整个生命周期中,对象只有一个实例的时候,就会使用单例设计模式。
相比于以往创建一个复杂对象总是费时费力,原型模式只需要复制现有的相似对象,并根据需要做适当修改。原型意味着使用克隆方法。克隆方法是一种复制对象的操作。克隆出的对象副本被初始化为调用克隆方法时原始对象的当前状态。这意味着对象的克隆避免了创建新对象。如果创建一个新对象的开销很大,而且有可能引起资源紧张时,我们就克隆对象。
使用克隆方法来复制对象时,具体是使用浅层复制还是深层复制是由业务需求来决定的。在使用原型模式时,使用克隆方法来复制对象仅仅是一个设计上的决策。克隆方法对于原型模式来说并不是强制性的最佳选择。
主要难点:
优点:
副作用:
结构型模式主要描述如何将对象和类组合在一起以组成更复杂的结构。在软件工程中结构型模式是用于帮助设计人员通过简单的方式来识别和实现对象之间关系的设计模式。结构型模式会以组的形式组织程序。这种划分形式使代码更加清晰,维护更加简便。结构型模式用于代码和对象的结构组织。 结构型模式会以组的形式组织程序。这种划分形式使代码更加清晰,维护更加简便。 结构型模式又分为以下子类别:
具体包括:
对象结构型模式 | 类结构型模式 |
---|---|
桥接模式 | 类适配器模式 |
组合模式 | |
装饰模式 | |
门面模式 | |
享元模式 | |
对象适配器模式 | |
代理模式 |
软件适配器的工作原理也和插座适配器完全一样。我们也经常需要在程序中使用到不同的类或模块。假设有一段代码写得很烂,如果我们直接将这些代码集成到程序中,会将现有的代码搞乱。但是我们又不得不调用这段代码,因为我们需要实现相关的功能,而从头写起会耽误很多宝贵的时间。这时的最佳实践就是编写适配器,并将所需要的代码包装进去。这样我们就能够使用自定义的接口,从而降低对外部代码的依赖。 适配器模式会将现有接口转换为新的接口,已实现对应用程序中不相关的类的兼容性和可重用性的目标。适配器模式也被称为包装模式。适配器模式能够帮助那些因为接口不兼容而无法一起工作的类,以便它们能够一同工作。 适配器模式也负责将数据转换成适当的形式。当客户端在接口中指定了其对数据格式的要求时,我们通常可以创建新的类以实现现有类的接口和子类。这种实现方式也会通过创建类适配器,实现对客户端调用命和现有类中被调用方法之间接口的转换。
在具体实践上,有两种实际应用适配器模式的方法:
应用场景:
桥接模式是结构型模式中的另一个典型模式。桥接模式用于将类的接口与接口的实现相互解耦。这样做提高了系统的灵活性使得接口和实现两者均可独立变化。 举一个例子,让我们想一下家用电器及其开关。例如,风扇的开关。开关是电器的控制接口,而一旦闭合开关,实际让风扇运转的是风扇电机。 所以,在这个示例中,开关和风扇之间是彼此独立的。如果我们将开关接到电灯泡的供电线路上,那么我们还可以选用其他开关来控制风扇。
桥接模式主要适用于系统的多个维度上都经常发生变化的情况。桥接模式能够将不同的抽象维度进行衔接。通过桥接模式,抽象化对象和实现化对象不会在编译时进行绑定,而能够在各自的类被调用时独立扩展。 当你经常需要在运行时在多个实现之间进行切换时,桥接模式也非常有用。
在大部分系统开发过程中,程序员都会遇到某个组件既可以是独立的个体对象,也能够作为对象集合的情况。组合模式就用于此类情况的设计。简单来说,组合模式是一组对象的集合,而这组对象中的每一个对象本身也是一个组合模式构成的对象,或者只是一个原始对象。 组合模式中存在着一个树形结构,并且在该结构中的分支节点和叶节点上都能够执行相同的操作。树形结构中每一个分支节点都包含子节点的类(能继承出叶节点和分支节点),这样的分支节点本身就是一个组合模式构成的节点。树形结构中的叶子节点仅是一个原始对象,其没有子节点(不能继承出叶节点和分支节点)。组合模式的子类(下一级节点)可以是叶子节点或其他组合模式。
组合模式的目的是能够使独立对象(单个分支节点或叶子节点)和对象集合(子树)都能够以同样的方式组织起来。组合模式中所有的对象都来自于其本身(成为一种嵌套结构)。组合模式允许我们使用递归的方式将类似的对象组合成一种树形结构,来实现复杂结构对象的构建。
装饰设计模式用来在运行时扩展或修改一个实例的功能。一般来说,继承可以扩展类的功能(用于类的所有实例)。但与继承不同的是,通过装饰模式,我们可以选择一个类的某个对象,并对其进行修改,而不会影响这个类中其他的实例。继承会直接为类增加功能,而装饰模式则会通过将对象与其他对象进行包装的方式将功能添加到类。
装饰设计模式用来在运行时扩展或修改一个实例的功能。一般来说,继承可以扩展类的功能(用于类的所有实例)。但与继承不同的是,通过装饰模式,我们可以选择一个类的某个对象,并对其进行修改,而不会影响这个类中其他的实例。继承会直接为类增加功能,而装饰模式则会通过将对象与其他对象进行包装的方式将功能添加到类。
许多业务流程都会涉及复杂的业务类操作。由于流程很复杂,所以其涉及了多个业务对象,这往往会导致各个类之间的紧密耦合,从而降低系统的灵活性和设计的清晰度。底层业务组件间的复杂关系会使客户端的代码编写变得很困难。 门面模式简化了到复杂系统的外部接口。为此它会对所有的类进行整合,并构建一个复杂系统的子系统。 门面模式能够将用户与系统内部复杂的细节相互屏蔽,并只为用户提供简化后的更容易使用的外部接口。同时它也将系统内部代码与接口子系统的代码相互解耦,以便修改和升级系统代码。 相比于其他设计模式,门面模式更注重实现代码的解耦。它所强调的是代码设计中很重要的一点,即代码抽象。通过提供一个简单的接口并隐藏其后的复杂性,从而实现抽象。 在这种方式下,代码的实现完全交由门面层处理。客户端只会与一个接口交互,同时也只有和这个接口交互的权限。这样就能隐藏全部系统的复杂性。总而言之,门面模式通过提供一个简单的接口为客户端简化了与复杂系统的交互。 从另一方面看,门面模式也保证了能够在不修改客户端代码的情况下对具体实现方法进行修改。
根据目的不同,有各种不同类型的代理。例如,有保护性代理,控制对某个对象的访问权限;有虚拟代理,处理开销过大而难以创建的对象,并通过远程访问控制来访问远程对象。
代理模式主要用于当我们需要用一个简单对象来表示一个复杂对象的情形。如果创建对象的开销很大,那么可以推迟其创建,并使用一个简单对象来代理其功能直到必须立即创建的时候。这个简单对象就可以称为复杂对象的代理。
享元模式能够减少用于创建和操作大量相似的细碎对象所花费的成本。享元模式主要用在需要创建大量类似性质的对象时。大量的对象会消耗高内存,享元模式给出了一个解决方案,即通过共享对象来减少内存负载它的具体实现则是根据对象属性将对象分成两种类型:内蕴状态和外蕴状态。 共享是享元模式的关键。
当我们选择享元模式的时候,需要考虑以下因素:
行为型模式是一类主要关注对象间相互通信(交互)的设计模式。这些对象之间的相互作用既能保证对象间能够交换数据,同时对象间仍然能够保持松耦合。 紧耦合一般会发生在一组紧密关联(相互依赖)的类之间。在面向对象的设计过程中,耦合引用的数量和设计过程中类与类之间的相互依赖是成正比的。用通俗的话讲,就是当一个类变化的时候,有多少可能需要同时修改其他类呢? 松耦合是软件架构设计的关键。在行为型模式中,功能实现与调用该实现的客户端之间应该是松耦合的,以避免硬编码和依赖性。 行为型模式处理不同的对象之间的通信关系,为其提供基本的通信方式,并提供实现这种通信方式的最常用、最灵活的解决方案。 行为型模式描述的不仅是类或对象的模式,同时也包括了它们之间的通信模式。行为型模式能够用来避免硬编码和依赖性。 行为型模式又分为以下子类别:
具体包括:
对象行为型模式 | 类行为型模式 |
---|---|
职责链模式 | |
解释器模式 | |
命令模式 | 模板方法模式 |
迭代器模式 | |
中介者模式 | |
备忘录模式 | |
观察者模式 | |
状态模式 | |
策略模式 | |
访问者模式 |
在职责链模式中,由发送端发送一个请求到一个对象链中,链中的对象自行处理请求。如果链中的对象决定不响应请求,它会将请求转发给链中的下一个对象。 职责链的目的是通过特定设计对请求的发送者和接收者之间进行解耦。解耦是软件设计中很重要的一个方面。通过该设计模式能够使我们彻底地将发送者和接收者之间完全解耦。发送者是用于调用操作的对象,接收者是接收请求并执行相关操作的对象。通过解耦,发送者不需要关心接收者的接口。 在职责链模式中,职责是前后传递的。对于链中的对象,决定谁来响应请求的责任由整个链中左侧的对象来承担。这就像问答测验的时候传递问题一样。当提问者向一个人提问,如果他不知道答案,他就把问题传给下一个人,以此类推。当一个人回答了问题,问题就会停止向下传递。有时,也可能到达最后一个人时,还是没有人能回答问题。 我们能举出若干个职责链模式的例子:硬币分拣机、ATM取款机、Servlet过滤器和Java的异常处理机制。 在Java中,我们可以在catch语句中列出的异常序列时就抛出一个异常,catch列表从上到下逐条扫描。如果赶上第一个进行异常处理就可以立刻完成任务,否则责任转移到下一行,直到最后一行。
命令模式(也称为行动模式、业务模式)是一个对象行为型模式。 这使我们能够实现发送者和接收者之间完全解耦。发送者是调用操作的对象,接收者是接收请求并执行特定操作的对象。通过解耦,发送者无须了解接收者的接口。在这里,请求的含义是需要被执行的命令。
注意事项:
解释器模式是一种用于在程序中解析特定语法的设计模式。解释器模式是组合模式的一种应用。 对于特定的某种语言,解释器模式能够定义针对其语法表示形式的解释器,并实现对该语言语句的翻译和解释。
解释器模式的适用范围非常有限。我们可以说解释器模式仅仅用于需要进行正式语法解释的地方,但这些领域往往已经有了更好的标准的解决方法,因此,在实际使用中,并不会经常使用该模式。该模式可以用于解释使用了特定语法的表达式或者建立某个简单的规则引擎的时候。
迭代器模式也是一种行为型模式。迭代器模式允许对一组对象元素的遍历(也叫收集)以完成功能实现。
中介者模式主要是关于数据交互的设计模式。中介者设计模式很容易理解,却难以实现。该模式的核心是一个中介者对象,负责协调一系列对象之间一系列不同的数据请求。这一系列对象称为同事类。 同事类会让中介者知道它们会发生变化这样中介者负责处理变化对不同对象之间交互的影响。
需要注意的问题: 实际使用中介者模式的时候,反而会让问题变得越来越复杂。所以最佳的实践是仅让中介者类负责对象之间的通信部分。
功能:
我们每天至少会使用一次这种模式。备忘录模式提供了一种使对象恢复到其以前状态的能力(通过回滚撤销)。备忘录模式是通过两个对象实现的:发起者和管理者。发起者是具有内部状态的某个对象。管理者则会对发起者执行一些操作,并实现撤销更改。
当我们在实际应用中需要提供撤销机制,当一个对象有可能需要在后续操作中恢复其内部状态时,就需要使用备忘录模式。结合本设计模式实现对象状态序列化,能够使其易于保存对象的状态并进行状态回滚。 当一个对象状态的快照必须被存储,且在后续操作过程中需要被恢复时,就可以使用备忘录模式。
在观察者模式中,一种叫作被观察者的对象维护了观察者对象的集合。当被观察者对象变化时,它会通知观察者。 在被观察者对象所维护的观察者集合中能够添加或删除观察者。被观察者的状态变化能够传递给观察者。这样观察者能够根据被观察者的状态变化做出相应的改变。
状态模式是一种行为型模式。状态模式背后的理念是根据其状态变化来改变对象的行为。状态模式允许对象根据内部状态(内容类)实现不同的行为。内容类可以具有大量的内部状态,每当对内容类调用 request方法时,消息就被委托给状态类进行处理。 状态类接口定义了一个对所有具体状态类都有效的通用接口,并在其中封装了与特定状态相关的所有操作。具体状态类对请求提供各自具体的实现。当内容类的状态变化时,那么与之关联的具体状态类也会发生一定相应的改变。
策略模式主要用于需要使用不同的算法来处理不同的数据(对象)时。这意味着策略模式定义了一系列算法,并且使其可以替换使用。策略模式是一种可以在运行时选择算法的设计模式。 本模式可以使算法独立于调用算法的客户端。策略模式也称为政策模式。在使用多种不同的算法(每种算法都可以对应一个单独的类,而每个类的功能又各不相同)时可以运用策略模式。
当我们有多种不同的算法可供选择(每种算法都可以对应一个单独的类,而每个类的功能又各不相同)时,可以应用策略模式。策略模式会定义一组算法并能够使其相互替代使用。
模板方法会定义算法的各个执行步骤。算法的一个或多个步骤可以由子类通过重写来实现,同时保证算法的完整性并能够实现多种不同的功能。 类行为型模式使用继承来实现模式的功能。在模板方法模式中,会有一个方法( Template method方法)来定义算法的各个步骤。这些步骤(即方法)的具体实现会放到子类中。也就是说,在模板方法中定义了特定算法,但该算法的具体步骤仍然需要通过子类来定义。模板方法会由一个抽象类来实现在这个抽象类中还会声明该算法的各个步骤(方法),最后将其具体实现的方法声明实现为抽象类的子类。
应用场景:
访问者模式用来简化对象相关操作的分组。这些操作是由访问者来执行的,而不是把这些代码放在被访问的类中。由于访问的操作是由访问者执行的,而不是由被访问的类,这样执行操作的代码会集中在访问者中,而不是分散在对象分组中。这为代码提供了更好的可维护性。访问者模式也避免了使用 instanceof运算符对相似的类执行计算。
在 visitCollection()方法中,我们调用 Visitable.accept(this)来实现对正确的访问者方法进行调用。这叫作双重分派。访问者调用元素类中的方法,又会回到对访问者类中进行调用。
模式问题: 在使用访问者模式的情况下,要想添加新的具体元素(数据结构)会更加困难。添加一个 ConcreteElement会涉及向访问者接口添加新的操作和在每一个具体访问者实现中添加对应的实现。
访问者模式更适用于对象结构非常稳定,而对象的操作却需要经常变化的情况下。 访问者模式只提供处理每种数据类型的方法,并且让数据对象确定调用哪个方法。由于数据对象本质上都知道其自身的类型,所以在访问者模式中算法决定所调用的方法所起到的作用是微不足道的。 因此,数据的整体处理包括对数据对象的分发以及通过对适当的访问者处理方法的二次分发。
这就叫作双重分派。 使用访问者模式的一个主要优点是,对于在我们的数据结构中添加需要执行的新的操作来说,是非常容易的。我们所要做的就是创建一个新的访问者,并定义相应的操作。 访问者模式的主要问题是,因为每个访问者需要有相应的方法来处理每一种可能的具体数据,那么一旦实现了访问者模式,其具体类的数量和类型就不能被轻易改变。