设计模式问答(2)(3)(4)合集

来源:互联网 发布:js img onload 编辑:程序博客网 时间:2024/06/04 19:34

介绍

这是设计模式问答1的连载。在这个系列,我们将覆盖到解释器、迭代器、调停者、备忘录和观察者模式。

如果你还没有阅读过我之前的系列,你可以随时从下面开始

  • 设计模式问答1:工厂模式,抽象工厂模式,构造者模式,原型模式,单例模式,命令模式
  • 设计模式问答3:状态模式,策略模式,访问者模式,适配器模式,享元模式
  • 设计模式问答4:桥接模式,组合模式,装饰者模式,外观模式,职责链模式(COR),代理模式,模板模式
  • UML问答1:UML部分1
  • UML问答2:UML部分2

什么是解释器模式?

解释器模式允许我们将语法解释为代码解决方案。好了,这意味着什么?语法会被映射到类,并形成解决方案。 举个例子,7 – 2能够被映射到“clsMinus”类。一句话,解释器模式为我们提供了一种解决方案,这个方案指导如何编写一个解析语法并执行代码的解释器。下面是一个 解释器的简单例子,它能够按照我们提供的日期格式语法,将日期解释为对应的代码,并输入正确的结果。

日期语法

让我们开始做图“日期语法”中所示的日期格式的解释器。在开始之前,我们要先理解一下解释器模式中不同的组件,然后再来处理映射。上下文部分包含数据,而逻辑部分包含将上下文中数据转换成可读格式的转换逻辑。

上下文和逻辑

让我们看一下日期格式的语法是怎么定义的。定义任何语法的第一步,是把语法分解成小的逻辑组件。图“语法映射与类 的映射”展示了怎么识别这些组件,以及怎么映射到处理这部分语法的逻辑类上面。我们已经把日期格式打断成了4个组件,分别是月、日、年和分隔符。对这4个 组件,我们将分别定义包含图中展示的逻辑的类。然后,我们将为日期格式的不同组件创建不同的类。

语法映射与类的映射

前 面说过,有两种类,一种是包含逻辑的表达式类,另一种是包含数据的上下文类,如图“表达式和上下文类”中所示。我们定义了不同类中的表达式解析算法,这些 类都从公共接口“ClsAbstractExpression”派生,并实现了“Evaluate”方法。“Evaluate”方法接收包含数据的上下文 类作为参数;它根据表达式逻辑来解析数据。“ClsYearExpression”实例将“YYYY”替换成年份值,而 “ClsMonthExpression”将“MM”替换成月份值,以此类推。

解释器类图

表达式和上下文类

现在,我们有了单独的表达式解析逻辑类,然后我们来看看客户端会如何使用这个逻辑。客户端首先把日期语法格式传递给 上下文类。依据日期格式,我们依次向集合中添加表达式实例。如果我们找到了“DD”,我们就添加一个“ClsDayExpression”实例;如果我们 找到了“MM”,就添加一个“ClsMonthExpression”实例,等等。最后,我们只需要遍历集合,并调用“Evaluate”函数。所有的 “Evaluate”函数执行完之后,我们就显示结果。

客户端解释器逻辑

你能解释迭代器模式吗?

迭代器模式允许在不暴露内部代码实现的情况下,顺序访问每个元素。让我们来理解一下。假设你有一个记录集合,你需要顺序遍历 每条记录,并且需要保持当前访问的位置,那么你需要的正是迭代器模式。这是最普通的设计模式,你会在不知不觉中用到它。在某些程度上,当你使用 “foreach”(它允许我们逐个元素地访问一个集合)时,你就已经在使用迭代器模式了。

迭代器业务逻辑

在图“迭代器业务逻辑”中,我们使用 “clsIterator”类来存放顾客类的集合。我们在“clsIterator”类内部,定义了一个数组,和一个名称为“FillObjects”的 方法,这个方法用来加载数组的内容。顾客集合数组是私有的,顾客的数据可以通过数组的下标来访问。因此我们定义了一组公有函数,包含 “getByIndex”(通过指定下表来访问),“Prev”(得到集合中前一个顾客数据),“Next”(得到集合中下一个顾客数 据),“getFirst”(得到集合中第一个顾客数据),“getLast”(得到集合中最后一个顾客数据)。

我们只对客户端暴露这些函数。这些函数小心的顺序遍历集合,并能够记忆当前遍历的索引。

下面的图“客户遍历逻辑”说明了该如何使用类“clsIterator”的实例“ObjIterator”,显示下一个、前一个、最后一个、第一个,以及通过索引显示顾客数据。

客户遍历逻辑

你能解释调停者模式吗?

大多数时候,项目中组件间的通讯都很复杂。因此组件间的逻辑关系也变得异常复杂。调停者模式帮助对象间用不互相关联的方式来通讯,从而使复杂度最小化。

调停者模式示例

让我们考虑图“调停者模式示例”,它描述了一个需要使用调停者模式的真实场景。它是一个非常用户友好的接口。它有3个典型的场景。

场景1:当用户在文本框中输入时,应该使添加和清除按钮可用。一旦当文本框中没有文字时,应该禁用添加和清除按钮。

场景1

场景2:当用户点击添加按钮时,文本框内的数据应该被输入到列表框内。一旦数据被输入到列表框,它应该清空文本框的内容,并禁用添加和清除按钮。

场景2

场景3:- 当用户点击清除按钮时,名字文本框内的内容被清空,并禁用添加和清除按钮。

场景3

现在从界面上来看上面几个场景,我们可以推断这些界面之间的交互是多么复杂。下图“组件间复杂交互”显示出了逻辑复杂性。

组件间复杂交互

好了,让我给你们看一个好看的图,即下图“通过调停者简化”。与其组件之间直接通讯,不如通过一个作为调停者的中心组件通讯,调停者组件管理着发送给其他组件的消息,这样逻辑更加优雅和清晰。

通过调停者简化

现在我们来看看代码会是什么样子。我们将使用C#,但是你可以很轻松的把这种思想应用在Java或其他语言上。下图“调停者类”展示了一个调停者类完整的代码概述。

调停者类做的第一件事,是保存拥有复杂通讯的类的引用。因此,我们 对外暴露了3个重载的方法“Register”。“Register”方法接收文本框对象和按钮对象为参数。交互场景集中在 “ClickAddButton”,“TextChange”和“ClickClearButton”三个方法上。将根据场景不同,这些方法将管理UI组 件的可用与禁用。

调停者类

现在的客户逻辑非常优雅、非常酷。在构造函数中,我们首先将 参与复杂通讯的所有组件注册到调停者对象中。然后在每个场景中,我们只需要调用调停者对象的函数。简单地说,当有文本变化时,我们调用调停者对象的 “TextChange”函数;当用户点击添加按钮时,我们调用“ClickAddButton”;当点击清除按钮时,调用 “ClickClearButton”函数。

调停者模式的客户逻辑

你能解释备忘录模式吗?

备忘录模式能够在不破坏封装原则的前提下,获取对象内部状态。备忘录模式帮助我们存储一个对象的快照,它可 以在任意时间被恢复。让我们通过实例来理解。考虑图“备忘录示例”,它展示了一个顾客的界面。假设用户开始编辑一条顾客记录,并做了一些修改。然后用户觉 得有错误,希望能够恢复到原始的数据。这时备忘录模式就登场了。它帮助我们存储数据的一个备份,并且当用户点击“取消”按钮时,对象能够恢复到它的原始状 态。

备忘录示例

让我们尝试用C#来实现刚才所讲的顾客界面。下图是顾客类 “clsCustomer”,它聚合了一个备忘录类“clsCustomerMemento”。备忘录类将保存数据的快照,它是顾客类 “clsCustomer”的精确的复制品(除了方法)。当顾客类“clsCustomer”初始化时,备忘录类也将被初始化。当顾客类数据变化时,备忘 录类的快照不变化。“Revert”函数把备忘录的数据写回到主类。

顾客类的备忘录

客户端的代码相当简单。我们创建一个顾客类。一旦遇到问题,我们点击“取消”按钮,调用“Revert”函数,将修改过的数据恢复到备忘录快照的原始数据。图“备忘录客户端代码”形象地展示了这个过程。

备忘录客户端代码

你能解释观察者模式吗?

观察者模式帮助我们与父类,关联类或者依赖类之间进行通讯。观察者模式中,有两个重要的概念,分别是“主体”和“观察者”。主体发 送通知,如果观察者已经注册到主体的话,观察者会收到通知。下图“主体和观察者”展示了应用程序(主体)是如何给所有观察者(邮件,事件日志,短消息服 务)发送通知的。你可以把这个例子对应到发布者与订阅者模型。发布者就是应用程序,而订阅者是邮件,事件日志和短消息服务。

主体和观察者

让我们尝试对前面定义的示例进行编码。 首先我们看一下订阅者/通知者类。图“订阅者类”做了一个直观地展示。对所有的订阅者,我们有一个公共的接口,“INotification”,它有一个 “notify”方法。所有需要接收通知的类,都需要实现这个“INotification”接口。所有需要接收通知的类,定义各自的响应方法。对当前场 景,我们只打印一个消息,表明特定的通知被执行了。

订阅者类

前面说过,观察者模式中,有两个部分,一个是我们前面说过的观察者/订阅者,另一个就是发布者,或者叫主体。

发布者有一个所有对接收通知感兴趣的订阅者的集合列表。通过“addNotification”和“removeNotification”,我们可以在列表中增加或者删除订阅者。“NotifyAll”方法遍历所有的订阅者,并发送通知。

发布者类、主体类

现在,我们已经有了发布者和订阅者类。我们来动手编写一下客户端代码。下面是观察者模式客户端的代码片段。首先我们创建一个拥有订阅者集合的通知者对象。然后我们向集合中添加需要被通知的订阅者。

现在,如果客户端输入的顾客代码超过10个字符,就需要通知所有的订阅者。

观察者模式客户端代码

如果你没有学习过设计模式,或者不愿完全阅读本文,请收看我们的免费视频设计模式培训和问答。

许可

本文,以及相关的代码和文件,通过 The Code Project Open License (CPOL) 协议授权。

原文链接: codeproject 翻译: ImportNew.com shenggordon
译文链接: http://www.importnew.com/14347.html

转载请保留原文出处、译者和译文链接。]


设计模式问答(3)

简介

这篇文章是设计模式问答系列(1)和(2)的延续。在这篇文章里,我们将会介绍状态模式,策略模式,访问者模式,适配器模式和享元模式。

如果你完全不了解设计模式或者你其实并不想通读这篇文章,你可以在这里看我们免费的视频 design pattern Training and interview questions / answers 。

如果你还没有读过我前边的系列,你可以通过下面的链接阅读:

  1. 设计模式问答(1):工程模式,抽象工程模式,构造器模式,原型模式,单例模式和命令模式
  2. 设计模式问答(2):解释器模式,迭代器模式,调停者模式,备忘录模式和观察者模式
  3. 设计模式问答(4):桥接模式、组合模式、外观模式、职责链模式、代理模式以及模板模式
  • UML 面试问题系列 1  UML Part 1
  • UML 面试问题系列 2  UML part 2

你能解释下状态模式吗?

状态模式允许一个对象根据对象的当前值改变自己的行为。参考下面的图片-“策略模式的例子”。这是一个开关操作的例子。如果灯泡的是关闭的状态,当你按下开关,灯泡会打开。如果灯泡是打开的状态,当你按下开关,灯泡将会关闭。简而言之,状态模式就是根据状态改变行为。

Figure:-策略模式的例子

现在让我们用C#来实现这个灯泡例子。图片“策略模式正在进行”同时显示了类和客户端的代码。我们创建一个叫‘clsState’的类,它包含一个enum类型其有‘on’和‘off’两种状态常量。我们定义了一个方法‘PressSwitch’,它会根据当前的状态切换自己的状态。在同一张图的右手边我们定义了一个客户端,它使用‘clsState’类并调用其‘PressSwitch()’方法。我们使用‘getStatus’函数在文本框中显示当前状态。

当我们点击‘Press Switch’按钮,灯泡将会从当前状态切换到相反的状态。

Figure: – 状态模式正在进行

你能解释下策略模式吗?

策略模式是一个类内置的算法集,可以根据使用的类交换算法。当你想在运行时决定使用的算法,这个模式会有用。

让我们看一个实际中策略模式如何工作的例子。以数学的计算为例,计算有相加和相减的策略。图片“策略模式正在进行”以形象的方式说明同样的情形。已知两个数,根据策略给出结果。所以如果是相加策略,两个数将会相加,如果是相减策略,将会得到相减的结果。这些策略只不过是算法。策略模式不过是对类内算法的封装而已。

Figure: – 策略模式正在进行

所以我们需要深入的第一件事就是如何封装类内的这些算法。下面的图片“封装算法”显示了‘add’算法如何封装在‘clsAddStatergy’类中,substract’算法如何封装在 ‘clsSubstractStatergy’类中。这两个类都继承自类‘clsStratergy’并重定义了‘calculate’方法。

Figure: – 封装算法

现在我们定义了一个叫做‘clsMaths’的类,它包含一个‘clsStatergy’的引用。这个类包含一个函数‘setStrategy’用于指定策略。

Figure: -策略类和包装类

下面的图片‘策略模式客户端代码’ 显示了如何使用包装类以及如何用‘setStatergy’方法在运行时设置策略

对象。

Figure: – 策略模式客户端代码

你能解释下访问者模式吗?

访问者模式允许我们不用改变实际的类就可以改变类的结构。它是分离当前数据结构和逻辑算法的一种方式。正因为如此,你可以不用改变类的结构就能向当前数据结构添加新逻辑。再一,你可以改变结构而不用触碰逻辑。

参考下面的图片“逻辑和数据结构”,其中有一个顾客(Customer)数据结构。每个顾客(Customer)对象包含多个地址(Address)对象,每个地址(address)对象又包含多个电话(Phones)对象。这个数据结构需要用两种不同的格式输出,一种是简单的字符串格式,另一种是XML格式。所以我们实现了两个类,一个是字符串逻辑类,另一个是XML逻辑类。这两个类遍历对象的结构,给出相应部分的输出。简言之访问者包含这些逻辑。

Figure: – 逻辑和数据结构

让我们根据上面顾客的例子,用C#实现相同的逻辑。如果你使用其它的编程语言,你也能够相应地映射到相同的逻辑。我们已经创建了两个访问者类,一个针对字符串逻辑进行解析,另一个针对XML逻辑。这两个类都有一个‘visit’方法来接收每个对象并进行解析。为了维持一致性,我们通过一个共同的接口‘IVisitor’来实现它们

Figure :- 访问者类

上面定义的访问者类会传给数据结构类,例如,顾客(Customer)类。在顾客(Customer)类,我们在‘accept’方法中传入访问者(visitor)类。在同一个方法中我们传入类类型并且调用其‘visit’方法。‘visit’方法是重载的,这样就可以根据传入的类类型来调用相应的‘visit’方法。

Figure: – 在数据结构类中传入的访问者

现在每个顾客(Customer)有多个地址(Address)对象,每个地址(Address)对象有多个电话(Phones)对象。所以,clsCustomer’类中包含一个objAddresses’列表对象,‘clsAddress’类中包含一个‘objPhones’列表对象。每个对象都有一个‘accept’方法接收访问者类,并把自身传入访问者类的‘visit’方法。因为访问者类的‘visit’方法是重载的,所以它会根据多态性调用正确的访问者方法。

现在我们有了访问者类中的逻辑和顾客(Customer)类中的数据结构,是时候在客户端使用它们了。下面的图片‘Visitor client code’显示了使用访问者模式的示例代码段。我们创建了访问者对象并把它传给顾客数据类。如果想以字符串的格式显示顾客对象的结构,我们就创建‘clsVisitorString’;如果想生成XML格式,就创建‘clsXML’对象并把它传给顾客对象的数据结构。你能够很容易的看出逻辑是如何与数据结构分离的。

Figure: – 访问者客户端代码

访问者模式和策略模式之间有什么区别?

访问者模式和策略模式看起来非常的相似因为它们都是处理来自数据的封装的复杂逻辑。可以说访问者模式是策略模式更通用的形式。

在策略模式中,我们只有一个上下文或者单个逻辑数据供多个算法操作。在前面的问题中,我们已经解释了策略模式和访问者模式的基础点。那就让我们用先前已经理解的例子进行理解。在策略模式中我们只有唯一一个上下文,并且多个算法在这个上下文中运行。下面的图片‘Strategy’向我们显示了多个算法是如何在这个上下文中运行。

Figure: – 访问者

简而言之,策略模式是一种特殊的访问者模式。在策略模式中我们只有一个数据上下文和多个算法,而在访问者模式中每个算法都关联一个数据上下文。选择策略模式还是访问者模式的基本准则是参考上下文和算法之间的关系。如果上下文和算法是一对多的关系,那么选择策略模式。如果上下和算法是多对多的关系,则选择访问者模式。

简而言之,策略模式是一种特殊的访问者模式。在策略模式中我们只有一个数据上下文和多个算法,而在访问者模式中每个算法都关联一个数据上下文。选择策略模式还是访问者模式的基本准则是参考上下文和算法之间的关系。如果上下文和算法是一对多的关系,那么选择策略模式。如果上下和算法是多对多的关系,则选择访问者模式。

你可以解释下适配器模式吗?

我们常常会碰到两个类因为接口不兼容而不兼容。适配器通过把已有的类重新封装成一个类从而使类之间能彼此兼容。参考下面的图片“不兼容的接口”,这两个类都是用于保存字符串值的集合。并且它们都有一个方法用于把字符串添加到集合。其中一个类的方法命名为‘Add’,另一个的方法命名为‘push’。一个类使用集合对象,而另一个则使用栈。我们想让栈对象可以和集合对象兼容。

Figure: – 不兼容的接口

有两种方法实现适配器模式,一种使用聚合(这种方式称为对象适配器模式),另一种使用继承(这种方式称为类适配器模式)。让我们先来介绍对象适配器模式。

图片‘对象适配器模式’比较宽泛的显示了如何实现这种模式。我们引入一个新的包装类‘clsCollectionAdapter’,它在‘clsStack’类上进行封装,在新的‘Add’方法里调用‘push’方法,从而使两个类兼容。

Figure: – 对象适配器模式

另一种实现适配器模式的方式是通过继承,也称为类适配器模式。图片‘类适配器模式’显示,通过让类‘clsCollectionAdapter’继承类‘clsStack’从而与类‘clsCollection’兼容。

Figure :- 类适配器模式

什么是享元模式(fly weight pattern)?

当我们需要创建许多对象并且这些对象共享一些相同的数据,享元模式非常有用。参考图片“对象和共同数据”。我们需要给一个机构里所有的员工打印名片。数据有两个部分,一部分是可变数据,例如:员工的姓名,另一部分是静态数据i,例如:地址。我们可以只维护一份静态数据的拷贝,让所有可变数据的对象引用这份拷贝,从而减少内存的使用。因此我们为可变数据创建了不同的拷贝,但是却引用了相同的静态数据拷贝。这样我们能优化内存的使用。

Figure: -“对象和共同数据”

下面C#示例代码显示了享元模式实际上是如何实现的。我们有两个类,‘clsVariableAddress’包含可变数据,第二个类‘clsAddress’包含静态数据。为了确保我们只有‘clsAddress’的一个实例,我们定义了一个包装类‘clsStatic’,并且创建了类‘clsAddress’的一个静态实例。这个对象聚合在类‘clsVariableAddress’里。

Figure: – 享元模式的类视图

从图片‘享元模式客户端代码’可以看到,我们创建了两个类‘clsVariableAddress’对象,但是它们内部的静态数据(例如,地址)却引用同一个实例。

Figure: – 享元模式客户端代码

如果你完全不熟悉设计模式或者你其实并不想读整篇文章,你可以看我们免费的视频 design pattern Training and interview questions / answers 。

关于作者: zdpg

设计模式问答(4)

这篇文章是设计模式问答(第1篇)、(第2篇)和(第3篇)的后续。在这篇文章中,我们将介绍桥接模式、组合模式、外观模式、职责链模式、代理模式以及模板模式。

如果你还没有阅读我之前的文章,请从下面开始:

  1. 设计模式问答(1):工程模式,抽象工程模式,构造器模式,原型模式,单例模式和命令模式
  2. 设计模式问答(2):解释器模式,迭代器模式,调停者模式,备忘录模式和观察者模式
  3. 设计模式问答(3):状态模式,策略模式,访问者模式,适配器模式和享元模式

你能解释桥接模式吗?

桥接模式能够将实现部分和抽象部分解耦。通过它,实现发生变化并不会影响到抽象,反之亦然。看看下图。开关是抽象部分,而电子设备是实现部分。开关可以连接到任何一个电子设备,因此开关是一个抽象的概念,而设备是实现部分。

1

图:抽象和实现

让我们尝试对这个开关和设备进行编码。第一部分,我们把实现和抽象分成两个不同的类。图“实现”展示了我们是如何实现接口“IEquipment”的“Start()”和“Stop()”方法的。我们实现了两个设备,一个是冰箱,另一个是电灯。

2

图:实现

第二部分是抽象。我们例子中的开关是抽象。它有一个“SetEquipment”方法,用来设置对象;“On”方法调用设备的“Start”方法,而“Off”调用“Stop”。

3

图:抽象

最终,我们看看客户端代码。你可以看到我们分别创建了实现对象和抽象对象。我们可以独立地使用它们。

4

图:桥接的客户端代码

你能解释组合模式吗?

GOF定义:一种简单和组合对象的树形数据结构

很多时候,对象以树形结构的方式组织,开发者必须理解叶子节点和分支节点的不同含义。这会使代码更加复杂,且容易导致错误。

如下例是一个简单对象的树形结构,其中customer是根对象,它有多个address对象,而每个address对象引用了多个phone对象。

5

图:通用程序

现在让我们假设你需要插入一个完整的对象树。示例代码会是下面所示的样子。代码遍历所有的customer,customer内的所有address,以及address内的所有phone。当循环执行时,会调用各自的更新方法,如下面代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
foreach (Customer objCust in objCustomers)
{
objCust.UpDateCustomer();
foreach (Address oAdd in objCust.Addresses)
{
oAdd.UpdateAddress();
foreach (Phone ophone in oAdd.Phones)
{
ophone.UpDatePhone();
}
}
}

上面代码的问题在于每个对象的更新方法是变化的。对customer是“UpdateCustomer”,对address是“UpdateAddress”,而对phone是“UpdatePhone”。换句话说,处理根对象和它包含的叶子节点的方式不同。这会导致疑惑,并使你的应用程序容易出错。

如果我们可以统一对待根对象和叶子节点,那么代码就可以更清晰和优雅。在下面的代码中,你可以看到我们创建了一个接口(IBusinessObject),它强制所有类(就是customer,address和phone)使用这个共同的接口。由于这个共同的接口,所有的对象现在都有一个名称为“Update”的方法:

1
2
3
4
5
6
7
8
9
10
11
12
foreach (IBusinessObject ICust in objCustomers)
{
ICust.Update();
foreach (IBusinessObject Iaddress in ((Customer)(ICust)).ChildObjects)
{
Iaddress.Update();
foreach (IBusinessObject iphone in ((Address)(Iaddress)).ChildObjects)
{
iphone.Update();
}
}
}

为了实现组合模式,我们首先创建一个接口,如下面代码所示:

1
2
3
4
5
6
publicinterface IBusinessObject
{
voidUpdate();
bool isValid();
voidAdd(object o);
}

强制所有的根对象/叶子节点实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
publicclass Customer : IBusinessObject
{
privateList<Address> _Addresses;
publicIEnumerable<Address> ChildObjects
{
get
{
return(IEnumerable<Address>)_Addresses;
}
}
publicvoid Add(object objAdd)
{
_Addresses.Add((Address) objAdd);
}
publicvoid Update()
{
}
publicbool isValid()
{
returntrue;
}
}

强制address对象也实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
publicclass Address : IBusinessObject
{
privateList<Phone> _Phones;
publicIEnumerable<Phone> ChildObjects
{
get
{
return(IEnumerable<Phone>)_Phones.ToList<object>();
}
}
publicvoid Add(object objPhone)
{
_Phones.Add((Phone)objPhone);
}
publicvoid Update()
{
}
publicbool isValid()
{
returntrue;
}
}

强制最后一个节点对象Phone实现接口。

1
2
3
4
5
6
7
8
9
10
11
publicclass Phone : IBusinessObject
{
publicvoid Update()
{}
publicbool isValid()
{returntrue;}
publicvoid Add(object o)
{
// no implementaton
}
}

你能解释装饰者模式吗?

定义:装饰者模式动态地顺序添加行为,帮助我们在运行状态下改变对象的行为。

我们有需要在运行时动态地顺序添加行为的情形。“顺序”是一个需要重点注意的词。例如,考虑下面饭店销售面包早餐的场景。他们有4款重要的产品,订单可以是下面的组合方式:

  • 单点面包
  • 面包、鸡肉
  • 面包、饮料
  • 面包、鸡肉、饮料

换句话说,根据组合方式,订单处理方式和订单成本会动态的发生变化。
6
图:订单组合方式

下面是一个只有面包的简单订单,它有两个函数“Prepare”和“CalculateCost”。我们会根据顾客的需要,动态地向这个基本面包订单上添加新的产品。

下面是每个订单都会拥有的一个简单接口,即Prepare和CalculateCost。

1
2
3
4
5
interfaceIOrder
{
string Prepare();
doubleCalculateCost();
}

面包是基本产品,它实现了IOrder接口。我们希望向面包订单添加新产品,并改变整个订单的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclass OrderBread : IOrder
{
publicstring Prepare()
{
string strPrepare=”";
strPrepare = “Bake the bread in ovenn”;
strPrepare = strPrepare + “Serve the bread”;
returnstrPrepare;
}
publicdouble CalculateCost()
{
return200.30;
}
}

我们可以使用装饰者模式动态地改变面包订单。实现装饰者模式需要5个步骤。

步骤1:创建一个聚合了我们需要动态地添加行为的对象/接口的装饰者类。

1
2
3
4
5
6
7
abstractclass OrderDecorator : IOrder
{
protectedIOrder Order;
. . . . .
. . . . .
. . . . .
}

装饰者类将包装这个对象,任何对主对象的方法调用,都会先经过被包装对象,然后才调用主对象的方法。

例如,当你调用Prepare方法时,装饰者类会先调用所有被包装类的Prepare方法,最后再调用自己的Prepare方法。你可以从图中看到装饰者是如何输出的:
7

图:装饰者输出结果

步骤2:被包装对象/接口需要被初始化。我们可以有很多种方法实现。在下面的例子中,我们将只暴露一个简单的构造函数,并传递对象给构造函数来初始化被包装对象。

1
2
3
4
5
6
7
8
9
abstractclass OrderDecorator : IOrder
{
protectedIOrder Order;
publicOrderDecorator(IOrder oOrder)
{
Order = oOrder;
}
. . . . .
}

步骤3:我们将实现IOrder接口,并通过虚方法调用包装类的方法。你可以看到我们创建了一些虚方法,它们会调用包装对象的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstractclass OrderDecorator : IOrder
{
protectedIOrder Order;
publicOrderDecorator(IOrder oOrder)
{
Order = oOrder;
}
publicvirtual string Prepare()
{
returnOrder.Prepare();
}
publicvirtual doubleCalculateCost()
{
returnOrder.CalculateCost();
}
}

步骤4:我们已经完成了最重要的步骤,就是创建装饰者。现在我们需要创建能够动态添加到装饰者中的动态行为对象。

下面是一个简单的鸡肉订单,它可以被添加到面包订单中,从而创建出一个不同的鸡肉+面包订单。鸡肉订单从订单装饰者类继承。

对这个对象的任何调用,都先执行鸡肉订单的自定义功能,然后再调用被包装对象的功能。例如,当调用Prepare函数时,它首先调用准备鸡肉的功能,然后执行被包装对象的准备功能。(译者:这里应该是作者说反了,先执行的是被包装类的方法)。

计算费用时,也是先添加鸡肉的费用,再计算被包装类的费用,并求和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
classOrderChicken : OrderDecorator
{
publicOrderChicken(IOrder oOrder) : base(oOrder)
{
}
publicoverride string Prepare()
{
returnbase.Prepare() + PrepareChicken();
}
privatestring PrepareChicken()
{
string strPrepare = “”;
strPrepare = “nGrill the chickenn”;
strPrepare = strPrepare + “Stuff in the bread”;
returnstrPrepare;
}
publicoverride doubleCalculateCost()
{
returnbase.CalculateCost() + 300.12;
}
}
Same way we can also prepare order drinks.
同样的方法,我们准备饮料的订单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
classOrderDrinks : OrderDecorator
{
publicOrderDrinks(IOrder oOrder)
: base(oOrder)
{
}
publicOrderDrinks()
{
}
publicoverride string Prepare()
{
returnbase.Prepare() + PrepareDrinks();
}
privatestring PrepareDrinks()
{
string strPrepare = “”;
strPrepare = “nTake the drink from freezern”;
strPrepare = strPrepare + “Serve in glass”;
returnstrPrepare;
}
publicoverride doubleCalculateCost()
{
returnbase.CalculateCost() + 10.12;
}
}

步骤5:最后一步是在行动上看看装饰者模式。你可以这么写客户端代码,来创建一个面包订单。

1
2
3
IOrder Order =newOrderBread();
Console.WriteLine(Order.Prepare());
Order.CalculateCost().ToString();

下面是上述代码的输出。

1
2
3
4
Order1:- Simple Bread menu
Bake the bread inoven
Serve the bread
200.3

如果你想创建一个包含鸡肉,饮料和面包的订单,就是类似下面的代码:

1
2
3
Order = newOrderDrinks(newOrderChicken(newOrderBread()));
Order.Prepare();
Order.CalculateCost().ToString();

组合了饮料+鸡肉+面包的订单输出是这样的:

1
2
3
4
5
6
7
8
Order2:- Drinks withchicken and bread
Bake the bread inoven
Serve the bread
Grill the chicken
Stuffinthe bread
Take the drink from freezer
Serveinglass
510.54

换句话说,你现在可以在运行时向主对象添加行为,并改变它的行为了。

下面是生成的不同订单组合,从而表明动态的改变了订单的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Order1:- Simple Bread menu
Bake the bread inoven
Serve the bread
200.3
Order2:- Drinks withchicken and bread
Bake the bread inoven
Serve the bread
Grill the chicken
Stuffinthe bread
Take the drink from freezer
Serveinglass
510.54
Order3:- Chicken withbread
Bake the bread inoven
Serve the bread
Grill the chicken
Stuffinthe bread
500.42
Order4:- drink withsimple bread
Bake the bread inoven
Serve the bread
Take the drink from frezer
Serveinglass
210.42

你能解释外观模式吗?

外观模式处于子系统集合的顶端,使他们以一种统一的方式通讯。

8
图:外观模式与子系统

图“订单外观”展示了这样的一个实现。为了发出一个订单,我们需要和产品,支付以及发票类交互。因此,订单成为统一了产品、支付和发票类的一个外观。

9
图:订单外观

图“外观模式”展示了类“clsOrder”如何统一/使用“clsProduct”,“clsPayment”以及“clsInvoice”来实现“PlaceOrder”功能。

10
图:外观模式

你能解释职责链模式吗(COR)?

当我们有一系列的逻辑处理器,来处理一系列的执行流程时,就需要使用职责链模式。让我们来理解一下它的意思。有些情况下,一条请求会被一系列的处理器处理。第一个处理器取出请求,它可能处理一部分,也可能不做处理。一旦处理结束,它把请求传递给链条中下一个处理器。一直持续下去,直到适当的处理器接收并完成整个处理流程。


图:职责链模式概念

让我们通过一个小的案例来理解这个概念。考虑图“简单案例”,我们有一些逻辑需要处理,需要经过3个处理流程。Process 1做一些处理,并传递给Process 2;Process 2做一些类似的处理后,传递给Process 3;最后完成整个处理流程。

12
图:简单案例

图“职责链模式类图”中,上述3个处理类都继承自同一个抽象父类。需要指出的一个重点是,每个处理流程都指向下一个将被调用的流程。在处理类中,我们聚合了另一个处理对象,叫做“objProcess”。对象“objProcess”指向下一个处理过程,它将在当前处理完成后被调用。

13
图:职责链模式类图

现在,我们已经定义了类,是时候在客户端调用这些类了。因此,我们为process1,process2和process3创建了所有的处理对象。通过“setProcess”方法,我们定义了处理对象的链表。你可以看到我们把process2链接到process1后面,把process3链接到process2后面。当这个链表建立完成后,我们运行处理流程。它按照链表的顺序依次执行每个处理流程。

14
图:职责链模式客户端代码

你能解释代理模式吗?

代理的本质是一个指向实际包含数据类的类,扮演一个接口的角色。这里的实际数据可能是一副很大的图像,或者是一个拥有大量数据、不易被复制的对象。因此你可以创建多个代理,指向这个包含大内存的对象,并对它施加操作。这样避免了对象的赋值,因此节省了内存。代理就是指向实际对象的引用。

图“代理和实际对象”展示了如何创建一个实际类所实现的接口。因此接口“IImageProxy”形成了代理,而类“clsActualImage”的实现就是实际对象。你可以在客户端代码中看到接口是如何指向实际对象的。

15
图:代理和实际对象

使用代理的优势是安全,避免大型对象的复制。通过传递代理而不是实际对象,从而避免了在客户端使用实际的代码。在客户端只使用代理,确保了更好的安全性。第二点是,当我们使用大型对象时,在网络或者其他领域移动这些对象会非常地耗内存。通过移动代理而不是大型对象,我们得到了更好的性能。

你能解释模板模式吗?

模板模式是一种行为模式。模板模式定义了一个主流程的模板,这个主流程模板包含子流程,以及子流程的执行顺序。然后,可以改变主流程的子流程,从而形成不同的行为。

定义:模板模式经常应用于在派生和特殊关系中,需要创建可扩展行为的场景。

例如,下面是一个格式化数据并写入到Oracle的一个简单流程。数据可能来源于多种源头,比如文件,SQL Server等。无论数据从哪里来,总体上的通用流程是,从数据源加载数据,解析数据,然后向Oracle写入数据。

16

图:通用流程

现在我们可以通过重载“Load”和“Parse”子流程的实现,来改变通用流程,创建出从CSV文件加载数据的流程,或者从SQL Server加载数据的流程。

17

图:模板流程思路

你可以从上图中看到,我们是如何修改“Load”和“Parse”子流程,以得到CSV文件和SQL Server加载流程。在派生的流程中,“Dump”函数和子流程的顺序不变化。

为了实现这个模板模式,我们需要做下面4个步骤:

  1. 通过创建抽象父类,创建模板或者说主流程。
  2. 定义抽象方法和函数,创建子流程。
  3. 创建一个定义子流程调用顺序的方法。这个方法应该是普通方法,因此子类不能重载它。
  4. 最后,创建子类,并重载抽象方法实现新的子流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
publicabstract class GeneralParser
{
protectedabstract void Load();
protectedabstract void Parse();
protectedvirtual voidDump()
{
Console.WriteLine(“Dump data in to oracle”);
}
publicvoid Process()
{
Load();
Parse();
Dump();
}
}
The ‘SqlServerParser’ inherits from ‘GeneralParser’ and overrides the ‘Load’ and ‘Parse’ with SQL server implementation.
“SqlServerParser”从“GeneralParser”继承,并重写了“Load”和“Parse”方法,提供了基于SQL server的实现。
publicclass SqlServerParser : GeneralParser
{
protectedoverride voidLoad()
{
Console.WriteLine(“Connect to SQL Server”);
}
protectedoverride voidParse()
{
Console.WriteLine(“Loop through the dataset”);
}
}

“FileParser”从“GeneralParser”继承,并重写了“Load”和“Parse”方法,提供了基于文件的实现。

1
2
3
4
5
6
7
8
9
10
11
publicclass FileParser : GeneralParser
{
protectedoverride voidLoad()
{
Console.WriteLine(“Load the data from the file”);
}
protectedoverride voidParse()
{
Console.WriteLine(“Parse the file data”);
}
}

在客户端,你可以这样同时调用两个parser。

1
2
3
4
5
6
FileParser ObjFileParser = newFileParser();
ObjFileParser.Process();
Console.WriteLine(“———————–”);
SqlServerParser ObjSqlParser = newSqlServerParser();
ObjSqlParser.Process();
Console.Read();

下面是两个parser的输出结果。

1
2
3
4
5
6
7
Load the data from the file
Parse the file data
Dump data in to oracle
———————–
Connect to SQL Server
Loop through the dataset
Dump data in to oracle

如果你是设计模式的新手,或者你不愿完整的阅读这篇文章,请看我们的免费视频设计模式培训和问答。

原文链接: codeproject 翻译: ImportNew.com shenggordon
译文链接: http://www.importnew.com/15921.html
转载请保留原文出处、译者和译文链接。]

0 0