设计模式问答(一)
什么是设计模式?您能说出工厂模式、抽象工厂模式、创建者模式、原型模式、原型模式的潜复制及深复制、单例模式、命令模式的原理吗?
简介
这是一个小巧的设计模式常见问题问答。在本节我们将一起探讨工厂模式、抽象工厂模式、创建者模式、原型模式、原型模式的浅复制及深复制、单例模式、命令模式的原理。
在下面的链接中您可以阅读设计模式常见问题问答的后续部分 :-
设计模式 FAQ’s 2 ---解释器模式、迭代器模式、调停者模式、备忘录模式、观察者模式(http://www.codeproject.com/KB/aspnet/SoftArch2.aspx)
设计模式 FAQ’s 3 ---状态模式、策略模式、访问者模式、适配器模式、享元模式 (http://www.codeproject.com/KB/aspnet/SoftArch3.aspx)
设计模式 FAQ’s 4 ---桥接模式、组合模式、装饰器模式、门面模式、责任链模式、代理模式、模板模式( http://www.codeproject.com/KB/aspnet/SoftArch4.aspx)
什么是设计模式?
设计模式是给定的上下文中重复出现问题的解决方案。所以基本上一个解决方案能处理同一类型的问题。设计模式应该在软件开发的初始阶段以适当的形式存在。比如说,您想实现一个排序算法并首先想到了冒泡排序。所以问题就是如何实现排序,解决方案是冒泡排序。设计模式也是如此。
设计模式主要有哪三种分类?
设计模式有三种基本类别:创建型模式、结构型模式和行为型模式
创建型模式:
抽象工厂模式:为一组类创建实例。
建造者模式:将对象的构造和表现分离开来。
工厂方法模式:为多个派生类创建一个实例。
原型模式:一个完全初始化对象实例的拷贝或克隆。
单例模式:只有一个实例存在的类。
注意记住创建型模式最好的方式是谨记ABFPS (Abraham Became FirstPresident of States 亚伯拉罕成为国家第一任总统)
结构型模式
适配器模式:匹配不同接口的类。
桥接模式:将一个对象的抽象部分从实现中分离出来。
组合模式:树结构的简单和复合对象。
装饰器模式:为对象动态添加职责。
门面模式:一个代表整个系统的类。
享元模式:用来细粒度的高效共享实例。
代理模式:用一个对象表示另一个对象。
行为型模式
调停者模式:定义了简化类和类之间的交互。
备忘录模式:捕获和恢复一个对象的内部状态。
解释器模式:将语言元素包含在一个程序中的一种方式。
迭代器模式:按顺序访问集合中的元素。
职责链模式:在一连串对象中传递气球的一种方式。
命令模式:将一个请求封装成一个对象。
状态模式:当一个对象状态发生变化时选择该对象的一种行为。
策略模式:将一个算法封装到一个类之中。
观察者模式:一种通知若干类发生变化的机制。
模版方法模式:推迟具体步骤的算法到子类中。
访问者模式:在类不变的情况下定义一个新的操作。
你能否解释清楚工厂模式?
工厂模式属于创建型模式。这你可以从名字中“工厂”两字看得出来,它意味这构造或者创建一些东西。在软件架构的范畴工厂模式意味着集中创建一些对象。下面是客户不同类型的发票的代码片段,这些发票的创建依赖于客户端指定的发票类型。以下代码有两个问题:
首先,我们有很多“new”关键字散落在客户端。换句话说,客户端加载时有肯多创建对象的过程使得客户端逻辑变得非常复杂。
其次,客户端需要知道所有的发票类型。所以,如果我们我们要添加一种新的发票类型如“InvoiceWithFooter”,那么客户端需要引入新的类并且需要重新编译。
图1:不同类型的发票
以这些问题作为基础,现在我们将看看工厂模式如何帮我们解决问题。下图中“工厂模式”展示的了两个具体类‘ClsInvoiceWithHeader’和‘ClsInvoiceWithOutHeader’。
第一个问题是这些类直接和客户端有联系导致了客户端代码中散落了很多“new”关键字。这个问题通过引入一个新的类‘ClsFactoryInvoice’来做所有创建对象的工作来解决。
第二个问题是客户端代码知道所有具体类,如‘ClsInvoiceWithHeader’和‘ClsInvoiceWithOutHeader’。这导致了增加新的发票类型时需要重新编译客户端代码,例如如果增加‘ClsInvoiceWithFooter’类型的发票,客户端代码需要改变并重新编译。为了解决这个问题我们引入了一个通用接口‘IInvoice’,两个具体类‘ClsInvoiceWithHeader’和‘ClsInvoiceWithOutHeader’都继承并实现‘IInvoice’接口。
客户端只需要引用‘IInvoice’接口,这使得客户端和具体类(‘ClsInvoiceWithHeader’和‘ClsInvoiceWithOutHeader’)0关联。所以现在当我们再去添加新的发票类时客户端不需要做任何改动。
在创建对象的那行只需要关注‘ClsFactoryInvoice’即可,这样客户端通过只关注‘IInvoice’接口取消了与具体类的关联。
图2:工厂模式
以下是用C#实现的工厂模式代码片段。为了避免重新编译客户端我们引入了‘IInvoice’发票接口。‘ClsInvoiceWithOutHeaders’和‘ClsInvoiceWithHeader’都实现该接口。
图3:接口和具体类
我们还引入了一个额外的包含getInvoice()方法的类‘ClsFactoryInvoice’,该类会根据发票类型来生成发票对象。简而言之,我们将创建对象的逻辑集中在了‘ClsFactoryInvoice’中;客户端只需要调用‘getInvoice’方法就可以生成发票对象。其中最重要的一点需要注意的是客户端只引用‘IInvoice’类型并且工厂类‘ClsFactoryInvoice’同样只返回‘IInvoice’类型的引用。这有助于客户端从具体类中解耦,因此,当我们新加发票类型对应的类时不需要重新编译客户端。
图4:生成发票的工厂类
你能否解释清楚抽象工厂模式?
抽象工厂扩展了基本工厂模式。抽象工厂模式帮助我们将相似的工厂模式类集中到一个统一的接口下。所以现在基本上所有常见的工厂模式都继承自一个通用的抽象工厂类来将他们统一到一个共同类中。其他和工厂模式相关的东西都和之前讨论的一样。
一个工厂类帮助我们将类的创建和类型集中起来。抽象工厂模式帮助我们形成相关工厂模式之间的一致性从而为客户端提供更简单的接口。
图5:抽象工厂结合相关工厂模式
现在既然我们已经知道基本要素那么让我们尝试来理解抽象工厂模式的实现细节。如之前所说,我们通过继承将工厂模式类(factory1和factory2)绑定至一个共同的抽象工厂(AbstractFactory接口)。工厂类位于具体类之上,再次从共同接口中推导。例如图“实现抽象工厂”中具体类‘product1’和‘product2’都继承自同一个接口,如‘common’。而需要使用具体类的客户端只需要和抽象工厂以及具体类所实现的抽象接口交互即可。
图6:实现抽象工厂
现在我们来看看如何在实际代码中实现抽象工厂模式。我们有一个场景是在ui对象创建中我们需要通过文本框(textbox)和按钮(button)各自的中心化工厂类‘ClsFactoryButton’和‘ClsFactoryText’来创建各自对象。这两个类都继承自同一个接口‘InterfaceRender’;并且两个工厂类都继承自共同的工厂类‘ClsAbstractFactory’。图“抽象工厂例子”中展示了这类的关系以及客户端代码是相同的。其中重要的一点是客户端代码不需要与具体类交互。对于对象的创建,客户端使用抽象工厂类(ClsAbstractFactory),而调用具体类则通过调用接口‘InterfaceRender’中的共用方法来完成。因此‘ClsAbstractFactory’类为‘ClsFactoryButton’和‘ClsFactoryText’的工厂类提供了共同的抽象接口。
图7:抽象工厂例子
下面我们将运行例子中抽象工厂的代码。下图“抽象工厂和工厂代码快照”中的代码快照展示的工厂模式类与抽象工厂的继承关系。
图8:抽象工厂和工厂代码快照
图“具体类通用接口”展示了具体类和通用接口‘InterFaceRender’的继承关系,其中‘InterFaceRender’接口为所有具体类强制增加‘render’方法。
图9:通用接口
最后要做的事情就是客户端通过使用‘InterfaceRender’接口和‘ClsAbstractFactory’抽象工厂来调用和创建对象。其中关键要素是该代码完全和具体类隔离。所以任何具体类的变化如增加或删除具体类都不会引起客户端改变。
图10:客户端、接口、抽象工厂
你能否解释清楚创建者模式?
创建者模式属于创建型模式分类。创建者模式帮助我们将对象的复杂构造从其表现中分离开来,所以相同的构造处理过程可以创建出不同的表现行为。当一个对象的构造函数非常复杂时创建模式非常有用;该模式主要目的就是将对象的构造从其表现中分离。如果我们可以将对象的构造与行为分离,我们就可以了从相同的构造中获得许多不同行为的对象。
图11:创建者模式概念
为了理解我们所说的构造和行为,我们举个“准备茶”的例子。我们可以从图“泡茶”中看到,从相同的泡茶步骤中我们可以得到3种不同表现的茶(如不含糖的茶、含糖或牛奶的茶、不含牛奶的茶)。
图12:泡茶
现在让我们来举个软件方面的例子来看看如何将对象复杂的创建和它的表现分开。假设我们有一个应用,我们需要将同一个报表显示为pdf和excel格式。图“请求报表”展示了一系列步骤来达到同一目的的流程。传入需要创建的报表类型、设置报表头和脚、最终报表得以展示。
图13:请求报表
接下来让我们从下图“不同视角”中从不同视角来再次看该问题。图“请求报表”中定义的相同流程被分解为表现和通用构造。其中构造过程对不同类型的报表处理都是相同的,但是他们的表现形式却各不相同。
还是这个报表问题,我们尝试使用创建者模式来解决该问题。实现创建者模式分为以下3个主要部分:
Builder:主要负责定义各个部分的处理过程。Builder将各个处理过程初始化并配置到产品中。
Director:负责取到各个处理过程并定义构建产品的流程。
Product:该对象为builder和director协作创建而成的最终对象。
下面让我们来看下创建者模式的类继承关系。我们从自定义创建者(builder)如‘ReportPDF’、‘ReportEXCEL’中抽象出类‘ReportBuilder’。
图14:创建者类继承关系
下图“创建者类代码”展示了这些类的方法。为了生成报表我们首先需要创建一个报表对象,接下来设置报表类型(EXCEL或者PDF)、设置表头、设置页脚,最终返回报表。目前我们定义了两个自定义创建者,一个是‘PDF’(ReportPDF)另一个为‘EXCEL’(ReportExcel)。这两个自定义创建者都根据报表类型定义有自身的处理过程。
图15:创建者代码
接下来我们来了解director是如何工作的。‘clsDirector’持有builder并按顺序调用各个方法处理。因此director就行司机一样持有所有单个处理过程并按照一定顺序来调用他们来生成最终产品;在这个例子中的体现就是报表的生成。下图“Director实践”展示了方法‘MakeReport’调用各个处理来生成PDF或EXCEL格式的报表。
图16:Director实践
创建者模式中第三部分组件就是产品(product),它在我们的例子中就仅仅是表示报表类。
图17:报表类
现在让我们来看一下创建者项目的完整视图。图“Client,builder,director以及product”展示了这些对象是如何构成创建者模式的。客户端(client)创建director类对象并将恰当的创建者(builder)传递给产品(product)来初始化。产品依赖于创建者被初始化或创建并最终传递给客户端。
图18:Client,builder,director以及product
输出的加过就像这样。我们可以看到两种类型的报表根据不同的构建者显示各自的报表头。
图19:创建者最终输出结果
你能否解释清楚原型模式?
原型模式也属于创建型模式。它提供一种方式来为已经存在的对象实例创建新的对象。一种方式是我们克隆已存在对象及其数据;通过这种方式,对克隆对象的任何修改都不影响原对象。如果你想仅仅通过设置原对象就可以修改克隆对象,那么就错了。想要通过将一个对象设置到另一个对象,我们需要传址到对象引用;因此修改新对象也会影响原对象。为了更清楚的理解传址(“BYREF”)请考虑下图“传址”。一下是代码流程:
第一步创建第一个对象,如class1的对象obj1
第二步创建第二个对象,如class1的对象ojb2
第三步设置旧的对象的值,如obj1设置为“old value”
第四步将obj1设置到obj2
第五步改变obj2的值
现在我们显示两个对象的值会发现两个对象的值都被改变
图20:传址
以上例子可得出结论一个对象设置为另一个对象为传地址。因此改变新对象值的同时也会改变原对象的值。
然而有很多实例我们希望复制对象值的改变不影响原对象。那么最佳选择就是原型模式。
下面我们看个c#的例子。下图“原型模式实践”中自定义了类‘ClsCustomer’需要克隆。在C#中可以使用‘MemberWiseClone’方法来实现。Java中则可以通过‘Clone’方法来达到目的。同样在代码中我们还展示了客户端代码,我们创建了自定义类的对象‘obj1’和‘obj2’。任何对obj2的改变都不会影响obj1,因为obj2是obj1的完全克隆复制。
图21:原型模式实践
你能否解释清楚原型模式中的浅拷贝和深拷贝?
原型模式有两种克隆方式。一种是浅拷贝,如以上例子中所示;在浅拷贝中只是拷贝对象,该对象的所包含的其他东西都不克隆;例如下图中“深克隆实践”我们自定义了一个类并且在该类中聚合了一个地址类。‘MemberWiseClone’则只会克隆自定义类‘ClsCustomer’而不会拷贝‘ClsAddress’类。因此我们也为地址类增加‘MemberWiseClone’功能。这样当调用‘getClone’功能时我们同时调用了父类和子类的克隆方法,这使得对象可以被完全拷贝。当父类对象和它们所包含的对象都被克隆时我们称之为深克隆;当只有父类对象被拷贝时我们称之为浅拷贝。
图22:深拷贝实践
你能否解释清楚单例模式?
重点:为一个对象创建唯一实例并通过一个统一的点来获取该唯一实例。
在项目中有这样一种场景,我们需要某个对象只创建一个实例并在客户端之间共享。例如,假设我们有连个类currency和country。
这些类加载主要数据并且会被在项目中一次又一次被引用,但是我们想只有一个共享实例来提升性能而不是一次又一次连接查询数据库。
实现单例模式有4个步骤:
步骤1:创建私有构造函数的封闭类
私有构造函数非常重要,因为客户端不能通过该类直接创建对象。本节中重点所示,这种模式的主要目的是为对象创建一个唯一的可以全局共享的实例,因此我们不希望给客户端直接创建该对象的权利。
步骤2:创建只需要一个对象类的对象(在这个例子中为currency和country)
步骤3:为该类创建一个静态只读对象并同样通过静态属性暴露出来,如下所示:
步骤4:现在可以通过以下代码在客户端使用该单例对象
以下是我们上边讨论的单例模式完整代码:
你能否解释清楚命令模式?
命令模式允许请求以对象的方式存在。下面我们来解释具体含义。下图“菜单及命令”中展示了点击不同菜单有不同操作的例子。因此,点击不同的菜单我们传递一个action中所包含的字符串;通过该action字符串我们执行不同的action。而以下代码中不好的一点是其中有很多if条件语句使得代码不够清晰。
图23:菜单及命令
命令模式将以上action移到对象中,由这些对象来执行真正的命令。
如之前所说,每个命令是一个对象,我们首先准备为每个命令准备各个类,如exit,open,file以及print等。以上所有action都被包装在类中如退出action被包装在‘clsExecuteExit’中,打开被包装在‘clsExecuteOpen’,print被包装在‘clsExecutePrint’中等等。说有这些类都继承自‘IExecute’接口。
图24:对象及命令
我们可以创建inovker来使用所有action类。invoker的主要工作就是封装对action的调用并持有所有action类。
这样我们要将所有actiont添加到一个集合中如ArrayList。同时我们还开放了一个方法‘getCommand’来接收一个字符串来返回抽象的‘IExecute’对象。到此,客户端代码就边的直接而灵活了;所有的if语句都被移动至‘clsInvoker’类。
图25:调用者及客户端代码
1. 本文由程序员学架构翻译
2. 本文译自
http://www.codeproject.com/Articles/28309/Design-pattern-FAQ-Part-Training
3. 转载请务必注明本文出自:程序员学架构(微信号:archleaner)