C# 设计模式(全)
一、单例模式
这一章涵盖了单例模式。
GoF 定义
确保一个类只有一个实例,并提供对它的全局访问点。
概念
让我们假设您有一个名为A,
的类,您需要从它创建一个对象。一般情况下,你会怎么做?您可以简单地使用这一行代码:A obA=new A();
但是让我们仔细看看。如果你使用关键字new
十次以上,你将有十个以上的对象,对吗?但是在真实的场景中,不必要的对象创建是一个大问题(特别是当构造函数调用非常昂贵时),所以您需要限制它。在这种情况下,单例模式就派上了用场。它限制了new
的使用,并确保您没有一个以上的类实例。
简而言之,这种模式认为一个类应该只有一个实例。如果实例不可用,您可以创建一个;否则,您应该使用现有的实例来满足您的需求。通过遵循这种方法,您可以避免创建不必要的对象。
真实世界的例子
让我们假设你有一个参加比赛的运动队。您的团队需要在整个锦标赛中与多个对手比赛。在每场比赛开始时,按照比赛规则,两队队长必须掷硬币。如果你的球队没有队长,你需要选举一个人在比赛期间担任队长。在每场比赛和每一次掷硬币之前,如果你已经选举了队长,你就不能重复这个过程。
计算机世界的例子
在一些软件系统中,您可能决定只维护一个文件系统,以便可以使用它来集中管理资源。这种方法可以帮助您有效地实现缓存机制。考虑另一个例子。您还可以使用这种模式在多线程环境中维护线程池。
履行
单例模式可以通过多种方式实现。每种方法都有自己的优点和缺点。在下面的演示中,我将向您展示一种简单的方法。这里,这个类被命名为Singleton,
,它具有以下特征。在继续之前,您必须仔细阅读它们。
-
在这个例子中,我使用了一个私有的无参数构造函数。因此,您不能以正常的方式实例化该类型(使用
new
)。 -
这门课是密封的。(对于我们即将进行的演示,这不是必需的,但是如果您对这个 Singleton 类进行特定的修改,这可能是有益的。这个在问答环节讨论)。
-
既然
new
被阻塞了,怎么获取实例呢?在这种情况下,您可以选择实用方法或属性。在这个例子中,我选择了一个属性,在我的 Singleton 类中,您会看到下面的代码:public static Singleton GetInstance { get { return Instance; } }
-
如果您喜欢使用表达式体的只读属性(在 C# v6 中提供),您可以用下面的代码行替换该代码段:
-
我在 Singleton 类中使用了一个静态构造函数。静态构造函数必须是无参数的。按照微软的说法,在 C# 中,它初始化静态数据,并且只执行一次特定的操作。此外,在创建第一个实例或引用任何静态类成员之前,会自动调用静态构造函数。您可以放心地假设我已经充分利用了这些规范。
-
在
Main()
方法中,我使用一个简单的检查来确保我使用的是同一个且唯一可用的实例。 -
您会在 Singleton 类中看到以下代码行:
public static Singleton GetInstance => Instance;
private static readonly Singleton Instance;
公共静态成员确保了一个全局访问点。它确认实例化过程不会开始,直到您调用类的Instance
属性(换句话说,它支持惰性实例化),并且readonly
确保赋值过程只发生在静态构造函数中。一旦退出构造函数,就不能给readonly
字段赋值。如果您错误地反复尝试分配这个static readonly
字段,您将会遇到CS0198
编译时错误which says that a static readonly field cannot be assigned (except in a static constructor or a variable initializer)
。
- Singleton 类也用 sealed 关键字标记,以防止类的进一步派生(这样它的子类就不能误用它)。
Note
我保留了重要的注释,以帮助您更好地理解。我将对本书中的大多数程序做同样的事情;例如,当您从 Apress 网站下载代码时,您可以在注释行中看到表达式体的只读属性的用法。
类图
图 1-1 是说明单例模式的类图。
图 1-1
类图
解决方案资源管理器视图
图 1-2 显示了程序的高层结构。
图 1-2
解决方案资源管理器视图
演示 1
浏览下面的实现,并使用支持性的注释来帮助您更好地理解。
using System;
namespace SingletonPatternUsingStaticConstructor
{
public sealed class Singleton
{
#region Singleton implementation using static constructor
private static readonly Singleton Instance;
private static int TotalInstances;
/*
* Private constructor is used to prevent
* creation of instances with the 'new' keyword
* outside this class.
*/
private Singleton()
{
Console.WriteLine("--Private constructor is called.");
Console.WriteLine("--Exit now from private constructor.");
}
/*
* A static constructor is used for the following purposes:
* 1\. To initialize any static data
* 2\. To perform a specific action only once
*
* The static constructor will be called automatically before:
* i. You create the first instance; or
* ii.You refer to any static members in your code.
*
*/
// Here is the static constructor
static Singleton()
{
// Printing some messages before you create the instance
Console.WriteLine("-Static constructor is called.");
Instance = new Singleton();
TotalInstances++;
Console.WriteLine($"-Singleton instance is created.Number of instances:{ TotalInstances}");
Console.WriteLine("-Exit from static constructor.");
}
public static Singleton GetInstance
{
get
{
return Instance;
}
}
/*
* If you like to use expression-bodied read-only
* property, you can use the following line (C# v6.0 onwards).
*/
// public static Singleton GetInstance => Instance;
#endregion
/* The following line is used to discuss
the drawback of the approach. */
public static int MyInt = 25;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Singleton Pattern Demonstration.***\n");
/* The following line is used to discuss
the drawback of the approach. */
//Console.WriteLine($"The value of MyInt is :{Singleton.MyInt}");
// Private Constructor.So, you cannot use the 'new' keyword.
//Singleton s = new Singleton(); // error
Console.WriteLine("Trying to get a Singleton instance, called firstInstance.");
Singleton firstInstance = Singleton.GetInstance;
Console.WriteLine("Trying to get another Singleton instance, called secondInstance.");
Singleton secondInstance = Singleton.GetInstance;
if (firstInstance.Equals(secondInstance))
{
Console.WriteLine("The firstInstance and secondInstance are the same.");
}
else
{
Console.WriteLine("Different instances exist.");
}
Console.Read();
}
}
}
输出
下面是这个例子的输出。
***Singleton Pattern Demonstration.***
Trying to get a Singleton instance, called firstInstance.
-Static constructor is called.
--Private constructor is called.
--Exit now from private constructor.
-Singleton instance is created.Number of instances:1
-Exit from static constructor.
Trying to get another Singleton instance, called secondInstance.
The firstInstance and secondInstance are the same.
Note
Microsoft 建议静态字段使用 Pascal 命名约定。我在前面的演示中遵循了这一点。
分析
在这一节中,我将讨论与前面的演示相关的两个要点。首先,我向您展示了如何缩短代码长度,然后我讨论了我刚刚采用的方法的一个潜在缺点。我们开始吧。
从相关的注释中,您会发现如果您喜欢使用表达式体的只读属性,您可以替换下面的代码段
public static Singleton GetInstance
{
get
{
return Instance;
}
}
使用下面的代码行。
public static Singleton GetInstance => Instance;
保留现有代码,在Singleton
类中添加以下代码段。
/* The following line is used to discuss
the drawback of the approach.*/
public static int MyInt = 25;
添加之后,Singleton
类如下。
public sealed class Singleton
{
#region Singleton implementation using static constructor
// Keeping all existing code shown in the previous demonstration
#endregion
/* The following line is used to discuss
the drawback of the approach.*/
public static int MyInt = 25;
}
现在假设您使用下面的Main()
方法。
static void Main(string[] args)
{
Console.WriteLine("***Singleton Pattern Demonstration.***\n");
Console.WriteLine($"The value of MyInt is :{Singleton.MyInt}");
Console.Read();
}
如果您现在执行该程序,您会看到以下输出。
***Singleton Pattern Demonstration.***
-Static constructor is called.
--Private constructor is called.
--Exit now from private constructor.
-Singleton instance is created.Number of instances:1
-Exit from static constructor.
The value of MyInt is :25
虽然您应该只看到输出的最后一行,但是您得到了Singleton
类的所有实例化细节,这说明了这种方法的缺点。具体来说,在Main()
方法中,您试图使用MyInt
静态变量,但是您的应用仍然创建了 Singleton 类的一个实例。因此,当您使用这种方法时,您对实例化过程的控制较少。
然而,除了这个问题之外,没有与之相关的显著缺点。您只需承认这是一次性活动,初始化过程不会重复。如果你能容忍这个缺点,你就可以宣称你已经实现了一个简单、漂亮的单例模式。在这里我要重复的是,每种方法都有自己的优点和缺点;没有一种方法是 100%完美的。根据您的需求,您可能会选择其中一个。
接下来,我将介绍这种实现的另一种常见变体。我可以直接使用下面的代码行
private static readonly Singleton Instance = new Singleton();
并避免使用静态构造函数在控制台中打印特殊消息。下面的代码段也演示了单例模式。
public sealed class Singleton
{
#region Using static initialization
private static readonly Singleton Instance = new Singleton();
private static int TotalInstances;
/*
* Private constructor is used to prevent
* creation of instances with 'new' keyword
* outside this class.
*/
private Singleton()
{
Console.WriteLine("--Private constructor is called.");
Console.WriteLine("--Exit now from private constructor.");
}
public static Singleton GetInstance
{
get
{
return Instance;
}
}
#endregion
}
这种编码通常被称为静态初始化。我想在控制台中打印定制消息,所以我的首选方法如演示 1 所示。
问答环节
你为什么要把事情复杂化?你可以简单地编写你的 单例类 如下。
public class Singleton
{
private static Singleton instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
是的,这种方法可以在单线程环境中工作,但是考虑一个多线程环境,其中两个(或更多)线程可能试图同时评估下面的代码。
if (instance == null)
如果实例尚未创建,每个线程将尝试创建一个新的实例。因此,您可能会得到该类的多个实例。
你能展示一种替代的方法来建模 单例设计模式吗?
有许多方法。每一种都有利弊。
以下代码显示了双重检查锁定。下面的代码段概述了这种方法。
// Singleton implementation using double checked locking.
public sealed class Singleton
{
/*
* We are using volatile to ensure
* that assignment to the instance variable finishes
* before it's accessed.
*/
private static volatile Singleton Instance;
private static object lockObject = new Object();
private Singleton() { }
public static Singleton GetInstance
{
get
{
// First Check
if (Instance == null)
{
lock (lockObject)
{
// Second(Double) Check
if (Instance == null)
Instance = new Singleton();
}
}
return Instance;
}
}
}
这种方法可以帮助您在需要时创建实例。但你必须记住,一般来说,锁定机制是昂贵的。
除了使用双锁,您还可以使用单锁,如下所示。
//Singleton implementation using single lock
public sealed class Singleton
{
/*
* We are using volatile to ensure
* that assignment to the instance variable finishes
* before it's access.
*/
private static volatile Singleton Instance;
private static object lockObject = new Object();
private Singleton() { }
public static Singleton GetInstance
{
get
{
// Locking it first
lock (lockObject)
{
// Single check
if (Instance == null)
{
Instance = new Singleton();
}
}
return Instance;
}
}
}
尽管这种方法看起来更简单,但它并不被认为是更好的方法,因为每次请求Singleton
实例的一个实例时,您都要获取锁,这会降低应用的性能。
在本章的最后,你会看到另一种使用 C# 内置结构实现单例模式的方法。
Note
当您保持客户端代码不变时,您可以使用您喜欢的方法简单地替换 Singleton 类。我提供了这方面的完整演示,您可以从 Apress 的网站下载。
1.3 为什么在 双重检查锁定 示例中将实例标记为 volatile?
许多开发商认为这是不必要的。NET 2.0 及以上,但有争论。为了简单起见,让我们看看 C# 规范是怎么表述的:“volatile 关键字表示一个字段可能会被同时执行的多个线程修改。出于性能原因,编译器、运行时系统甚至硬件可能会重新安排对内存位置的读写。声明为 volatile 的字段不受这些优化的影响。添加 volatile 修饰符可确保所有线程都将按照执行顺序观察任何其他线程执行的易失性写入。这仅仅意味着volatile
关键字有助于提供一种序列化的访问机制,因此所有线程都可以按照它们的执行顺序观察到任何其他线程的变化。它确保最新的值总是出现在字段中。因此,使用 volatile 修饰符使 s 你的代码更加安全。
在这个上下文中,y ou 应该记住volatile
关键字不能应用于所有类型,并且有一定的限制。例如,您可以将它应用于类或结构字段,但不能应用于局部变量。
1.4 为什么多重 物体创作 是一个大问题?
这里有两点需要记住。
-
如果您正在处理资源密集型对象,则对象创建的成本会很高。
-
在某些应用中,您可能需要将一个公共对象传递到多个位置。
1.5 什么时候应该使用单例模式?
看情况。这里有一些这种模式有用的常见用例。
-
当使用集中式系统(例如数据库)时
-
维护公共日志文件时
-
当在多线程环境中维护线程池时
-
当实现缓存机制或设备驱动程序时,等等
1.6 为什么使用 sealed
关键字 ?Singleton 类有一个私有构造函数,足以停止派生过程。
接得好。这不是强制性的,但最好清楚地表明你的意图。我用它来保护一种特殊的情况:当你试图使用一个派生的嵌套类,并且你喜欢在私有构造函数内部初始化。为了更好地理解这一点,我们假设您有下面这个类,它不是密封的。在这个类中,不使用静态构造函数;相反,您使用私有构造函数来跟踪实例的数量。我用粗体显示了关键的变化。
public class Singleton
{
private static readonly Singleton Instance = new Singleton();
private static int TotalInstances;
/*
* Private constructor is used to prevent
* creation of instances with 'new' keyword
* outside this class.
*/
private Singleton()
{
Console.WriteLine("--Private constructor is called.");
TotalInstances++;
Console.WriteLine($"-Singleton instance is created. Number of instances:{ TotalInstances}");
Console.WriteLine("--Exit now from private constructor.");
}
public static Singleton GetInstance
{
get
{
return Instance;
}
}
// The keyword "sealed" can guard this scenario.
// public class NestedDerived : Singleton { }
}
在Main() method
,
中,让我们对控制台消息的第一行做一点小小的修改,以区别于原始的输出,但让我们保持其余部分不变。它现在看起来如下。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Singleton Pattern Q&A***\n");
Console.WriteLine("Trying to get a Singleton instance, called firstInstance.");
Singleton firstInstance = Singleton.GetInstance;
Console.WriteLine("Trying to get another Singleton instance, called secondInstance.");
Singleton secondInstance = Singleton.GetInstance;
if (firstInstance.Equals(secondInstance))
{
Console.WriteLine("The firstInstance and secondInstance are same.");
}
else
{
Console.WriteLine("Different instances exist.");
}
//Singleton.NestedDerived nestedClassObject1 = new Singleton.NestedDerived();
//Singleton.NestedDerived nestedClassObject2 = new Singleton.NestedDerived();
Console.Read();
}
}
如果您运行该程序,您将得到以下输出。
***Singleton Pattern Q&A***
Trying to get a Singleton instance, called firstInstance
。
--Private constructor is called.
-Singleton instance is created. Number of instances:1
--Exit now from private constructor.
Trying to get another Singleton instance, called secondInstance
。
The firstInstance and
??。这很简单,类似于我们最初演示的输出。
现在取消 Singleton 类中下面一行的注释。
//public class NestedDerived : Singleton { }
然后在Main()
方法中取消下面两行代码的注释。
//Singleton.NestedDerived nestedClassObject1 = new Singleton.NestedDerived();
//Singleton.NestedDerived nestedClassObject2 = new Singleton.NestedDerived();
再次运行应用。这一次,您将获得以下输出。
***Singleton Pattern Q&A***
Trying to get a Singleton instance, called firstInstance.
--Private constructor is called.
-Singleton instance is created.Number of instances:1
--Exit now from private constructor.
Trying to get another Singleton instance, called secondInstance.
The firstInstance and secondInstance are same.
--Private constructor is called.
-Singleton instance is created.Number of instances:2
--Exit now from private constructor.
--Private constructor is called.
-Singleton instance is created.Number of instances:3
--Exit now from private constructor.
您是否注意到实例的总数正在增加?虽然在我最初的演示中,我可以排除使用sealed
,但我保留了它来防范这种情况,这种情况可能是由于修改了 Singleton 类的原始实现而出现的。
替代实现
现在我将向您展示另一种使用 C# 内置结构的方法。在本书的前一版本中,我跳过了这一点,因为要理解这段代码,您需要熟悉泛型、委托和 lambda 表达式。如果您不熟悉委托,可以暂时跳过这一部分;否则,我们继续。
在这个例子中,我将向您展示有效使用代码的三种不同方式(使用自定义委托、使用内置Func
委托,以及最后使用 lambda 表达式)。让我们看看带有相关注释的 Singleton 类的核心代码段,然后进行分析。
// Singleton implementation using Lazy<T>
public sealed class Singleton
{
// Custom delegate
delegate Singleton SingletonDelegateWithNoParameter();
static SingletonDelegateWithNoParameter myDel = MakeSingletonInstance;
// Using built-in Func<out TResult> delegate
static Func<Singleton> myFuncDelegate= MakeSingletonInstance;
private static readonly Lazy<Singleton> Instance = new Lazy<Singleton>(
//myDel() // Also ok. Using a custom delegate
myFuncDelegate()
//() => new Singleton() // Using lambda expression
);
private static Singleton MakeSingletonInstance()
{
return new Singleton();
}
private Singleton() { }
public static Singleton GetInstance
{
get
{
return Instance.Value;
}
}
}
分析
这段代码最重要的部分是
private static readonly Lazy<Singleton> Instance = new Lazy<Singleton>(
//myDel() // Also ok. Using a custom delegate
myFuncDelegate()
//() => new Singleton() // Using lambda expression
);
这里myDel()
被注释掉;当您使用自定义委托时,可以使用它。在使用内置的Func
委托的地方myFuncDelegate()
已经被执行。如果您想使用 lambda 表达式而不是委托,可以使用最后一行注释。简而言之,当您尝试这些方法中的任何一种时,其他两行应该被注释掉。
如果将鼠标悬停在Lazy<Singleton>
上,会看到Lazy<T>
支持惰性初始化;在撰写本文时,它有七个重载版本的构造函数,其中一些可以接受一个Func
委托实例作为方法参数。现在你知道我为什么在这个例子中使用了Func
委托了。图 1-3 是 Visual Studio 截图。
图 1-3
懒惰
在这个例子中,我使用了下面的版本。
public Lazy(Func<T> valueFactory);
虽然Func
委托有很多重载版本,但是在这种情况下,你只能使用下面的版本。
public delegate TResult Func<[NullableAttribute(2)] out TResult>();
这个Func
版本可以指向一个不接受任何参数但返回一个由TResult
参数指定的类型的值的方法,这就是为什么它可以正确地指向下面的方法。
private static Singleton MakeSingletonInstance()
{
return new Singleton();
}
如果您想使用自己的委托,您可以这样做。以下代码段可用于此目的。
// Custom delegate
delegate Singleton SingletonDelegateWithNoParameter();
static SingletonDelegateWithNoParameter myDel = MakeSingletonInstance;
在这种情况下,你需要使用myDel()
而不是myFuncDelegate()
。
最后,如果选择 lambda 表达式,就不需要MakeSingletonInstance()
方法,可以直接使用下面这段代码。
private static readonly Lazy<Singleton> Instance = new Lazy<Singleton>(
() => new Singleton() // Using lambda expression
);
Note
在所有实现单例模式的方法中,Main()
方法本质上是相同的。因此,为了简洁起见,我没有在讨论中包括这一点。
问答环节
1.7 你用了术语。这是什么意思?
**这是一种用来延迟对象创建过程的技术。基本思想是,只有在真正需要时,才应该创建对象。当创建对象是一项开销很大的操作时,此方法很有用。
希望您对单例设计模式有更好的理解。在这种模式中,性能与懒惰总是一个问题,一些开发人员总是质疑这些方面。但事实是,这种模式以各种形式出现在许多应用中。让我们引用 Erich Gamma(瑞士计算机科学家和 GoF 作者之一)在 2009 年的一次采访来结束这一章:“当讨论放弃哪些模式时,我们发现我们仍然热爱它们。不尽然——我赞成放弃辛格尔顿。它的用途几乎总是一种设计气味。”有兴趣看本次面试详情的可以关注链接:https://www.informit.com/articles/article.aspx?p=1404056
。**
二、原型模式
本章涵盖了原型模式。
GoF 定义
使用原型实例指定要创建的对象种类,并通过复制该原型来创建新对象。
概念
原型模式提供了另一种方法,通过复制或克隆现有对象的实例来实例化新对象。使用这个概念可以避免创建新实例的开销。如果你观察模式的意图(GoF 定义),你会发现这个模式的核心思想是创建一个基于另一个对象的对象。这个现有对象充当新对象的模板。
当你为这种模式编写代码时,一般来说,你会看到有一个抽象类或接口扮演着抽象原型的角色。这个抽象原型包含一个由具体原型实现的克隆方法。客户可以通过要求原型克隆自己来创建一个新对象。在本章的下一个程序(演示 1)中,我遵循同样的方法。
真实世界的例子
假设你有一份有价值文件的主拷贝。您需要对其进行一些更改,以分析更改的效果。在这种情况下,您可以复印原始文档,并在复印的文档中编辑更改。
计算机世界的例子
让我们假设您已经有了一个稳定的应用。将来,您可能希望对应用进行一些小的修改。您必须从原始应用的副本开始,进行更改,然后进一步分析它。你不想仅仅为了改变而从头开始;这会耗费你的时间和金钱。
英寸 NET 中,ICloneable
接口包含一个Clone()
方法。在 Visual Studio IDE 中,您可以很容易地找到以下详细信息。
namespace System
{
//
// Summary:
// Supports cloning, which creates a new instance of a class with // the same value
as an existing instance.
[NullableContextAttribute(1)]
public interface ICloneable
{
//
// Summary:
// Creates a new object that is a copy of the current instance.
//
// Returns:
// A new object that is a copy of this instance.
object Clone();
}
}
您可以在实现原型模式时使用这个内置的构造,但是在这个例子中,我使用了自己的Clone()
方法。
履行
在这个例子中,我遵循图 2-1 所示的结构。
图 2-1
原型示例
这里BasicCar
是原型。它是一个抽象类,有一个名为Clone()
的抽象方法。Nano
和Ford
是具体的类(即具体的原型),它们继承自BasicCar
。两个具体的类都实现了Clone()
方法。在这个例子中,最初,我用默认价格创建了一个BasicCar
对象。后来,我修改了每个型号的价格。Program.cs
是实现中的客户端。
在BasicCar
类内部,有一个名为SetAdditionalPrice()
的方法。它生成一个介于 200,000(含)和 500,000(不含)之间的随机值。在我计算汽车的最终onRoad
价格之前,这个值被加到基础价格中。在这个例子中,我用印度货币(卢比)提到了这些汽车的价格。
汽车模型的基本价格是由具体原型的建造者设定的。因此,您会看到如下代码段,其中具体的原型(Nano)初始化基本价格。同样,这个类也覆盖了BasicCar
中的Clone()
方法。
public class Nano : BasicCar
{
public Nano(string m)
{
ModelName = m;
// Setting a basic price for Nano.
basePrice = 100000;
}
public override BasicCar Clone()
{
// Creating a shallow copy and returning it.
return this.MemberwiseClone() as Nano;
}
}
Ford
,另一个混凝土原型,也有类似的结构。在这个例子中,我使用了两个具体的原型(Ford
和Nano
)。为了更好地理解原型模式,一个具体的原型就足够了。因此,如果您愿意,您可以简单地删除这些具体的原型来减少代码大小。
最后也是最重要的,在接下来的例子中您会看到MemberwiseClone()
方法。它在Object
类中定义,有如下描述。
// Summary:
// Creates a shallow copy of the current System.Object.
//
// Returns:
// A shallow copy of the current System.Object.
[NullableContextAttribute(1)]
protected Object MemberwiseClone();
Note
你可能对术语浅薄感到疑惑。实际上,克隆有两种类型:浅层克隆和深层克隆。这一章包括一个讨论和一个完整的程序来帮助你理解他们的区别。现在,您只需要知道在浅层复制中,类的简单类型字段被复制到克隆的实例中;但是对于引用类型字段,只复制引用。因此,在这种类型的克隆中,原始实例和克隆实例都指向同一个引用,这在某些情况下可能会导致问题。为了克服这一点,您可能需要使用深层拷贝。
类图
图 2-2 显示了类图。
图 2-2
类图
解决方案资源管理器视图
图 2-3 显示了程序各部分的高层结构。
图 2-3
解决方案资源管理器视图
演示 1
下面是实现。
// BasicCar.cs
using System;
namespace PrototypePattern
{
public abstract class BasicCar
{
public int basePrice = 0, onRoadPrice=0;
public string ModelName { get; set; }
/*
We'll add this price before
the final calculation of onRoadPrice.
*/
public static int SetAdditionalPrice()
{
Random random = new Random();
int additionalPrice = random.Next(200000, 500000);
return additionalPrice;
}
public abstract BasicCar Clone();
}
}
// Nano.cs
namespace PrototypePattern
{
public class Nano : BasicCar
{
public Nano(string m)
{
ModelName = m;
// Setting a base price for Nano.
basePrice = 100000;
}
public override BasicCar Clone()
{
// Creating a shallow copy and returning it.
return this.MemberwiseClone() as Nano;
}
}
}
// Ford.cs
namespace PrototypePattern
{
public class Ford : BasicCar
{
public Ford(string m)
{
ModelName = m;
// Setting a basic price for Ford.
basePrice = 500000;
}
public override BasicCar Clone()
{
// Creating a shallow copy and returning it.
return this.MemberwiseClone() as Ford;
}
}
}
// Client
using System;
namespace PrototypePattern
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Prototype Pattern Demo***\n");
// Base or Original Copy
BasicCar nano = new Nano("Green Nano");
BasicCar ford = new Ford("Ford Yellow");
BasicCar basicCar;
// Nano
basicCar = nano.Clone();
// Working on cloned copy
basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");
// Ford
basicCar = ford.Clone();
// Working on cloned copy
basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");
Console.ReadLine();
}
}
}
输出
下面是一个可能的输出。
***Prototype Pattern Demo***
Car is: Green Nano, and it's price is Rs. 368104
Car is: Ford Yellow, and it's price is Rs. 878072
Note
您可能会在系统中看到不同的价格,因为我在BasicCar
类的SetAdditionalPrice()
方法中生成了一个随机价格。但是我保证了Ford
的价格大于Nano
。
修改的实现
在演示 1 中,在制作克隆之前,客户端按如下方式实例化对象。
BasicCar nano = new Nano("Green Nano");
BasicCar ford = new Ford("Ford Yellow");
这很好,但是在原型模式的一些例子中,您可能会注意到一个额外的参与者创建原型并将它们提供给客户。专家通常喜欢这种方法,因为它向客户端隐藏了创建新实例的复杂性。让我们在演示 2 中看看如何实现这一点。
类图
图 2-4 显示了修改后的类图中的关键变化。
图 2-4
演示 2 的类图中的主要变化
演示 2
为了演示这一点,我在前面的演示中添加了下面这个名为CarFactory
的类。
class CarFactory
{
private readonly BasicCar nano, ford;
public CarFactory()
{
nano = new Nano("Green Nano");
ford = new Ford("Ford Yellow");
}
public BasicCar GetNano()
{
return nano.Clone();
}
public BasicCar GetFord()
{
return ford.Clone();
}
}
使用这个类,您的客户端代码可以修改如下。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Prototype Pattern Demo2.***\n");
CarFactory carFactory = new CarFactory();
// Get a Nano
BasicCar basicCar = carFactory.GetNano();
//Working on cloned copy
basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");
// Get a Ford now
basicCar = carFactory.GetFord();
// Working on cloned copy
basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");
Console.ReadLine();
}
}
输出
下面是一个可能的输出。
***Prototype Pattern Demo2.***
Car is: Green Nano, and it's price is Rs. 546365
Car is: Ford Yellow, and it's price is Rs. 828518
分析
这个输出就和之前的输出一样,没有什么魔力。类满足了我们的需求,但是它有一个潜在的缺点。我在CarFactory
的构造函数中初始化了汽车。因此,在初始化该类时,它总是创建这两种汽车类型的实例。因此,如果您想实现一个惰性初始化,您可以修改CarFactory
类中的GetNano()
方法,如下所示。
public BasicCar GetNano()
{
if (nano!=null)
{
// Nano was created earlier.
// Returning a clone of it.
return nano.Clone();
}
else
{
/*
Create a nano for the first
time and return it.
*/
nano = new Nano("Green Nano");
return nano;
}
}
你可以用同样的方法修改GetFord()
方法。
Note
当您实现这些更改时,不要忘记移除只读修饰符以避免编译时错误。
下面是修改后的类。
class CarFactory
{
private BasicCar nano,ford;
public BasicCar GetNano()
{
if (nano!=null)
{
// Nano was created earlier.
// Returning a clone of it.
return nano.Clone();
}
else
{
/*
Create a nano for the first
time and return it.
*/
nano = new Nano("Green Nano");
return nano;
}
}
public BasicCar GetFord()
{
if (ford != null)
{
// Ford was created earlier.
// Returning a clone of it.
return ford.Clone();
}
else
{
/*
Create a nano for the first
time and return it.
*/
ford = new Ford("Ford Yellow");
return ford;
}
}
}
最后,这不是最终的修改。在第一章中,你了解到在多线程环境中,当你检查 if 条件时,可能会产生额外的对象。由于你在第一章中学习了可能的解决方案,所以我不会在这次讨论或接下来的讨论中关注它们。我相信您现在应该对这种模式的意图有了清晰的认识。
问答环节
2.1 使用原型设计模式的 优势 有哪些?
以下是一些重要的用法。
-
您不希望修改现有对象并在其上进行实验。
-
您可以在运行时包含或丢弃产品。
-
在某些情况下,您可以以更低的成本创建新的实例。
-
您可以专注于关键活动,而不是复杂的实例创建过程。例如,一旦您忽略了复杂的对象创建过程,您就可以简单地从克隆或复制对象开始,并实现其余部分。
-
您希望在完全实现新对象之前,先感受一下它的行为。
2.2 与使用原型设计模式相关的 挑战 有哪些?
以下是一些挑战。
-
每个子类都需要实现克隆或复制机制。
-
如果所考虑的对象不支持复制或者存在循环引用,那么实现克隆机制可能会很有挑战性。
在这个例子中,我使用了MemberwiseClone()
成员方法,它提供了一个浅层拷贝。这是一个非常简单的技术,可以满足你的基本需求。但是,如果您需要为一个复杂的对象提供深度复制实现,这可能会很昂贵,因为您不仅需要复制对象,还需要处理所有的引用,这可能会形成一个非常复杂的图。
2.3 能否详细说明一下 C# 中浅拷贝和深拷贝的区别?
下一节解释了它们的区别。
浅层拷贝与深层拷贝
浅层复制创建一个新对象,然后将非静态字段从原始对象复制到新对象。如果原始对象中存在值类型字段,则执行逐位复制。但是如果该字段是引用类型,则该方法复制引用,而不是实际的对象。让我们试着用一个简单的图表来理解这个机制(见图 2-5 )。假设您有一个对象X1
,它有一个对另一个对象Y1
的引用。此外,假设对象Y1
具有对对象Z1
的引用。
图 2-5
在引用的浅拷贝之前
通过对X1
的浅层复制,一个新的对象(比如说X2
)被创建,它也引用了Y1
(见图 2-6 )。
图 2-6
在引用的浅拷贝之后
我在实现中使用了MemberwiseClone()
。它执行浅层复制。
对于X1
的深层副本,创建一个新对象(比如说,X3
),并且X3
具有对新对象Y3
的引用,该新对象是Y1
的副本。此外,Y3
又引用了另一个新对象Z3
,它是Z1
的副本(见图 2-7 )。
图 2-7
在引用的深层副本之后
现在考虑下面的演示,以便更好地理解。
演示 3
这个简单的演示向您展示了浅层拷贝和深层拷贝之间的区别。它还向您展示了为什么深层副本在某些情况下很重要。以下是该计划的主要特点。
-
有两类:
Employee
和EmpAddress
。 -
EmpAddress
只有一个读写属性,叫做Address
。它设置一个雇员的地址,但是Employee
类有三个读写属性:Id, Name,
和EmpAddress.
-
要形成一个
Employee
对象,需要传递一个 ID 和员工的名字,同时还需要传递地址。因此,您会看到如下代码段。EmpAddress initialAddress = new EmpAddress("21, abc Road, USA"); Employee emp = new Employee(1, "John", initialAddress);
-
在客户端代码中,首先创建一个
Employee
对象(emp
),然后通过克隆创建另一个对象empClone
。您会看到下面几行代码。Console.WriteLine("Making a clone of emp1 now."); Employee empClone = (Employee)emp.Clone();
-
稍后,您更改
empClone
中的值。
当使用浅层拷贝时,这种变化的副作用是emp
对象的地址也发生了变化,这是不希望的。(原型模式很简单;在处理对象的克隆副本时,不应更改原始对象)。
在下面的示例中,深层副本的代码最初是注释的,因此您只能看到浅层副本的效果。
现在来看一下演示。
using System;
namespace ShallowVsDeepCopy
{
class EmpAddress
{
public string Address { get; set; }
public EmpAddress(string address)
{
this.Address = address;
}
public override string ToString()
{
return this.Address;
}
public object CloneAddress()
{
// Shallow Copy
return this.MemberwiseClone();
}
}
class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public EmpAddress EmpAddress { get; set; }
public Employee(int id, string name, EmpAddress empAddress)
{
this.Id = id;
this.Name = name;
this.EmpAddress = empAddress;
}
public override string ToString()
{
return string.Format("Employee Id is : {0},Employee Name is : {1}, Employee Address is : {2}", this.Id,this.Name,this.EmpAddress);
}
public object Clone()
{
// Shallow Copy
return this.MemberwiseClone();
#region For deep copy
//Employee employee = (Employee)this.MemberwiseClone();
//employee.EmpAddress = (EmpAddress)this.EmpAddress.//CloneAddress();
/*
* NOTE:
* Error: MemberwiseClone() is protected, you cannot access it via a qualifier of type EmpAddress. The qualifier must be Employee or its derived type.
*/
//employee.EmpAddress = (EmpAddress)this.EmpAddress.MemberwiseClone(); // error
// return employee;
#endregion
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Shallow vs Deep Copy Demo.***\n");
EmpAddress initialAddress = new EmpAddress("21, abc Road, USA");
Employee emp = new Employee(1, "John", initialAddress);
Console.WriteLine("The original object is emp1 which is as follows:");
Console.WriteLine(emp);
Console.WriteLine("Making a clone of emp1 now.");
Employee empClone = (Employee)emp.Clone();
Console.WriteLine("empClone object is as follows:");
Console.WriteLine(empClone);
Console.WriteLine("\n Now changing the name, id and address of the cloned object ");
empClone.Id=10;
empClone.Name="Sam";
empClone.EmpAddress.Address= "221, xyz Road, Canada";
Console.WriteLine("Now emp1 object is as follows:");
Console.WriteLine(emp);
Console.WriteLine("And emp1Clone object is as follows:");
Console.WriteLine(empClone);
}
}
}
浅层拷贝的输出
以下是程序的输出。
***Shallow vs Deep Copy Demo.***
The original object is emp1 which is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Making a clone of emp1 now.
empClone object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Now changing the name, id and address of the cloned object
Now emp1 object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 221, xyz Road, Canada
And emp1Clone object is as follows:
Employee Id is : 10,Employee Name is : Sam, Employee Address is : 221, xyz Road, Canada
分析
有一个不想要的副作用。在前面的输出中,原始对象(emp
)的地址由于修改克隆对象(empClone
)而被修改。发生这种情况是因为原始对象和克隆对象指向同一个地址,并且它们不是 100%分离的。图 2-8 描述了该场景。
图 2-8
浅拷贝
现在让我们用深度复制实现来做实验。让我们修改Employee
类的Clone
方法如下。(我取消了深层副本的代码注释,并注释掉了浅层副本中的代码。)
public Object Clone()
{
// Shallow Copy
//return this.MemberwiseClone();
#region For deep copy
Employee employee = (Employee)this.MemberwiseClone();
employee.EmpAddress = (EmpAddress)this.EmpAddress.CloneAddress();
/*
* NOTE:
Error: MemberwiseClone() is protected, you cannot access it via a qualifier of type EmpAddress.The qualifier must be Employee or its derived type.
*/
//employee.EmpAddress = (EmpAddress)this.EmpAddress.MemberwiseClone();//error
return employee;
#endregion
}
深层拷贝的输出
下面是修改后的输出。
***Shallow vs Deep Copy Demo***
The original object is emp1 which is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Making a clone of emp1 now.
empClone object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Now changing the name, id and address of the cloned object
Now emp1 object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
And emp1Clone object is as follows:
Employee Id is : 10,Employee Name is : Sam, Employee Address is : 221, xyz Road, Canada
分析
这一次,您不会看到由于修改empClone
对象而产生的不必要的副作用。这是因为原始对象和克隆对象彼此不同且相互独立。图 2-9 描述了这个场景。
图 2-9
深层拷贝
问答环节
2.4 什么时候你应该选择浅层拷贝而不是 深层拷贝 (反之亦然)?
以下是主要原因。
-
浅层拷贝速度更快,成本更低。如果您的目标对象只有基本字段,那么使用总是更好。
-
深层拷贝开销大,速度慢,但是如果目标对象包含许多引用其他对象的字段,它就很有用。
2.5 在 C# 中,如果我需要复制一个对象,我需要使用 MemberwiseClone()
方法 。这是正确的吗?
不,还有其他选择。例如,在实现深度复制时,可以选择序列化机制,或者可以编写自己的复制构造函数,等等。每种方法都有其优点和缺点。因此,最终,开发人员有权决定哪种方法最适合他的需求。许多对象非常简单,它们不包含对其他对象的引用。因此,要从这些对象复制,一个简单的浅层复制机制就足够了。
你能给我举个例子演示一下 复制构造器 的用法吗?
由于 C# 不支持默认的复制构造函数,您可能需要编写自己的复制构造函数。演示 4 供您参考。
演示 4
在这个例子中,Employee
和EmpAddress
类都有与演示 3 几乎相同的描述。唯一的不同是,这一次,你注意到在Employee
类中出现了一个复制构造函数,而不是Clone()
方法。我们继续吧。
这一次,使用下面的实例构造函数,
// Instance Constructor
public Employee(int id, string name, EmpAddress empAddress)
{
this.Id = id;
this.Name = name;
this.EmpAddress = empAddress;
}
你可以如下创建一个Employee
的对象。
EmpAddress initialAddress = new EmpAddress("21, abc Road, USA");
Employee emp = new Employee(1, "John",initialAddress);
在这个Employee
类中,还有一个用户自定义的复制构造函数,如下。
// Copy Constructor
public Employee(Employee originalEmployee)
{
this.Id = originalEmployee.Id;
this.Name = originalEmployee.Name;
//this.EmpAddress = (EmpAddress)this.EmpAddress.CloneAddress(); // ok
this.EmpAddress = originalEmployee.EmpAddress.CloneAddress() as EmpAddress; // also ok
}
您可以看到,通过使用复制构造函数,我复制了简单类型(Id, Name
)和引用类型(EmpAddress
)。因此,一旦创建了像emp
这样的Employee
对象,就可以使用下面的代码从它创建另一个empClone
对象。
Employee empClone= new Employee(emp);
和前面的演示一样,一旦我从现有的对象(emp
)创建了一个副本(empClone
),我就为了验证的目的对复制的对象进行了修改,使其更容易理解。这是完整的代码。
using System;
namespace UserdefinedCopyConstructorDemo
{
class EmpAddress
{
public string Address { get; set; }
public EmpAddress(string address)
{
this.Address = address;
}
public override string ToString()
{
return this.Address;
}
public object CloneAddress()
{
// Shallow Copy
return this.MemberwiseClone();
}
}
class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public EmpAddress EmpAddress { get; set; }
// Instance Constructor
public Employee(int id, string name, EmpAddress empAddress)
{
this.Id = id;
this.Name = name;
this.EmpAddress = empAddress;
}
// Copy Constructor
public Employee(Employee originalEmployee)
{
this.Id = originalEmployee.Id;
this.Name = originalEmployee.Name;
//this.EmpAddress = (EmpAddress)this.EmpAddress.CloneAddress(); // ok
this.EmpAddress = originalEmployee.EmpAddress.CloneAddress() as EmpAddress; // Also ok
}
public override string ToString()
{
return string.Format("Employee Id is : {0},Employee Name is : {1}, Employee Address is : {2}", this.Id, this.Name, this.EmpAddress);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***A simple copy constructor demo***\n");
EmpAddress initialAddress = new EmpAddress("21, abc Road, USA");
Employee emp = new Employee(1, "John",initialAddress);
Console.WriteLine("The details of emp is as follows:");
Console.WriteLine(emp);
Console.WriteLine("\n Copying from emp1 to empClone now.");
Employee empClone= new Employee(emp);
Console.WriteLine("The details of empClone is as follows:");
Console.WriteLine(empClone);
Console.WriteLine("\nNow changing the id,name and address of empClone.");
empClone.Name = "Sam";
empClone.Id = 2;
empClone.EmpAddress.Address= "221, xyz Road, Canada";
Console.WriteLine("The details of emp is as follows:");
Console.WriteLine(emp);
Console.WriteLine("The details of empClone is as follows:");
Console.WriteLine(empClone);
Console.ReadKey();
}
}
}
输出
这是示例输出。
***A simple copy constructor demo***
The details of emp is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Copying from emp1 to empClone now.
The details of empClone is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Now changing the id,name and address of empClone.
The details of emp is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
The details of empClone is as follows:
Employee Id is : 2,Employee Name is : Sam, Employee Address is : 221, xyz Road, Canada
分析
请注意输出的最后部分。它反映出只对复制的对象进行了适当的更改。
本章向您展示了原型设计模式的多种实现,并讨论了浅拷贝和深拷贝之间的区别。您还了解了用户定义的复制构造函数。现在你可以进入下一章,学习构建器模式。
三、构建器模式
本章涵盖了构建器模式。
GoF 定义
将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。
概念
构建器模式对于创建包含多个部分的复杂对象非常有用。对象创建过程应该独立于这些部分;换句话说,构建过程并不关心这些部分是如何组装的。此外,根据定义,您应该能够使用相同的构造过程来创建对象的不同表示。
根据 GoF,这种模式涉及四个不同的玩家,他们的关系如图 3-1 所示。
图 3-1
构建器模式示例
这里,Product
是考虑中的复杂对象,是最终输出。Builder
是一个接口,包含构建最终产品部件的方法。ConcreteBuilder
实现了Builder
接口,并组装了一个Product
对象的不同部分。ConcreteBuilder
对象构建了Product
实例的内部表示,它有一个方法可以被调用来获得这个Product
实例。Director
负责使用Builder
接口创建最终对象。值得注意的是Director
是决定构建产品的步骤顺序的类/对象。所以,你可以放心地假设一个Director
对象可以用来改变生产不同产品的顺序。
在演示 1 中,IBuilder
表示Builder
接口;Car
和Motorcycle
分别是ConcreteBuilder
s. Product
和Director
类有它们通常的含义。
真实世界的例子
订购计算机时,会根据客户的喜好组装不同的硬件部件。例如,一个客户可以选择采用英特尔处理器的 500 GB 硬盘,另一个客户可以选择采用 AMD 处理器的 250 GB 硬盘。这里计算机是最终产品,客户扮演导演的角色,销售人员/组装人员扮演具体建造者的角色.
计算机世界的例子
当您想要将一种文本格式转换为另一种文本格式时,例如从 RTF 转换为 ASCII,可以使用这种模式。
履行
这个例子有以下几个部分:IBuilder
Car
MotorCycle
Product
Director
。IBuilder
创建Product
对象的一部分,其中Product
代表正在构建的复杂对象。Car
和MotorCycle
是IBuilder
接口的具体实现。(是的,IVehicle
可能是比IBuilder,
更好的命名,但我选择了后者,以强调它是一个构建器接口。)它们实现了IBuilder
接口,其表示如下。****
interface IBuilder
{
void StartUpOperations();
void BuildBody();
void InsertWheels();
void AddHeadlights();
void EndOperations();
Product GetVehicle();
}
这就是为什么Car
和Motorcycle
需要为以下方法供应身体:StartUpOperations()
、BuildBody()
、InsertWheels()
、AddHeadlights()
、EndOperations()
、GetVehicle()
。前五种方法很简单;他们在开始时执行各种操作,构建车辆的车身,添加车轮和大灯,并在结束时执行一项操作。(比方说,制造商想要添加一个标志或打磨车辆,等等。在接下来的例子中,我通过为摩托车画一条简单的线,为汽车画一条虚线,使操作变得非常简单。)方法GetVehicle()
返回最终的乘积。Product
类非常容易理解,虽然我在其中使用了 LinkedList 数据结构,但是您可以出于类似的目的使用任何您喜欢的数据结构。
最后,Director
类负责使用IBuilder
接口构建这些产品的最终部分。(参见图 3-1 中 GoF 定义的结构。)因此,在我们的代码中,Director
类如下所示。
class Director
{
IBuilder builder;
/*
* A series of steps.In real life, these steps
* can be much more complex.
*/
public void Construct(IBuilder builder)
{
this.builder = builder;
builder.StartUpOperations();
builder.BuildBody();
builder.InsertWheels();
builder.AddHeadlights();
builder.EndOperations();
}
}
一个Director
对象调用这个Construct()
方法来创建不同类型的车辆。
现在让我们浏览一下代码,看看不同的部分是如何组装成这个模式的。
类图
图 3-2 显示了类图。
图 3-2
类图
解决方案资源管理器视图
图 3-3 显示了程序的高层结构。
图 3-3
解决方案资源管理器视图
Note
长话短说,我没有扩展汽车和摩托车类。这些类实现了IBuilder
,很容易理解。如果需要的话也可以参考类图(见图 3-2 )。对于这本书的其他一些截图,我遵循了相同的机制;就是当一个截图真的很大的时候,我只显示重要的部分。
演示 1
在这个例子中,我为所有不同的玩家使用不同的文件。下面是完整的实现。
// IBuilder.cs
namespace BuilderPatternSimpleExample
{
// The common interface
interface IBuilder
{
void StartUpOperations();
void BuildBody();
void InsertWheels();
void AddHeadlights();
void EndOperations();
Product GetVehicle();
}
}
// Car.cs
namespace BuilderPatternSimpleExample
{
// Car is a ConcreteBuilder
class Car : IBuilder
{
private string brandName;
private Product product;
public Car(string brand)
{
product = new Product();
this.brandName = brand;
}
public void StartUpOperations()
{ // Starting with brandname
product.Add("-----------");
product.Add($"Car model name :{this.brandName}");
}
public void BuildBody()
{
product.Add("This is a body of a Car");
}
public void InsertWheels()
{
product.Add("4 wheels are added");
}
public void AddHeadlights()
{
product.Add("2 Headlights are added");
}
public void EndOperations()
{
product.Add("-----------");
}
public Product GetVehicle()
{
return product;
}
}
}
// Motorcycle.cs
namespace BuilderPatternSimpleExample
{
// Motorcycle is another ConcreteBuilder
class Motorcycle : IBuilder
{
private string brandName;
private Product product;
public Motorcycle(string brand)
{
product = new Product();
this.brandName = brand;
}
public void StartUpOperations()
{
product.Add("_________________");
}
public void BuildBody()
{
product.Add("This is a body of a Motorcycle");
}
public void InsertWheels()
{
product.Add("2 wheels are added");
}
public void AddHeadlights()
{
product.Add("1 Headlights are added");
}
public void EndOperations()
{
// Finishing up with brandname
product.Add($"Motorcycle model name :{this.brandName}");
product.Add("_________________");
}
public Product GetVehicle()
{
return product;
}
}
}
// Product.cs
using System;
using System.Collections.Generic; // For LinkedList
namespace BuilderPatternSimpleExample
{
// "Product"
class Product
{
/*
You can use any data structure that you prefer e.g.List<string> etc.
*/
private LinkedList<string> parts;
public Product()
{
parts = new LinkedList<string>();
}
public void Add(string part)
{
// Adding parts
parts.AddLast(part);
}
public void Show()
{
Console.WriteLine("\nProduct completed as below :");
foreach (string part in parts)
Console.WriteLine(part);
}
}
}
// Director.cs
namespace BuilderPatternSimpleExample
{
// "Director"
class Director
{
private IBuilder builder;
/*
* A series of steps.In real life, these steps
* can be much more complex.
*/
public void Construct(IBuilder builder)
{
this.builder = builder;
builder.StartUpOperations();
builder.BuildBody();
builder.InsertWheels();
builder.AddHeadlights();
builder.EndOperations();
}
}
}
// Client (Program.cs)
using System;
namespace BuilderPatternSimpleExample
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Builder Pattern Demo.***");
Director director = new Director();
IBuilder b1 = new Car("Ford");
IBuilder b2 = new Motorcycle("Honda");
// Making Car
director.Construct(b1);
Product p1 = b1.GetVehicle();
p1.Show();
// Making Motorcycle
director.Construct(b2);
Product p2 = b2.GetVehicle();
p2.Show();
Console.ReadLine();
}
}
}
输出
这是输出。
***Builder Pattern Demo.***
Product completed as below :
-----------
Car model name :Ford
This is a body of a Car
4 wheels are added
2 Headlights are added
-----------
Product completed as below :
_________________
This is a body of a Motorcycle
2 wheels are added
1 Headlights are added
Motorcycle model name :Honda
_________________
分析
在Main(),
内部,一个Director
实例创建了两个不同的产品,因为我在Construct()
方法中传递了两个不同的构建器,该方法只是依次调用了StartUpOperations(), BuildBody(), InsertWheels(), AddHeadlights()
和EndOperations()
方法。此外,不同的构建者对这些方法有不同的实现。
问答环节
3.1 使用构建器模式的 优势 有哪些?
以下是一些优点。
-
您指导构建器一步一步地构建对象,并且通过隐藏复杂的构建过程的细节来促进封装。当整个建造过程结束时,导演可以从建造者那里取回最终产品。一般来说,在一个高层次上,你似乎只有一个方法来制作完整的产品,但是其他的内部方法也参与了创建过程。因此,你可以更好地控制施工过程。
-
使用这种模式,相同的构建过程可以产生不同的产品。
-
您还可以改变产品的内部表示。
3.2 与构建器模式相关的 和 有哪些缺点?
以下是一些缺点。
-
如果你想处理易变的对象(可以在以后修改),它是不合适的。
-
您可能需要复制部分代码。这些重复在某些情况下可能会产生重大影响。
-
要创建不同类型的产品,您需要创建不同类型的混凝土建筑商。
3.3 在这个模式的例子中,你可以用一个 抽象类 来代替接口吗?
是的。在这个例子中,你可以使用一个抽象类来代替接口。
3.4 如何决定在应用中使用抽象类还是接口?
如果你想要集中的或者默认的行为,抽象类是更好的选择。在这些情况下,您可以提供一些默认的实现。另一方面,接口实现从零开始,并指示规则/契约,如要做什么,但它不会将“如何做”的部分强加于您。此外,当您试图实现多重继承的概念时,最好使用接口。
请记住,如果您需要在一个接口中添加一个新方法,那么您需要跟踪该接口的所有实现,并且您需要将该方法的具体实现放在所有这些地方。在这种情况下,抽象类是更好的选择,因为您可以在具有默认实现的抽象类中添加新方法,并且现有代码可以顺利运行。但是 C# v8 在。NET Core 3.0 也引入了默认接口方法的概念。因此,如果您使用的是 C# v8.0 以上的遗留版本,建议的最后几行是最好的。
以下是 MSDN 社区的一些重要建议。
-
当你有多个版本的组件时,使用一个抽象类。一旦更新了基类,所有派生类都会自动更新。另一方面,接口一旦创建就不应该更改。
-
当功能分布在不同的/不相关的对象中时,使用接口。抽象类应该用于共享公共功能的紧密相关的对象。
-
抽象类允许您部分实现您的类,而接口不包含任何成员的实现(忽略 C# v8.0 中的默认接口方法)。
3.5 在汽车示例中,型号名称添加在开头,但对于摩托车,型号名称添加在结尾。这是故意的吗?
是的。我这样做是为了证明这样一个事实,即每个混凝土建造者都可以决定如何生产最终产品的各个部分。他们有这种自由。
3.6 你为什么为导演 使用单独的班级?您可以使用客户端代码来扮演导演的角色。
没有人强迫你这样做。在前面的实现中,我想在实现中将这个角色与客户端代码分开。但是在接下来的演示中,我使用客户端作为导演。
3.7 什么叫 客户代码 ?
包含Main()
方法的类是客户端代码。
你几次提到不同的步骤。你能演示一个用不同的变化和步骤创建最终产品的实现吗?
接得好。您是在要求我展示构建器模式的真正威力。让我们考虑下一个例子。
一种替代实施方式
让我们考虑一个替代实现。它给你更多的灵活性。下面是修改后的实现的主要特征。
-
为了关注核心设计,在这个实现中,让我们把汽车看作最终产品。
-
在这个实现中,客户机代码本身扮演着一个指挥者的角色。
-
和前面的例子一样,
IBuilder
表示构建器接口,但是这次,我没有使用GetVehicle()
方法,而是将其重命名为ConstructCar()
。 -
如演示 1 所示,
Car
类已经实现了接口中定义的所有方法,定义如下:interface IBuilder { /* * All these methods return type is IBuilder. * This will help us to apply method chaining. * I'm also providing values for default arguments. */ IBuilder StartUpOperations(string optionalStartUpMessage = " Making a car for you."); IBuilder BuildBody(string optionalBodyType = "Steel"); IBuilder InsertWheels(int optionalNoOfWheels = 4); IBuilder AddHeadlights(int optionalNoOfHeadLights = 2); IBuilder EndOperations(string optionalEndMessage = "Car construction is completed."); /*Combine the parts and make the final product.*/ Product ConstructCar(); }
请注意,这些方法与前面演示中的方法相似,但是有两个主要的变化:它们的返回类型是IBuilder
,并且它们接受可选参数。这为您提供了灵活性——您可以向它们传递参数,也可以简单地忽略它们。但最重要的是,由于返回类型是IBuilder
,现在您可以应用方法链接,这就是为什么您会在Main()
中看到如下代码段。
-
在前面的部分中,我没有向
EndOperations
方法传递任何参数。同样,在我调用StartUpOperations
方法之前,我调用了InsertWheels
和AddHeadlights
方法。这给了客户对象(在这种情况下是导演)自由,他想如何创建最终产品。 -
最后,
Product
类如下。sealed class Product { /* * You can use any data structure that you prefer * e.g. List<string> etc. */ private LinkedList<string> parts; public Product() { parts = new LinkedList<string>(); } public void Add(string part) { // Adding parts parts.AddLast(part); } public void Show() { Console.WriteLine("\nProduct completed as below :"); foreach (string part in parts) Console.WriteLine(part); } }
-
这次我做了
Product
类sealed
,因为我想防止继承。像前面的演示一样,parts 属性是private,
,并且在类中没有 setter 方法。所有这些构造都可以帮助您提高不变性(这在接下来的演示中是可选的),这在您使用构建器模式时是经常需要的。您甚至可以从部件声明中排除private
修饰符,因为默认情况下类成员拥有私有访问权。 -
你可以注意到另一点。在客户端代码内部,我使用了
customCar and CustomCar2
来制造汽车。这些是Product
类实例。第一个是静态场,第二个是非静态场。我保留了这两个来给你展示Main()
中Product
类的用法变化。
Product customCar2 = new Car("Sedan")
.InsertWheels(7)
.AddHeadlights(6)
.StartUpOperations("Sedan creation in progress")
.BuildBody()
.EndOperations()//will take default end message
.ConstructCar();
customCar2.Show();
类图
图 3-4 显示了演示 2 中替代实现的修改后的类图。
图 3-4
备选实现的类图
解决方案资源管理器视图
图 3-5 显示了新的解决方案浏览器视图。
图 3-5
解决方案资源管理器视图
演示 2
下面是构建器模式的另一个实现。
using System;
using System.Collections.Generic;
namespace BuilderPatternSecondDemonstration
{
// The common interface
interface IBuilder
{
/*
* All these methods return types are IBuilder.
* This will help us to apply method chaining.
* I'm also providing values for default arguments.
*/
IBuilder StartUpOperations(string optionalStartUpMessage = "Making a car for you.");
IBuilder BuildBody(string optionalBodyType = "Steel");
IBuilder InsertWheels(int optionalNoOfWheels = 4);
IBuilder AddHeadlights(int optionalNoOfHeadLights = 2);
IBuilder EndOperations(string optionalEndMessage = "Car construction is complete.");
// Combine the parts and make the final product.
Product ConstructCar();
}
// Car class
class Car : IBuilder
{
Product product;
private string brandName;
public Car(string brand)
{
product = new Product();
this.brandName = brand;
}
public IBuilder StartUpOperations(string optionalStartUpMessage = " Making a car for you.")
{ // Starting with brandname
product.Add(optionalStartUpMessage);
product.Add($"Car model name :{this.brandName}");
return this;
}
public IBuilder BuildBody(string optionalBodyType = "Steel")
{
product.Add(($"Body type:{optionalBodyType}"));
return this;
}
public IBuilder InsertWheels(int optionalNoOfWheels = 4)
{
product.Add(($"Wheels:{optionalNoOfWheels.ToString()}"));
return this;
}
public IBuilder AddHeadlights(int optionalNoOfHeadLights = 2)
{
product.Add(($"Headlights:{optionalNoOfHeadLights.ToString()}"));
return this;
}
public IBuilder EndOperations(string optionalEndMessage = "Car construction is completed.")
{
product.Add(optionalEndMessage);
return this;
}
public Product ConstructCar()
{
return product;
}
}
// Product class
/*
* Making the class sealed. The attributes are also private and
* there is no setter methods. These are used to promote immutability.
*/
sealed class Product
{
/* You can use any data structure that you prefer e.g.List<string> etc.*/
private LinkedList<string> parts;
public Product()
{
parts = new LinkedList<string>();
}
public void Add(string part)
{
// Adding parts
parts.AddLast(part);
}
public void Show()
{
Console.WriteLine("\nProduct completed as below :");
foreach (string part in parts)
Console.WriteLine(part);
}
}
// Director class (Client Code)
class Program
{
static Product customCar;
static void Main(string[] args)
{
Console.WriteLine("***Builder Pattern alternative implementation.***");
/* Making a custom car (through builder)
Note the steps:
Step1:Get a builder object with required parameters
Step2:Setter like methods are used.They will set the optional fields also.
Step3:Invoke the ConstructCar() method to get the final car.
*/
customCar = new Car("Suzuki Swift").StartUpOperations()//will take default message
.AddHeadlights(6)
.InsertWheels()//Will consider default value
.BuildBody("Plastic")
.EndOperations("Suzuki construction Completed.")
.ConstructCar();
customCar.Show();
/*
Making another custom car (through builder) with a different sequence and steps.
*/
// Directly using the Product class now.
// (Just for a variation of usage)
Product customCar2 = new Car("Sedan")
.InsertWheels(7)
.AddHeadlights(6)
.StartUpOperations("Sedan creation in progress")
.BuildBody()
.EndOperations() // will take default end message
.ConstructCar();
customCar2.Show();
}
}
}
输出
这是新的输出。粗体行是为了让您注意输出中的差异。
***Builder Pattern alternative implementation.***
Product completed as below :
Making a car for you.
Car model name :Suzuki Swift
Headlights:6
Wheels:4
Body type:Plastic
Suzuki construction Completed.
Product completed as below :
Wheels:7
Headlights:6
Sedan creation in progress
Car model name :Sedan
Body type:Steel
Car construction is completed.
分析
仔细看一下Main()
方法。您可以看到,主管(客户)可以使用构建器创建两个不同的产品,并且每次都遵循不同的步骤序列。这使得您的应用非常灵活。
问答环节
你在试图推广不变性。与 不可变对象 相关的关键好处是什么?
一旦构造好,就可以安全地共享它们,最重要的是,它们是线程安全的,并且在多线程环境中可以节省同步成本。
3.10 什么时候我应该考虑使用构建器模式?
如果您需要制作一个复杂的对象,它涉及到构建过程的各个步骤,同时,产品需要是不可变的,那么 Builder 模式是一个不错的选择。***
四、工厂方法模式
本章涵盖了工厂方法模式。
Note
为了更好地理解这种模式,我建议你首先阅读第二十四章,它涵盖了简单工厂模式。简单工厂模式没有直接落入四人组设计模式,所以它出现在本书的第二部分;然而,如果您首先理解简单工厂模式的优点和缺点,工厂方法模式会更有意义。
GoF 定义
定义一个创建对象的接口,但是让子类决定实例化哪个类。工厂方法让一个类将实例化推迟到子类。
概念
这里,您从定义应用基本结构的抽象 creator 类开始,子类(从这个抽象类派生)负责执行实际的实例化过程。当你分析下面的例子时,这个概念就会对你有意义。
真实世界的例子
简单工厂模式的例子也适用于此。例如,在餐馆中,根据顾客的喜好,厨师可以在最终产品的准备过程中添加更多(或更少)的香料、油等。
让我们看另一个例子。假设一家汽车制造公司每年生产不同型号的汽车。根据他们的市场调查,他们决定一个模型,并开始生产。基于汽车的模型,不同的零件被制造和组装。一家公司应该随时准备好应对客户未来可能选择更好模式的变化。如果公司需要为只需要几个新功能的新模型创建一个全新的设置,这可能会极大地影响公司的利润率。因此,该公司应该以这样一种方式建立工厂,它可以很容易地为即将到来的模型生产零件。
计算机世界的例子
在数据库编程中,您可能需要支持不同的数据库用户。例如,一个用户可能使用 SQL Server,而另一个用户可能选择 Oracle。当你需要向你的数据库中插入数据时,你首先要创建一个连接对象,比如SqlConnection
或者OracleConnection
,然后才能继续。如果您将代码放入一个if-else
块(或switch
语句),您可能需要重复许多相似的代码,这不容易维护。此外,每当您决定开始支持一种新的连接类型时,您需要重新打开代码并进行一些修改。使用工厂方法模式可以解决这类问题。
履行
接下来的例子提供了一个名为AnimalFactory
的抽象 creator 类来定义基本结构。根据定义,实例化过程是通过从这个抽象类派生的子类来执行的。这个例子里有很多小类。我可以为每个类创建单独的文件,这种方法经常被许多开发人员所鼓励。但是这些类非常简短、简单和直接。所以,我把它们放在一个文件里。对于本书中类似的例子,我遵循同样的原则。
类图
图 4-1 为类图。
图 4-1
类图
解决方案资源管理器视图
图 4-2 显示了程序的高层结构。
图 4-2
解决方案资源管理器视图
演示 1
下面是实现。类似于第二十四章中的简单工厂模式,我使用相同的继承层次结构;也就是说,这一次,您看到Dog
和Tiger
类都实现了它们的父接口IAnimal
的AboutMe()
方法。因此,您会在示例的开头看到下面的代码段。
public interface IAnimal
{
void AboutMe();
}
public class Dog : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The dog says: Bow-Wow. I prefer barking.");
}
}
public class Tiger : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The tiger says: Halum. I prefer hunting.");
}
}
您可以看到另一个继承层次结构,其中两个具体的类——称为DogFactory
和TigerFactory
—创建了 dog 和 tiger 对象。它们中的每一个都继承自一个抽象类AnimalFactory
。这两个具体的类推迟了实例化过程。我加入了支持性的评论来帮助你更好地理解。下面的代码段描述了它。
public abstract class AnimalFactory
{
/*
Remember the GoF definition which says
"....Factory method lets a class defer instantiation
to subclasses." The following method will create a tiger or a dog object, but at this point it does not know whether it will get a dog or a tiger. It will be decided by
the subclasses i.e. DogFactory or TigerFactory.
So, the following method is acting like a factory
(of creation).
*/
public abstract IAnimal CreateAnimal();
}
// DogFactory is used to create dog
public class DogFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
// Creating a Dog
return new Dog();
}
}
// TigerFactory is used to create tigers
public class TigerFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
// Creating a Tiger
return new Tiger();
}
}
这是完整的演示。
using System;
namespace FactoryMethodPattern
{
#region Animal Hierarchy
/*
* Both the Dog and Tiger classes will
* implement the IAnimal interface method.
*/
public interface IAnimal
{
void AboutMe();
}
// Dog class
public class Dog : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The dog says: Bow-Wow. I prefer barking.");
}
}
//Tiger class
public class Tiger : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The tiger says: Halum. I prefer hunting.");
}
}
#endregion
#region Factory Hierarchy
// Both DogFactory and TigerFactory will use this.
public abstract class AnimalFactory
{
/*
Remember the GoF definition which says
"....Factory method lets a class defer instantiation
to subclasses." The following method will create a Tiger
or a Dog, but at this point it does not know whether
it will get a dog or a tiger. It will be decided by
the subclasses i.e. DogFactory or TigerFactory.
So, the following method is acting like a factory
(of creation).
*/
public abstract IAnimal CreateAnimal();
}
// DogFactory is used to create dog
public class DogFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
// Creating a Dog
return new Dog();
}
}
// TigerFactory is used to create tigers
public class TigerFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
// Creating a Tiger
return new Tiger();
}
}
#endregion
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Factory Pattern Demo.***\n");
// Creating a Tiger Factory
AnimalFactory tigerFactory = new TigerFactory();
// Creating a tiger using the Factory Method
IAnimal tiger = tigerFactory.CreateAnimal();
tiger.AboutMe();
// Creating a DogFactory
AnimalFactory dogFactory = new DogFactory();
// Creating a dog using the Factory Method
IAnimal dog = dogFactory.CreateAnimal();
dog.AboutMe();
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Factory Pattern Demo.***
The tiger says: Halum. I prefer hunting.
The dog says: Bow-Wow. I prefer barking.
修改后的实现 1
现在让我们来看看您可以对演示 1 进行的两个重要修改。
在第一个修改的实现中,更多的灵活性被添加到我们先前的实现中。注意,AnimalFactory
类是一个抽象类,所以你可以利用它。假设您希望一个子类遵循一个可以从其父类(或基类)强加的规则。为简单起见,让我们通过控制台消息来实施该规则,如下面的演示所示。
部分演示 1
在修改后的实现中,我在AnimalFactory class
中引入了一个叫做MakeAnimal()
的新方法。
// Modifying the AnimalFactory class.
public abstract class AnimalFactory
{
public IAnimal MakeAnimal()
{
Console.WriteLine("AnimalFactory.MakeAnimal()-You cannot ignore parent rules.");
IAnimal animal = CreateAnimal();
animal.AboutMe();
return animal;
}
/*
Remember the GoF definition which says
"....Factory method lets a class defer instantiation
to subclasses." Following method will create a Tiger
or a Dog class, but at this point it does not know whether
it will get a dog or a tiger. It will be decided by
the subclasses i.e.DogFactory or TigerFactory.
So, the following method is acting like a factory
(of creation).
*/
public abstract IAnimal CreateAnimal();
}
客户端代码采用了这些更改;也就是说,不是先调用CreateAnimal()
再使用AboutMe()
。我只是在下面的代码段中调用了MakeAnimal()
。旧代码被注释以供参考,并与新代码进行比较。
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Factory Pattern Modified Demo.***\n");
// Creating a Tiger Factory
AnimalFactory tigerFactory = new TigerFactory();
// Creating a tiger using the Factory Method
//IAnimal tiger = tigerFactory.CreateAnimal();
//tiger.AboutMe();
IAnimal tiger = tigerFactory.MakeAnimal();
// Creating a DogFactory
AnimalFactory dogFactory = new DogFactory();
// Creating a dog using the Factory Method
//IAnimal dog = dogFactory.CreateAnimal();
//dog.AboutMe();
IAnimal dog = dogFactory.MakeAnimal();
Console.ReadKey();
}
}
输出
下面是修改后的输出。
***Factory Pattern Modified Demo.***
AnimalFactory.MakeAnimal()-You cannot ignore parent rules.
The tiger says: Halum. I prefer hunting.
AnimalFactory.MakeAnimal()-You cannot ignore parent rules.
The dog says: Bow-Wow. I prefer barking.
分析
现在,在每种情况下,您都会看到以下警告:“…您不能忽略父规则。”这是对演示 1 的增强。
问答环节
4.1 为什么将 CreateAnimal()
方法 从客户端代码中分离出来?
我做这件事只有一个目的。我想让子类创建专门化的对象。如果你仔细观察,你会发现只有这个“创造性部分”在不同的产品中有所不同。我在第二十四章的“问答环节”中详细讨论了这一点。
使用这样的工厂有哪些 优势 ?
以下是一些关键优势。
-
您将变化的代码与不变化的代码分开(换句话说,使用简单工厂模式的优势仍然存在),这有助于您轻松地维护代码。
-
代码不是紧密耦合的,所以您可以在系统中随时添加新的类,如
Lion
、Bear
等,而无需修改现有的架构。换句话说,我遵循了“修改时封闭,扩展时开放”的原则。
4.3 使用这样的工厂有哪些 挑战 ?
如果您需要处理许多不同类型的对象,那么系统的整体性能会受到影响。
4.4 工厂方法模式支持两个平行的层次结构。这是正确的吗?
接得好。是的,从类图来看,很明显这种模式支持并行的类层次结构(见图 4-3 )。
图 4-3
本例中的两个类层次结构
在这个例子中,AnimalFactory
、DogFactory
和TigerFactory
被放置在一个层次中,而IAnimal
、Dog
和Tiger
被放置在另一个层次中。因此,你可以看到创作者和他们的创作/产品是并行运行的两个层次。
你应该总是用一个抽象的关键字来标记工厂方法,这样子类就可以完成它们。这是正确的吗?
不。当创建者没有子类时,您可能会对默认的工厂方法感兴趣。在这种情况下,不能用关键字abstract
标记工厂方法。
然而,要看到工厂方法模式的真正威力,您可能需要遵循这里在大多数情况下实现的设计。
看来工厂方法模式与简单工厂模式没有太大的不同。这是正确的吗?
如果你看看这两章的例子中的子类,你可能会发现一些相似之处。但是你不应该忘记工厂方法模式的主要目的;它为你提供了一个框架,通过这个框架,不同的子类可以生产不同的产品。在简单工厂模式中,您不能以类似的方式改变产品。您可以将简单的工厂模式视为一次性交易,但最重要的是,您的创造性部分不会因为修改而关闭。每当您想要添加新的东西时,您需要在简单工厂模式的工厂类中添加一个if-else
块或一个switch
语句。
在这种情况下,永远记住 GoF 定义,它说,“工厂方法模式让一个类延迟实例化到子类。”仔细看看修改后的实现。你可以看到CreateAnimal()
通过AnimalFactory
的适当子类创建了一只狗或一只老虎。所以,CreateAnimal()
是这个设计中抽象的工厂方法。当MakeAnimal()
在其体内使用CreateAnimal()
时,它不知道是对狗还是对老虎有效。AnimalFactory
的子类只知道为这个应用创建具体的实现(一只狗或一只老虎)。
Note
在System.Web.WebRequest
类中,你可以看到Create
方法,它有两个重载。在这个方法中,您可以传递一个统一资源标识符(URI)。此方法为请求确定适当的协议并返回适当的子类,例如,HttpWebRequest(如果 URI 以 http://或 https://开头)、FtpWebRequest(如果 URI 以 ftp://开头)等等。如果 URI 从 HTTP 更改为 FTP,底层代码不需要更改,调用者也不需要担心协议的细节。这种架构促进了工厂模式的使用,但是对于新的开发不推荐使用 HttpWebRequest。微软建议你使用系统。请改用. Net.Http.HttpClient 类。
修改后的实现 2
本章以对我们的初始实现的额外更新结束。现在让我们通过使用方法参数来更新演示 1。我们继续吧。当您从 Apress 网站下载代码时,您可以获得完整的实现。为了简洁起见,这里只展示了部分演示。
部分演示 2
这段代码表明,如果在CreateAnimal()
.
中使用方法参数,可以使原来的实现变得更好,这种方法提供了一个好处。您可以只创建一个具体的工厂类,而不是创建DogFactory, TigerFactory
等等,如下所示。
#region Factory Hierarchy
// Both DogFactory and TigerFactory will use this.
public abstract class AnimalFactory
{
/*
Remember the GoF definition which says
"....Factory method lets a class defer instantiation
to subclasses." Following method will create a Tiger
or a Dog, but at this point it does not know whether
it will get a dog or a tiger. It will be decided by
the subclasses i.e.DogFactory or TigerFactory.
So, the following method is acting like a factory
(of creation).
*/
public abstract IAnimal CreateAnimal(string animalType);
}
/*
* ConcreteAnimalFactory is used to create dogs or tigers
* based on method parameter of CreateAnimal() method.
*/
public class ConcreteAnimalFactory : AnimalFactory
{
public override IAnimal CreateAnimal(string animalType)
{
if (animalType.Contains("dog"))
{
// Creating a Dog
return new Dog();
}
else
if (animalType.Contains("tiger"))
{
// Creating a Dog
return new Tiger();
}
else
{
throw new ArgumentException("You need to pass either a dog or a tiger as an argument.");
}
}
}
#endregion
现在你可以在CreateAnimal(...)
方法中传递一个“狗”字符串或一个“老虎”字符串来创建一个Dog
或一个Tiger
实例。为了适应这些变化,您可以按如下方式更新客户端代码。(这一次,animalFactory
创建了Dog
和Tiger
实例。每个人都知道“编程到接口”有这种好处。)
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Factory Pattern Demo.***");
Console.WriteLine("***It's a modified version using method parameter(s).***\n");
// Creating a factory that can produce animals
AnimalFactory animalFactory = new ConcreteAnimalFactory();
// Creating a tiger using the Factory Method
IAnimal tiger = animalFactory.CreateAnimal("tiger");
tiger.AboutMe();
// Now creating a dog.
IAnimal dog = animalFactory.CreateAnimal("dog");
dog.AboutMe();
Console.ReadKey();
}
}
输出
现在,如果您执行这个程序,您可以得到以下输出。
***Factory Pattern Demo.***
***It's a modified version using method parameter(s).***
The tiger says: Halum. I prefer hunting.
The dog says: Bow-Wow. I prefer barking.
我希望您现在对如何实现工厂方法模式有了更好的理解。提供两个修改的实现作为参考。(Apress 网站上提供了完整的实现。).您可以决定是否要在您的程序中使用这些修改中的一个(或两个)。但是您应该记住,工厂方法应该在幕后为客户机创建适当的对象,这是该模式的终极座右铭。
五、抽象工厂模式
本章涵盖了抽象工厂模式。
GoF 定义
提供创建相关或依赖对象系列的接口,而无需指定它们的具体类。
Note
如果你理解简单工厂模式(第章第二十四部分)和工厂方法模式(第章第四部分),抽象工厂模式对你来说会更有意义。简单工厂模式并不直接属于四人组设计模式,所以对该模式的讨论出现在本书的第二部分。我建议你先阅读第四章和第二十四章,然后再开始阅读这一章。
概念
抽象工厂通常被称为工厂中的工厂。这种模式提供了一种封装一组具有共同主题的单个工厂的方法。在这个过程中,你不直接实例化一个类;相反,您实例化一个具体的工厂,然后使用该工厂创建产品。
在我们接下来的例子中,实例化了一个工厂实例(animalFactory
)。通过使用这个工厂实例,我创建了狗和老虎实例(狗和老虎是最终产品),这就是为什么您会在客户端代码中看到下面的代码段。
// Making a wild dog and wild tiger through WildAnimalFactory
IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
IDog dog = animalFactory.GetDog();
ITiger tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();
当产品相似,但产品系列不同时,这种模式最适合(例如,家养的狗和野生的狗很不一样)。这种模式有助于您交换特定的实现,而无需更改使用它们的代码,甚至在运行时也是如此。但是,这可能会导致不必要的复杂性和额外的工作。在某些情况下,甚至调试也变得很困难。
真实世界的例子
假设你用两种不同类型的桌子装饰你的房间;一个是木制的,另一个是钢制的。对于木制的,你需要拜访一个木匠,对于其他类型的,你可能需要去一个金属商店。所有这些都是桌子工厂。所以,基于需求,你决定你需要什么样的工厂。
计算机世界的例子
ADO.NET 实现了类似的概念来建立到数据库的连接。
履行
维基百科描述了这种模式的典型结构,类似于图 5-1 (见 https://en.wikipedia.org/wiki/Abstract_factory_pattern
)所示。
图 5-1
抽象工厂模式
在本章的实现中,我遵循类似的结构。在这个例子中,有两种动物:宠物和野生动物。Program.cs
客户正在寻找一些动物(在本例中是野狗、宠物狗、野生老虎和宠物老虎)。在这个实现中,您将探索宠物和野生动物的构造过程。
IAnimalFactory
是一个抽象工厂。名为WildAnimalFactory
和PetAnimalFactory
的两个具体工厂继承自这个抽象工厂。你可以看到这些混凝土工厂负责创造狗和老虎的混凝土产品。顾名思义,WildAnimalFactory
创造野生动物(野狗、野虎),而PetAnimalFactory
创造宠物(宠物狗、宠物虎)。下面总结了参与者及其角色。
-
IAnimalFactory
:抽象工厂 -
WildAnimalFactory
:实现IAnimalFactory
的混凝土工厂;它创造了野狗和野生老虎 -
PetAnimalFactory
:实现IAnimalFactory,
的混凝土工厂,但是这个工厂制造宠物狗和宠物老虎 -
ITiger
和IDog
:抽象产品 -
PetTiger
、PetDog
、WildTiger
、WildDog
:混凝土制品。PetTiger
和WildTiger
实现ITiger
接口。PetDog
和WildDog
实现IDog
接口。IDog
和ITiger
接口只有一个方法AboutMe()
,在简单工厂模式和工厂方法模式中都使用。 -
客户端代码中使用了一个名为
FactoryProvider
的静态类,如下所示:// Making a wild dog and wild tiger through // WildAnimalFactory IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild"); IDog dog = animalFactory.GetDog(); ITiger tiger = animalFactory.GetTiger(); dog.AboutMe(); tiger.AboutMe();
-
从前面代码段中的粗线可以看出,我是而不是直接实例化工厂实例;相反,我使用
FactoryProvider
静态类来获取工厂实例。(这个类的结构类似于在工厂方法模式中使用具体工厂时的结构。)FactoryProvider
根据在GetAnimalFactory(...)
方法中传递的参数提供合适的工厂。
类图
图 5-2 显示了类图。
图 5-2
类图
解决方案资源管理器视图
图 5-3 显示了程序的高层结构。
图 5-3
解决方案资源管理器视图
演示 1
这是完整的程序。
using System;
namespace AbstractFactoryPattern
{
// Abstract Factory
public interface IAnimalFactory
{
IDog GetDog();
ITiger GetTiger();
}
// Abstract Product-1
public interface ITiger
{
void AboutMe();
}
// Abstract Product-2
public interface IDog
{
void AboutMe();
}
// Concrete product-A1(WildTiger)
class WildTiger : ITiger
{
public void AboutMe()
{
Console.WriteLine("Wild tiger says: I prefer hunting in jungles. Halum.");
}
}
// Concrete product-B1(WildDog)
class WildDog : IDog
{
public void AboutMe()
{
Console.WriteLine("Wild dog says: I prefer to roam freely in jungles. Bow-Wow.");
}
}
// Concrete product-A2(PetTiger)
class PetTiger : ITiger
{
public void AboutMe()
{
Console.WriteLine("Pet tiger says: Halum. I play in an animal circus.");
}
}
// Concrete product-B2(PetDog)
class PetDog : IDog
{
public void AboutMe()
{
Console.WriteLine("Pet dog says: Bow-Wow. I prefer to stay at home.");
}
}
// Concrete Factory 1-Wild Animal Factory
public class WildAnimalFactory : IAnimalFactory
{
public ITiger GetTiger()
{
return new WildTiger();
}
public IDog GetDog()
{
return new WildDog();
}
}
// Concrete Factory 2-Pet Animal Factory
public class PetAnimalFactory : IAnimalFactory
{
public IDog GetDog()
{
return new PetDog();
}
public ITiger GetTiger()
{
return new PetTiger();
}
}
// Factory provider
class FactoryProvider
{
public static IAnimalFactory GetAnimalFactory(string factoryType)
{
if (factoryType.Contains("wild"))
{
// Returning a WildAnimalFactory
return new WildAnimalFactory();
}
else
if (factoryType.Contains("pet"))
{
// Returning a PetAnimalFactory
return new PetAnimalFactory();
}
else
{
throw new ArgumentException("You need to pass either wild or pet as argument.");
}
}
}
// Client
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Abstract Factory Pattern Demo.***\n"); // Making a wild dog and wild tiger through WildAnimalFactory
IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
IDog dog = animalFactory.GetDog();
ITiger tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();
Console.WriteLine("******************");
// Making a pet dog and pet tiger through PetAnimalFactory now.
animalFactory = FactoryProvider.GetAnimalFactory("pet");
dog = animalFactory.GetDog();
tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();
Console.ReadLine();
}
}
}
输出
这是输出。
***Abstract Factory Pattern Demo.***
Wild dog says: I prefer to roam freely in jungles. Bow-Wow.
Wild tiger says: I prefer hunting in jungles. Halum.
******************
Pet dog says: Bow-Wow.I prefer to stay at home.
Pet tiger says: Halum.I play in an animal circus.
问答环节
5.1IDog
和 ITiger
接口 都包含同名的方法。例如,两个接口都包含 AboutMe()
方法 。这是强制性的吗?
不,你可以为你的方法使用不同的名字。此外,这些接口中方法的数量可以不同。然而,在第二十四章中,我介绍了简单工厂模式,在第四章中,我介绍了工厂方法模式。在这一章中,我继续例子,这就是为什么我保持同样的方法。
使用这样一个抽象工厂有哪些挑战?
**抽象工厂中的任何变化都会迫使您将修改传播到具体工厂。标准设计理念建议你对接口编程,而不是对实现编程。这是开发人员应该始终牢记的关键原则之一。在大多数情况下,开发人员不想改变他们的抽象工厂。
此外,整体架构很复杂,这就是为什么在某些情况下调试非常具有挑战性。
如何区分简单工厂模式 和工厂方法模式或者抽象工厂模式?
我将在第四章的“问答”部分讨论简单工厂模式和工厂方法模式的区别。
让我们修改一下客户端代码如何使用这些工厂,如下图所示。下面是简单工厂模式的代码片段。
IAnimal preferredType = null;
SimpleFactory simpleFactory = new SimpleFactory();
#region The code region that can vary based on users preference
/*
* Since this part may vary, we're moving the
* part to CreateAnimal() in SimpleFactory class.
*/
preferredType = simpleFactory.CreateAnimal();
#endregion
#region The codes that do not change frequently.
preferredType.AboutMe();
#endregion
图 5-4 显示了简单的工厂模式。
图 5-4
简单工厂模式
下面是工厂方法模式的代码片段。
// Creating a Tiger Factory
AnimalFactory tigerFactory = new TigerFactory();
// Creating a tiger using the Factory Method
IAnimal tiger = tigerFactory.CreateAnimal();
tiger.AboutMe();
// Creating a DogFactory
AnimalFactory dogFactory = new DogFactory();
// Creating a dog using the Factory Method
IAnimal dog = dogFactory.CreateAnimal();
dog.AboutMe();
图 5-5 显示了工厂方法模式。
图 5-5
工厂方法模式
下面是抽象工厂模式的代码片段。
// Making a wild dog and wild tiger through WildAnimalFactory
IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
IDog dog = animalFactory.GetDog();
ITiger tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();
Console.WriteLine("******************");
// Making a pet dog and pet tiger through PetAnimalFactory now.
animalFactory = FactoryProvider.GetAnimalFactory("pet");
dog = animalFactory.GetDog();
tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();
图 5-6 显示了抽象工厂模式。
图 5-6
抽象工厂模式
简而言之,使用简单工厂模式,您可以将不同于其他代码的代码分离出来(基本上,您可以将客户端代码解耦)。这种方法有助于您更轻松地管理代码。这种方法的另一个主要优点是客户端不知道对象是如何创建的。因此,它同时促进了安全性和抽象性。
但是,这种方法会违反开闭原则。使用工厂方法模式可以克服这个缺点,工厂方法模式允许子类决定如何完成实例化过程。简而言之,您将对象创建委托给实现工厂方法来创建对象的子类。
抽象工厂基本上是工厂的工厂。它创建了一系列相关的对象,但是它不依赖于具体的类。在这个模式中,您封装了一组具有共同主题的独立工厂。在这个过程中,你不直接实例化一个类;取而代之的是,您得到一个具体的工厂(我为此使用了一个提供者),然后,使用该工厂创建产品。
最后,我试图保持例子简单。工厂方法促进继承,它的子类需要实现工厂方法来创建对象。抽象工厂模式可以通过使用工厂接口中公开的方法创建相关对象来促进对象组合。最后,所有的工厂都通过减少对具体类的依赖来促进松散耦合。**
六、代理模式
本章介绍代理模式。
GoF 定义
为另一个对象提供代理或占位符,以控制对它的访问。
概念
您需要支持这种设计,因为在许多情况下,与原始对象的直接通信并不总是可能的。这是由许多因素造成的,包括安全性和性能问题、资源限制、最终产品处于开发阶段等等。代理可以是不同的类型,但从根本上说,它是原始对象的替代物(或占位符)。因此,当客户端与代理对象交互时,看起来它是在直接与实际对象对话。因此,使用这种模式,您可能希望使用一个可以作为原始类的接口的类。
真实世界的例子
在教室里,当一个学生缺席时,他最好的朋友可能会在点名时试图模仿他的声音,让老师认为他的朋友在那里。除了这个例子,您还可以考虑另一个领域的例子,例如,ATM。ATM 实现可以保存远程服务器上的银行信息的代理对象。
计算机世界的例子
在真实的编程世界中,创建一个复杂对象的多个实例可能成本很高,因为您可能需要不容易获得或分配的资源。在这种情况下,您可以创建多个可以指向原始对象的代理对象。这种机制可以帮助您节省计算机/系统内存并提高应用的性能。
代理的另一个常见用途是当用户不想公开他/她的机器的真实 IP 地址并使其匿名时。
在 WCF 应用中,您可能会注意到 WCF 客户端代理,客户端应用使用它与服务进行通信。您还可以配置一个 REST API 在代理服务器后面工作,以促进授权的通信。
履行
在这个程序中,Subject
是一个抽象类,它有一个名为DoSomeWork()
.
的抽象方法,如下所示。
public abstract class Subject
{
public abstract void DoSomeWork();
}
ConcreteSubject
是一个继承自Subject
的具体类,完成了DoSomeWork()
方法。所以,看起来是这样的。
public class ConcreteSubject : Subject
{
public override void DoSomeWork()
{
Console.WriteLine("I've processed your request.");
}
}
让我们假设您想要限制客户端直接调用ConcreteSubject
中的方法。(考虑一下计算机世界例子中讨论的案例,这背后有一些原因。)所以,你做了一个名为Proxy
的代理类。在我们的实现中,Proxy
类还包含一个名为DoSomeWork()
的方法,客户端可以通过一个Proxy
实例来使用这个方法。当客户端调用代理对象的DoSomeWork()
方法时,这个调用又被传播到ConcreteSubject
对象中的DoSomeWork()
方法。这让客户感觉好像他们直接调用了来自ConcreteSubject
的方法,这就是为什么Proxy
类看起来像下面这样。
public class Proxy : Subject
{
Subject subject;
public override void DoSomeWork()
{
Console.WriteLine("Welcome, my client.");
/*
Lazy initialization:We'll not instantiate until
the method is called.
*/
if (subject == null)
{
subject = new ConcreteSubject();
}
subject.DoSomeWork();
}
}
类图
图 6-1 为类图。
图 6-1
类图
解决方案资源管理器视图
图 6-2 显示了程序的高层结构。(注意,您可以将代理类分离到一个不同的文件中,但是由于本例中的各个部分都很小,所以我将所有内容都放在一个文件中。同样的评论也适用于本书中的其他程序。)
图 6-2
解决方案资源管理器视图
演示 1
下面是完整的实现。
using System;
namespace ProxyPatternDemo
{
/// <summary>
/// Abstract class Subject
/// </summary>
public abstract class Subject
{
public abstract void DoSomeWork();
}
/// <summary>
/// ConcreteSubject class
/// </summary>
public class ConcreteSubject : Subject
{
public override void DoSomeWork()
{
Console.WriteLine("I've processed your request.");
}
}
/// <summary>
/// Proxy class
/// </summary>
public class Proxy : Subject
{
Subject subject;
public override void DoSomeWork()
{
Console.WriteLine("Welcome, my client.");
/*
Lazy initialization:We'll not instantiate the object until the method is called.
*/
if (subject == null)
{
subject = new ConcreteSubject();
}
subject.DoSomeWork();
}
}
/// <summary>
/// Client class
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Proxy Pattern Demo.***\n");
Subject proxy = new Proxy();
proxy.DoSomeWork();
Console.ReadKey();
}
}
}
输出
这是输出。
***Proxy Pattern Demo.***
Welcome, my client.
I've processed your request.
问答环节
6.1 有哪些不同类型的代理人 ?
这些是代理的常见类型。
-
远程代理:这些代理可以隐藏位于不同地址空间的对象。
-
虚拟代理:这些代理执行优化技术,比如按需创建一个重对象。
-
保护代理:这些代理一般处理不同的访问权限。
-
智能引用:当客户端访问对象时执行额外的内务处理。典型的操作可以包括计算在某一时刻对对象的引用次数。
6.2 您可以在代理类构造函数中创建 ConcreteSubject
实例 ,如下所示。
/// <summary>
/// Proxy class
/// </summary>
public class Proxy : Subject
{
Subject subject;
public Proxy()
{
// Instantiating inside the constructor
subject = new ConcreteSubject();
}
public override void DoSomeWork()
{
Console.WriteLine("Proxy call happening now..");
cs.DoSomeWork();
}
}
这是正确的吗?
是的,你可以这样做。但是不要忘记代理类可以有不依赖于ConcreteSubject
的额外方法。因此,如果您需要来自代理类的这些方法,并且遵循您提出的设计,无论何时您实例化一个代理对象,您也实例化了一个ConcreteSubject
类的对象。因此,这最终可能会创建不必要的对象。
6.3 使用这个 惰性实例化过程 ,你可能会在多线程应用中创建不必要的对象。这是正确的吗?
是的。这是一个简单的插图,让您了解实际模式背后的核心思想。在第一章对单例模式的讨论中,我们分析了一些告诉你如何在多线程环境中工作的替代方法。在这种情况下,你可以参考这些讨论。(例如,在这种情况下,您可以实现一个智能代理,以确保在授予对象访问权限之前锁定该对象)。
6.4 能否举一个 远程代理 的例子?
假设您想要调用一个对象的方法,但是该对象正在不同的地址空间中运行(例如,在不同的位置或不同的计算机上)。你是如何进行的?在远程代理的帮助下,您可以调用代理对象上的方法,该方法又将调用转发给远程计算机上运行的实际对象。(如果实际的方法存在于不同的计算机上,并且您通过网络上的代理对象连接到它,那么演示 1 就是这种情况下的一个例子。)这种类型的需求可以通过不同的公知机制来实现,例如 ASP.NET、CORBA、COM/DCOM 或 Java 的 RMI。在 C# 应用中,您可以使用 WCF(.NET Framework 版及更高版本)或。NET web 服务/remoting(主要用于早期版本)。值得注意的是。NET remoting 不被支持。NET Core,微软未来也不打算增加这种支持(见 https://docs.microsoft.com/en-us/dotnet/core/porting/net-framework-tech-unavailable#:~:text=NET%20Remoting%20isn't%20supported
、IO)。
图 6-3 显示了一个简单的远程代理结构。
图 6-3
一个简单的远程代理图
6.5 你什么时候使用 虚拟代理 ?
虚拟代理保护内存不被分配给对象。如果实际的对象创建是一个昂贵的操作,您可以创建一个包含最重要细节的目标对象的简单副本,并将其提供给用户。昂贵的对象只有在真正需要的时候才会被创建。例如,您可以使用这个概念来避免加载不必要的超大图像,以获得更好的应用性能。
6.6 什么时候使用 保护代理 ?
在组织中,安全团队可以实施保护代理来阻止对特定网站的 Internet 访问。
考虑下面的例子,它是前面描述的代理模式实现的修改版本。为了简单起见,让我们假设只有三个注册用户可以使用DoSomeWork()
代理方法。如果一个不需要的用户(名为 Robin)试图调用该方法,系统会拒绝他的访问请求。当系统拒绝这种不需要的访问时,创建代理对象就没有意义了。在接下来的例子中,这些注册用户在代理类构造函数中初始化,但是我避免了在其中实例化一个ConcreteSubject
对象。它帮助我避免为未授权用户创建不必要的对象。
现在让我们来看一下修改后的实现。
演示 2
下面是修改后的实现。
using System;
using System.Linq; // For Contains() method below
namespace ProxyPatternQAs
{
/// <summary>
/// Abstract class Subject
/// </summary>
public abstract class Subject
{
public abstract void DoSomeWork();
}
/// <summary>
/// ConcreteSubject class
/// </summary>
public class ConcreteSubject : Subject
{
public override void DoSomeWork()
{
Console.WriteLine("I've processed your request.\n");
}
}
/// <summary>
/// Proxy class
/// </summary>
public class Proxy : Subject
{
Subject subject;
string[] registeredUsers;
string currentUser;
public Proxy(string currentUser)
{
/*
* Avoiding to instantiate ConcreteSubject
* inside the Proxy class constructor.
*/
//subject = new ConcreteSubject();
// Registered users
registeredUsers = new string[] { "Admin", "Rohit", "Sam" };
this.currentUser = currentUser;
}
public override void DoSomeWork()
{
Console.WriteLine($"{currentUser} wants to access into the system.");
if (registeredUsers.Contains(currentUser))
{
Console.WriteLine($"Welcome, {currentUser}.");
/* Lazy initialization: We'll not instantiate until the method is called through an authorized user. *.
if (subject == null)
{
subject = new ConcreteSubject();
}
subject.DoSomeWork();
}
else
{
Console.WriteLine($"Sorry {currentUser}, you do not have access into the system.");
}
}
}
/// <summary>
/// Client
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Proxy Pattern Demo2.***\n");
// Authorized user-Admin
Subject proxy = new Proxy("Admin");
proxy.DoSomeWork();
// Authorized user-Sam
proxy = new Proxy("Sam");
proxy.DoSomeWork();
// Unauthorized User-Robin
proxy = new Proxy("Robin");
proxy.DoSomeWork();
Console.ReadKey();
}
}
}
输出
这是修改后的输出。
***Proxy Pattern Demo2.***
Admin wants to access into the system.
Welcome, Admin.
I've processed your request.
Sam wants to access into the system.
Welcome, Sam.
I've processed your request.
Robin wants to access into the system.
Sorry Robin, you do not have access into the system.
看起来代理人就像装饰者一样(见第七章)。这是正确的吗?
有时候代理实现和装饰器有一些相似之处,但是你不应该忘记代理的真正意图。装饰者专注于增加职责,而代理者专注于控制对对象的访问。所以,如果你记得它们的用途,在大多数情况下,你可以区分代理和装饰者。
什么时候我应该考虑设计一个代理?
下面是代理可以帮助您的一些重要用例。
-
您正在为一个仍处于开发阶段或很难重现的场景编写测试用例。例如,当您想要评估应用中只能在客户环境中看到的行为,但您也认识到当应用正在运行时,获得该行为的概率非常低。在这种情况下,您可以在您的代理对象中模拟客户环境行为,并执行您的测试用例来评估该行为的正确性。您不希望您的客户端直接与目标对象对话。
-
你想隐藏复杂性,增强系统的安全性。
6.9 与代理相关的 缺点有哪些?
使用这种模式时,您应该记住以下一些因素。
-
总的响应时间可能是一个问题,因为您没有直接与实际的对象对话。
-
您需要为代理维护额外的层。
-
代理可以隐藏对象的实际响应,这在某些情况下可能会造成混乱。
七、装饰模式
本章涵盖了装饰模式。
GoF 定义
动态地将附加责任附加到对象上。Decorators 为扩展功能提供了子类化的灵活替代方案。
概念
从 GoF 定义来看,很明显这种模式使用了子类化的替代方法(即继承)。如果不允许继承,怎么进行?是的,你猜对了。它规定你用合成代替继承。
通过遵循 SOLID 原则,这种模式推广了这样一个概念,即类对修改是封闭的,但对扩展是开放的。(如果你想了解更多坚实的原理,去 https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)
。)使用这种模式,您可以在不改变底层类的情况下向特定对象添加特殊功能。
装饰器就像一个包装器(或顶层),包裹着原始对象,并为其添加额外的功能。这就是装饰模式也被称为包装模式的原因。当您动态添加装饰者时,这种模式是最有效的。由于 decorators 通常是动态添加的,所以如果您不希望在开发的后期阶段使用它们,这完全没问题,因为原始对象仍然可以工作。
真实世界的例子
假设你有一栋单层的房子,你决定在上面加建一层。您可能不希望更改底层的架构,但是您可能希望为新添加的楼层采用一种新的设计,以适合现有架构的顶部。
图 7-1 、 7-2 和 7-3 说明了这个概念。
图 7-3
在已有装饰者的基础上再加一个装饰者,并修改房子(现在粉刷房子)
图 7-2
有装修工的原始房屋(附加楼层建在原始结构的顶部)
图 7-1
原始房屋
Note
图 7-3 所示的情况是可选的。您可以使用现有的 decorator 对象来增强行为,或者您可以创建一个新的 decorator 对象并将新的行为添加到其中。在第二步中,你也可以直接油漆原来的房子。一旦添加了新地板,您就不需要开始粉刷了。
计算机世界的例子
假设您想在基于 GUI 的工具包中添加边框属性。您可以使用继承来做到这一点,但这不能被视为最终的解决方案,因为您可能无法从一开始就对所有事情拥有绝对的控制权。因此,这种技术本质上是静态的。
在这种情况下,装饰者可以为你提供一种灵活的方法。他们推广动态选择的概念。例如,您可以将组件包装在另一个对象中(类似于图 7-2 和 7-3 )。封闭对象被称为装饰器,它必须符合它所装饰的组件的接口。它将请求转发给原始组件,并可以在这些请求之前或之后执行附加操作。事实上,这个概念允许您添加无限数量的责任。
履行
在这个例子中,涉及五个玩家:AbstractHome, ConcreteHome, AbstractDecorator, FloorDecorator
和PaintDecorator
。
AbstractHome
定义如下。
abstract class AbstractHome
{
public double AdditionalPrice { get; set; }
public abstract void MakeHome();
}
AbstractHome
的具体实现者必须实现MakeHome()
方法。除此之外,您可以通过使用AdditionalPrice
属性来设置价格。这就是为什么从AbstractHome,
继承的一个叫ConcreteHome
的具体类完成了原来的结构,看起来像下面这样(我假设家一旦建好,不需要立即修改;所以,AdditionalPrice
初始设置为 0)。
class ConcreteHome : AbstractHome
{
public ConcreteHome()
{
AdditionalPrice = 0;
}
public override void MakeHome()
{
Console.WriteLine($"Original House is constructed.Price for this 10000$");
}
}
此时,你可以选择在现有的房屋上加建一层,或者你可以粉刷房屋,或者两者兼而有之。于是,FloorDecorator
和PaintDecorator
both
出现了。虽然并不严格要求共享公共代码,但是两个装饰器都继承了AbstractDecorator
,它具有以下结构。
abstract class AbstractDecorator : AbstractHome
{
protected AbstractHome home;
public AbstractDecorator(AbstractHome home)
{
this.home = home;
this.AdditionalPrice = 0;
}
public override void MakeHome()
{
home.MakeHome();
}
}
请注意,AbstractDecorator
保存了对AbstractHome
的引用。因此,具体的装饰者(本例中的FloorDecorator
或PaintDecorator
)正在装饰AbstractHome
的一个实例。
现在我们来看一个混凝土装饰工的结构,FloorDecorator
,如下。
// Floor Decorator used to add a floor
class FloorDecorator : AbstractDecorator
{
public FloorDecorator(AbstractHome home) : base(home)
{
this.AdditionalPrice = 2500;
}
public override void MakeHome()
{
base.MakeHome();
// Adding a floor on top of original house.
AddFloor();
}
private void AddFloor()
{
Console.WriteLine($"-Additional Floor added.Pay additional {AdditionalPrice}$ for it .");
}
}
你可以看到FloorDecorator
可以加一层楼(使用AddFloor()
的方法),使用的时候必须额外支付 2500 美元的额外建设费用。更重要的是,在添加楼层之前,它调用了AbstractHome
类的MakeHome()
方法,后者又从AbstractHome
(即ConcreteHome
)的一个具体实现中调用了MakeHome()
方法。
类似的行为,但你要为此付出更多。(是的,我假设你正在为你的家使用奢华的油漆。)
类图
图 7-4 显示了类图中最重要的部分。
图 7-4
类图。这里没有显示客户端类。
解决方案资源管理器视图
图 7-5 显示了程序的高层结构。
图 7-5
解决方案资源管理器视图
示范
下面是完整的实现,它测试了两个场景(用#region 标记)。在场景 1 中,我在现有房屋上添加了一层,然后对其进行了粉刷。在场景 2 中,我粉刷了原来的家,然后在现有建筑的顶部添加了两层。
using System;
namespace DecoratorPatternDemo
{
abstract class AbstractHome
{
public double AdditionalPrice { get; set; }
public abstract void MakeHome();
}
class ConcreteHome : AbstractHome
{
public ConcreteHome()
{
AdditionalPrice = 0;
}
public override void MakeHome()
{
Console.WriteLine($"Original House is constructed.Price for this $10000");
}
}
abstract class AbstractDecorator : AbstractHome
{
protected AbstractHome home;
public AbstractDecorator(AbstractHome home)
{
this.home = home;
this.AdditionalPrice = 0;
}
public override void MakeHome()
{
home.MakeHome();//Delegating task
}
}
// Floor Decorator is used to add a floor
class FloorDecorator : AbstractDecorator
{
public FloorDecorator(AbstractHome home) : base(home)
{
//this.home = home;
this.AdditionalPrice = 2500;
}
public override void MakeHome()
{
base.MakeHome();
// Adding a floor on top of original house.
AddFloor();
}
private void AddFloor()
{
Console.WriteLine($"-Additional Floor added.Pay additional ${AdditionalPrice} for it .");
}
}
// Paint Decorator used to paint the home.
class PaintDecorator : AbstractDecorator
{
public PaintDecorator(AbstractHome home):base(home)
{
//this.home = home;
this.AdditionalPrice = 5000;
}
public override void MakeHome()
{
base.MakeHome();
// Painting home.
PaintHome();
}
private void PaintHome()
{
Console.WriteLine($"--Painting done.Pay additional ${AdditionalPrice} for it .");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Decorator pattern Demo***\n");
#region Scenario-1
Console.WriteLine("\n**Scenario-1:");
Console.WriteLine("**Building home.Adding floor and then painting it.**");
AbstractHome home = new ConcreteHome();
Console.WriteLine("Current bill breakups are as follows:");
home.MakeHome();
// Applying a decorator
// Adding a floor
home = new FloorDecorator(home);
Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
home.MakeHome();
// Working on top of the previous decorator.
// Painting the home
home = new PaintDecorator(home);
Console.WriteLine("\nPaint applied.Current bill breakups are as follows:");
home.MakeHome();
#endregion
#region Scenario-2
Console.WriteLine("\n**Scenario-2:");
Console.WriteLine("**Building home,painting it and then adding two additional floors on top of it.**");
// Fresh start once again.
home = new ConcreteHome();
Console.WriteLine("\nGoing back to original home.Current bill breakups are as follows:");
home.MakeHome();
// Applying paint on original home.
home = new PaintDecorator(home);
Console.WriteLine("\nPaint applied.Current bill breakups are as follows:");
home.MakeHome();
// Adding a floor on the painted home.
home = new FloorDecorator(home);
Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
home.MakeHome();
// Adding another floor on the current home.
home = new FloorDecorator(home);
Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
home.MakeHome();
#endregion
Console.ReadKey();
}
}
}
输出
***Decorator pattern Demo***
**Scenario-1:
**Building home. Adding floor and then painting it.**
Current bill breakups are as follows:
Original House is constructed. Price for this $10000
Floor added. Current bill breakups are as follows:
Original House is constructed.Price for this $10000
-Additional Floor added.Pay additional $2500 for it.
Paint applied. Current bill breakups are as follows:
Original House is constructed.Price for this $10000
-Additional Floor added. Pay additional $2500 for it.
--Painting done. Pay additional $5000 for it.
**Scenario-2:
**Building home, painting it and then adding two additional floors on top of it.**
Going back to original home. Current bill breakups are as follows:
Original House is constructed. Price for this $10000
Paint applied. Current bill breakups are as follows:
Original House is constructed. Price for this $10000
--Painting done. Pay additional $5000 for it.
Floor added.Current bill breakups are as follows:
Original House is constructed.Price for this $10000
--Painting done.Pay additional $5000 for it.
-Additional Floor added. Pay additional $2500 for it.
Floor added.Current bill breakups are as follows:
Original House is constructed. Price for this $10000
--Painting done.Pay additional $5000 for it.
-Additional Floor added.Pay additional $2500 for it.
-Additional Floor added.Pay additional $2500 for it.
问答环节
7.1 你能解释一下合成是如何促进一种继承所不能的动态行为的吗?
当派生类从基类继承时,它只继承基类当时的行为。尽管不同的子类可以以不同的方式扩展基类或父类,但这种类型的绑定在编译时是已知的。所以这个方法是静态的。但是通过使用组合的概念,就像前面的例子一样,您可以获得动态行为。
当你设计一个父类时,你可能没有足够的洞察力去了解你的客户在以后的某个阶段可能需要什么样的额外职责。由于约束是您不能修改现有的代码,在这种情况下,对象组合不仅远远超过继承,而且还确保您不会在旧的架构中引入错误。
最后,在这种情况下,你必须记住一个关键的设计原则,即类应该对扩展开放,但对修改关闭。
使用装潢师的关键 优势 是什么?
以下是一些关键优势。
-
现有的结构是原封不动的,所以你不能在那里引入错误。
-
新的功能可以很容易地添加到现有的对象。
-
你不仅可以在界面上添加行为,还可以改变行为。
-
您不需要立即预测/实现所有支持的功能(例如,在初始设计阶段)。你可以增量开发。例如,您可以逐个添加装饰器对象来支持您的需求。你必须承认,如果你先创建一个复杂的类,然后想扩展它的功能,这将是一个乏味的过程。
7.3 整体设计模式与 传承 有何不同?
您可以通过简单地附加或分离 decorators 来添加、改变或删除职责。但是使用简单的继承技术,您需要为新的职责创建新的类。所以,你最终可能会得到一个复杂的系统。
再次考虑这个例子。假设你想加一层新地板,粉刷房子,做一些额外的工作。为了满足这一需求,您可以从FloorDecorator
开始,因为它已经提供了添加地板的支持,然后使用PaintDecorator
来粉刷房子。然后,您需要添加一个简单的包装器来完成这些额外的职责。
但是如果你从继承开始,然后你可能有多个子类;比如一个加层,一个粉刷房子,如图 7-6 (一个层次继承)。
图 7-6
等级继承
因此,如果你需要一个带有额外功能的额外油漆地板,你可能需要最终得到如图 7-7 所示的设计。
图 7-7
一个类(额外特性)需要从多个基类继承
现在你感受到了“钻石效应”的热度,因为在许多编程语言中,包括 C#,多个基类是不允许的。
您还会发现,与装饰模式相比,继承机制不仅更具挑战性和耗时,而且可能会在应用中产生重复代码。最后,不要忘记继承只促进编译时绑定(不是动态绑定)。
7.4 你为什么要创建一个职责单一的类?你可以创建一个子类,简单地添加一个地板,然后进行绘画。在这种情况下,您可能会得到更少的子类。这是正确的吗?
如果你熟悉固体原理,你就知道有一个原理叫单责。这个原则背后的思想是,每个类应该对软件中提供的功能的一个部分负责。当您使用单一责任原则时,装饰模式是有效的,因为您可以简单地动态添加或删除责任。
7.5 与此模式相关的 缺点 有哪些?
我相信如果你小心的话,没有显著的缺点。但是如果你在系统中创建了太多的装饰器,那么维护和调试将会很困难。所以,在这种情况下,他们会制造不必要的混乱。
7.6 例子中的 AbstractDecorator 类 是抽象的,但是里面没有抽象方法。这怎么可能?
在 C# 中,一个类可以是抽象的,而不包含抽象方法,但反之则不然。换句话说,如果一个类包含至少一个抽象方法,就意味着这个类是不完整的,你被迫用abstract
关键字来标记它。
同样,如果你阅读了图 7-8 中的注释,你正在将任务委托给一个具体的装饰者,在这种情况下,因为你只想使用和实例化具体的装饰者。
图 7-8
抽象类:AbstractDecorator
因此,在这个例子中,您不能简单地实例化一个AbstractDecorator
实例,因为它用abstract
关键字标记。
下面一行创建了一个编译错误。
AbstractDecorator abstractDecorator = new AbstractDecorator();
saying “CS0144 Cannot create an instance of the abstract class or interface 'AbstractDecorator'”
7.7 装饰器是否只用于 动态绑定 ?
不。您可以将这个概念用于静态和动态绑定。但是动态绑定是它的强项,所以我在这里集中讨论这一点。GoF 定义也只关注动态绑定。
Note
中的 I/O 流实现。NET 框架,。NET 核心,Java 使用装饰模式。例如,BufferedStream
类继承自Stream
类。注意这个类中存在两个重载的构造函数;它们每个都以一个Stream
(父类)作为参数(就像演示 1 一样)。当您看到这种结构时,您可能会看到装饰模式的一个例子。BufferedStream
在. NET 中表现得像个装潢师。
八、适配器模式
本章介绍适配器模式。
GoF 定义
将一个类的接口转换成另一个客户期望的接口。适配器允许类一起工作,否则由于不兼容的接口而不能。
概念
从 GoF 定义中,您可以猜测这种模式处理至少两个不兼容的继承层次结构。在特定领域的系统中,客户习惯于如何调用软件中的方法。这些方法可以遵循继承层次结构。现在假设您需要升级系统,并且需要实现一个新的继承层次结构。当你这样做的时候,你不想强迫你的客户学习访问软件的新方法。那么,你能做什么?解决方案很简单:编写一个适配器,接受客户机请求,并以新层次结构中的方法可以理解的形式翻译这些请求。因此,客户可以享受更新的软件,没有任何麻烦。
下面的例子也可以帮助你更好地理解这些模式。
真实世界的例子
这种模式的一个常见应用是在国际旅行中使用电源插座适配器/交流电源适配器。这些适配器可以充当中间人,以便电子设备(如接受美国电源的笔记本电脑)可以插入欧洲电源插座。
考虑另一个例子。假设你需要给手机充电。但是你看到电源插座和你的充电器不兼容。在这种情况下,您可能需要使用适配器。即使是一个将一种语言转换成另一种语言的译者在现实生活中也遵循这种模式。
让我们考虑这样一种情况,您有两个不同的形状(例如,形状 1 和形状 2),它们都不是矩形,它们看起来像图 8-1 。
图 8-1
在使用适配器之前
让我们进一步假设,将这两种不同的形状结合起来,你需要形成一个矩形。你是如何进行的?一个简单的解决方法是再带一个有界的 X 形图形(填充颜色),如图 8-2 所示。
图 8-2
电源适配器
然后贴上三个形状,如图 8-3 所示。
图 8-3
使用适配器后
在编程中,你可以把 Shape1 和 Shape2 想象成两个不同的接口,除非你用这个 X 形的图形把它们组合起来形成一个矩形,否则它们是不能一起工作的。在这个场景中,X 形图形扮演着适配器的角色。
计算机世界的例子
假设您有一个应用,可以大致分为两部分:用户界面(UI 或前端)和数据库(后端)。通过用户界面,客户端可以传递一些特定类型的数据或对象。您的数据库与那些对象兼容,可以顺利地存储它们。随着时间的推移,你可能会意识到你需要升级你的软件来让你的客户满意。因此,您可能希望允许一些其他类型的对象也通过 UI。但是在这种情况下,第一个问题来自您的数据库,因为它不能存储这些新类型的对象。在这种情况下,可以使用适配器将这些新对象转换成现有数据库可以接受和存储的兼容形式。
履行
在接下来的例子中,有两个层次结构:一个用于Rectangle
,一个用于Triangle
。IRectangle
接口有两个方法叫做CalculateArea()
和AboutMe()
。Rectangle
类实现了IRectangle
接口,并形成了第一个层次,如下所示。
class Rectangle : IRectangle
{
double length;
public double width;
public Rectangle(double length, double width)
{
this.length = length;
this.width = width;
}
public double CalculateArea()
{
return length * width;
}
public void AboutMe()
{
Console.WriteLine("Actually, I am a Rectangle");
}
}
ITriangle
接口有两个方法:CalculateAreaOfTriangle()
和AboutTriangle()
。Triangle
类实现了ITriangle
接口并形成了另一个层次结构,如下所示。
class Triangle : ITriangle
{
double baseLength; // base
double height; // height
public Triangle(double length, double height)
{
this.baseLength = length;
this.height = height;
}
public double CalculateAreaOfTriangle()
{
return 0.5 * baseLength * height;
}
public void AboutTriangle()
{
Console.WriteLine("Actually, I am a Triangle.");
}
}
这两个层次很容易理解。现在,让我们来看一个问题,在这个问题中,您需要使用矩形层次结构来计算三角形的面积。
你是如何进行的?您可以使用适配器来解决这个问题,如下例所示。
/*
* RectangleAdapter is implementing IRectangle.
* So, it needs to implement all the methods
* defined in the target interface.
*/
class RectangleAdapter : IRectangle
{
ITriangle triangle;
public RectangleAdapter(ITriangle triangle)
{
this.triangle = triangle;
}
public void AboutMe()
{
triangle.AboutTriangle();
}
public double CalculateArea()
{
return triangle.CalculateAreaOfTriangle();
}
}
注意使用适配器的好处。您没有对任何层次结构进行任何更改,从高层次上看,似乎通过使用IRectangle
方法,您可以计算一个三角形的面积。这是因为您在高层使用了IRectangle
接口的AboutMe()
和CalculateArea()
方法,但是在这些方法内部,您调用了ITriangle
方法。
除了这个优点,您还可以扩展使用适配器的好处。例如,假设您需要在一个应用中有大量的矩形,但是对您创建的矩形的数量有一个限制。(为了简单起见,让我们假设在一个应用中,您最多可以创建五个矩形和十个三角形,但是当应用运行时,在某些情况下,您可能需要提供十个矩形。)在这些情况下,使用这种模式,您可以使用一些行为类似矩形对象的三角形对象。怎么会?嗯,当使用适配器时,你调用的是CalculateArea(),
,但它调用的是CalculateAreaOfTriangle()
.
,所以你可以根据需要修改方法体。例如,在您的应用中,假设每个矩形对象的长度为 20 个单位,宽度为 10 个单位,而每个三角形对象的底边为 20 个单位,高度为 10 个单位。因此,每个矩形对象的面积为 2010=200 平方单位,每个三角形对象的面积为 0.520*10=100 平方单位。因此,您可以简单地将每个三角形面积乘以 2,以获得一个等效的矩形面积,并在需要矩形面积的地方替换(或使用)它。我希望这对你有意义。
最后,您需要记住,当您处理不完全相同但非常相似的对象时,这种技术最适合。
Note
在前一点的上下文中,您不应该尝试将圆形区域转换为矩形区域(或进行类似类型的转换),因为它们是不同的形状。在这个例子中,我谈论三角形和矩形是因为它们有相似之处。
类图
图 8-4 显示了程序重要部分的类图。
图 8-4
类图。这里没有显示客户端类。
解决方案资源管理器视图
图 8-5 显示了程序的高层结构。
图 8-5
解决方案资源管理器视图
演示 1
下面是实现。
using System;
namespace AdapterPatternDemonstration
{
interface IRectangle
{
void AboutMe();
double CalculateArea();
}
class Rectangle : IRectangle
{
double length;
public double width;
public Rectangle(double length, double width)
{
this.length = length;
this.width = width;
}
public double CalculateArea()
{
return length * width;
}
public void AboutMe()
{
Console.WriteLine("Actually, I am a Rectangle");
}
}
interface ITriangle
{
void AboutTriangle();
double CalculateAreaOfTriangle();
}
class Triangle : ITriangle
{
double baseLength; // base
double height; // height
public Triangle(double length, double height)
{
this.baseLength = length;
this.height = height;
}
public double CalculateAreaOfTriangle()
{
return 0.5 * baseLength * height;
}
public void AboutTriangle()
{
Console.WriteLine("Actually, I am a Triangle.");
}
}
/*
* RectangleAdapter is implementing IRectangle.
* So, it needs to implement all the methods
* defined in the target interface.
*/
class RectangleAdapter : IRectangle
{
ITriangle triangle;
public RectangleAdapter(ITriangle triangle)
{
this.triangle = triangle;
}
public void AboutMe()
{
triangle.AboutTriangle();
}
public double CalculateArea()
{
return triangle.CalculateAreaOfTriangle();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Adapter Pattern Demo***\n");
IRectangle rectangle = new Rectangle(20, 10);
Console.WriteLine("For initial verification purposes, printing the areas of both shapes.");
Console.WriteLine("Rectangle area is:{0} Square unit", rectangle.CalculateArea());
ITriangle triangle = new Triangle(20, 10);
Console.WriteLine("Triangle area is:{0} Square unit", triangle.CalculateAreaOfTriangle());
Console.WriteLine("\nNow using the adapter.");
IRectangle adapter = new RectangleAdapter(triangle);
Console.Write("True fact : ");
adapter.AboutMe();
Console.WriteLine($" and my area is : {adapter.CalculateArea()} square unit.");
// Alternative way:
Console.WriteLine("\nUsing the adapter in a different way now.");
// Passing a Triangle instead of a Rectangle
Console.WriteLine($"Area of the triangle using the adapter is :{GetDetails(adapter)} square unit.");
Console.ReadKey();
}
/*
* The following method does not know
* that through the adapter, it can
* actually process a
* Triangle instead of a Rectangle.
*/
static double GetDetails(IRectangle rectangle)
{
rectangle.AboutMe();
return rectangle.CalculateArea();
}
}
}
输出
这是输出。
***Adapter Pattern Demo***
For initial verification purposes, printing the areas of both shapes.
Rectangle area is:200 Square unit
Triangle area is:100 Square unit
Now using the adapter.
True fact : Actually, I am a Triangle.
and my area is : 100 square unit.
Using the adapter in a different way now.
Actually, I am a Triangle.
Area of the triangle using the adapter is :100 square unit.
分析
注意下面的代码段,在Main()
方法中有注释,如下所示。
/*
* The following method does not know
* that through the adapter, it can
* actually process a
* Triangle instead of a Rectangle.
*/
static double GetDetails(IRectangle rectangle)
{
rectangle.AboutMe();
return rectangle.CalculateArea();
}
此部分是可选的。我保留它是为了向您展示在哪里可以在一次调用中调用这两个 adaptee 方法。
适配器的类型
GoF 描述了两种适配器:类适配器和对象适配器。
对象适配器
对象适配器通过对象组合进行适配,如图 8-6 所示。因此,到目前为止讨论的适配器是对象适配器的一个例子。
图 8-6
对象适配器
在我们的例子中,RectangleAdapter
是实现IRectangle (Target interface). ITriangle
的适配器,它是Adaptee
接口。适配器保存被适配器实例。
类别适配器
类适配器通过子类化来适应,并支持多重继承。但是你知道在 C# 中,不支持通过类的多重继承。(你需要接口来实现多重继承的概念。)
图 8-7 显示了支持多重继承的类适配器的典型类图。
图 8-7
类别适配器
问答环节
8.1 如何在 C# 中实现一个 类适配器设计模式 ?
您可以子类化现有的类并实现所需的接口。演示 2 向您展示了一个完整的输出示例。
演示 2
这个演示展示了一个类适配器。为了使这个例子简单明了,我只用一种方法制作了IRectangle
和ITriangle
接口。IRectangle
只有AboutMe()
方法, and the Rectangle
类实现了IRectangle
接口,这样就形成了下面的层次结构。
interface IRectangle
{
void AboutMe();
}
class Rectangle : IRectangle
{
public void AboutMe()
{
Console.WriteLine("Actually, I am a Rectangle");
}
}
ITriangle
有了AboutTriangle()
方法.``Triangle
类实现了这个接口,下面的层次结构就形成了。
interface ITriangle
{
void AboutTriangle();
}
class Triangle : ITriangle
{
public void AboutTriangle()
{
Console.WriteLine("Actually, I am a Triangle");
}
}
现在是我们的类适配器,它使用了多重继承的概念,使用了一个具体的类和一个接口。附加的注释有助于您更好地理解代码。
/*
* RectangleAdapter is implementing IRectangle.
* So, it needs to implement all the methods
* defined in the target interface.
*/
class RectangleAdapter : Triangle, IRectangle
{
public void AboutMe()
{
// Invoking the adaptee method
AboutTriangle();
}
}
现在您可以进行完整的演示,如下所示。
using System;
namespace AdapterPatternAlternativeImplementationDemo
{
interface IRectangle
{
void AboutMe();
}
class Rectangle : IRectangle
{
public void AboutMe()
{
Console.WriteLine("Actually, I am a Rectangle");
}
}
interface ITriangle
{
void AboutTriangle();
}
class Triangle : ITriangle
{
public void AboutTriangle()
{
Console.WriteLine("Actually, I am a Triangle");
}
}
/*
* RectangleAdapter is implementing IRectangle.
* So, it needs to implement all the methods
* defined in the target interface.
*/
class RectangleAdapter : Triangle, IRectangle
{
public void AboutMe()
{
// Invoking the adaptee method
AboutTriangle();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Adapter Pattern Alternative Implementation Technique Demo.***\n");
IRectangle rectangle = new Rectangle();
Console.WriteLine("For initial verification purposes, printing the details from of both shapes.");
Console.WriteLine("The rectangle.AboutMe() says:");
rectangle.AboutMe();
ITriangle triangle = new Triangle();
Console.WriteLine("The triangle.AboutTriangle() says:");
triangle.AboutTriangle();
Console.WriteLine("\nNow using the adapter.");
IRectangle adapter = new RectangleAdapter();
Console.Write("True fact : ");
adapter.AboutMe();
}
}
}
输出
这是输出。
***Adapter Pattern Alternative Implementation Technique Demo.***
For initial verification purposes, printing the details from of both shapes.
The rectangle.AboutTriangle() says:
Actually, I am a Rectangle.
The triangle.AboutTriangle() says:
Actually, I am a Triangle.
Now using the adapter.
True fact : Actually, I am a Triangle.
分析
这种方法可能不适用于所有情况。例如,您可能需要采用 C# 接口中没有指定的方法。在这些情况下,对象适配器更好。
问答环节
8.2 你更喜欢哪个——****类适配器 还是 对象适配器 ?
在大多数情况下,我更喜欢组合而不是继承。对象适配器使用组合,更加灵活。在许多情况下,当您需要从 adaptee 接口修改特定方法时,实现真正的类适配器是一项挑战,但是在目标接口中没有与之非常匹配的方法。除此之外,如果 adaptee 类(在我们的例子中是 Triangle)是密封的,那么您不能从它继承。
8.3 你说过,“……当你需要从一个被适配者接口适配一个特定的方法时,实现一个真正的类适配器是具有挑战性的,但是在目标接口中没有与之非常匹配的方法。”你能详细说明一下吗?
在我的例子中,目标接口方法和适配器接口方法是相似的。比如在IRectangle
里有AboutMe()
法,在ITriangle
里有AboutTriangle()
法。他们是做什么的?他们指出它是矩形还是三角形。
现在假设在IRectangle,
中没有这个叫做AboutMe()
的方法,但是在ITriangle
中仍然存在AboutTriangle()
。所以,在这种情况下,如果你需要采用AboutTriangle()
方法,你需要分析如何进行。在我们的例子中,AboutTriangle()
是一个简单的方法,但是在现实世界的编程中,这个方法要复杂得多,并且可能存在与之相关的依赖关系。因此,当您没有相应的目标方法时,您可能会发现从一个被适应者那里适应该方法是一个挑战。
我明白客户不应该知道他们正在使用适配器。这是正确的吗?
没错。我做这个实现是为了向您展示,客户机不需要知道它们的请求通过适配器被转换到被适配器。如果您希望它们显示任何消息,您只需在演示 2 中的适配器中添加一个控制台消息,如下所示。
class RectangleAdapter : Triangle, IRectangle
{
public void AboutMe()
{
// Invoking the adaptee method
// For Q&A
Console.WriteLine("You are using an adapter now.");
AboutTriangle();
}
}
8.5 如果 目标接口 和 adaptee 接口方法 签名不同会怎样?
一点问题都没有。如果一个适配器方法有几个参数,您可以用一些额外的伪参数调用 adaptee 方法。在构建器模式中(第三章中的演示 2),您看到了可选参数。您可以在这里使用相同的概念。
在相反的情况下(如果适配器方法比 adaptee 方法有更多的参数),通过使用这些额外的参数,您可以在将调用转移到 adaptee 方法之前添加功能。
最后,如果方法参数不兼容,您可能需要进行转换(如果可能的话)。
8.6 与此模式相关的 弊端 有哪些?
我看不出有什么重大挑战。我认为适配器的工作简单明了,但是您需要编写一些额外的代码。然而,回报是巨大的,特别是对于那些不能改变的遗留系统,但是您仍然希望使用它们的稳定性和简单性。
九、外观模式
本章涵盖了外观模式。
GoF 定义
为子系统中的一组接口提供统一的接口。Facade 定义了一个更高级的接口,使得子系统更容易使用。
概念
这种模式支持松耦合。使用这种模式,您可以通过公开一个简单的接口来强调抽象并隐藏复杂的细节。
考虑一个简单的例子。假设在一个应用中,有多个类,每个类由多个方法组成。客户可以使用这些类中的方法组合来制作产品,但是他需要记住选择哪些类,哪些方法用于这些构造的调用序列。这没什么,但是如果这些产品之间有很多差异,客户的日子就不好过了。
为了克服这一点,Facade 模式很有用。它为客户提供了一个用户友好的界面,因为所有内在的复杂性都被隐藏起来了。因此,客户可以简单地专注于他需要做的事情。
真实世界的例子
假设你要举办一个有 300 名客人的生日聚会。现在,你可以雇佣一个聚会组织者,让他们知道聚会的类型、日期和时间、参加人数等关键信息。组织者会为您完成剩下的工作。你不需要考虑他们如何装饰聚会房间,他们如何管理食物,等等。
考虑另一个例子。假设客户向银行申请贷款。在这种情况下,客户只对贷款能不能批下来感兴趣;他不关心在后端进行的内部背景验证过程。
计算机世界的例子
想想当你使用一个库中的方法时(在编程语言的上下文中)。该方法在库中是如何实现的并不重要,您只需调用该方法以便于使用。下面的例子更清楚地说明了这一点。
履行
在这个例子中,一个客户可以请求得到不同种类的机器人和他喜欢的颜色。为了达到这个目的,只有两个类。第一个是RobotBody
,制作机器人的身体。第二类是RobotColor
,给机器人上色。
RobotBody
有一个参数化的构造函数,有两个方法叫做MakeRobotBody
和DestroyRobotBody
。这些方法负责制造一个机器人和摧毁一个机器人。我用一个计数器来记录机器人的数量。如果系统中没有机器人,销毁请求将被忽略。如果您愿意,可以忽略计数器,将注意力完全集中在描述该模式重要方面的部分。现在我们来看看RobotBody
级。
class RobotBody
{
string robotType;
/*
* To keep a count of number of robots.
* This operation is optional for you.
*/
static int count = 0;
public RobotBody(string robotType)
{
this.robotType = robotType;
}
public void MakeRobotBody()
{
Console.WriteLine($"Constructing one {robotType} robot.");
Console.WriteLine("Robot creation finished.");
Console.WriteLine($"Total number of robot created at this moment={++count}");
}
public void DestroyRobotBody()
{
if (count > 0)
{
--count;
Console.WriteLine("Robot's destruction process is over.");
}
else
{
Console.WriteLine("All robots are destroyed.");
Console.WriteLine("Color removal operation will not continue.");
}
}
}
RobotColor
很容易理解。它有一个参数化的构造函数和两个方法——SetColor()
和RemoveColor()
——来给机器人上色或从机器人身上移除颜料。下面的代码段是针对RobotColor
的。
public class RobotColor
{
string color;
public RobotColor(string color)
{
this.color = color;
}
public void SetColor()
{
if (color == "steel")
{
Console.WriteLine($"The default color {color} is set for the robot.");
}
else
{
Console.WriteLine($"Painting the robot with your favourite {color} color.");
}
}
public void RemoveColor()
{
Console.WriteLine("Attempting to remove the colors from the robot.");
}
}
现在是最重要的部分。您可以看到,客户端可以通过向对象RobotBody
提供所需的字符串参数来创建机器人,调用MakeRobotBody()
,
,然后使用RobotColor
类的SetColor()
来绘制机器人。因此,可以使用下面几行。
// Without Facade pattern
RobotBody robotBody = new RobotBody("Milano");
robotBody.MakeRobotBody();
RobotColor robotColor = new RobotColor("green");
robotColor.SetColor();
但是,如果一个客户端有一个名为RobotFacade
的类,并且像下面这样调用,会发生什么呢?
RobotFacade facade = new RobotFacade("Milano","green");
facade.ConstructRobot();
或者,你允许他像下面这样打电话(通过提供默认颜色)?
// Making a robonaut robot with default steel color.
facade = new RobotFacade("Robonaut");
facade.ConstructRobot();
你知道答案:客户会很高兴;在这些情况下,他不需要记住创建机器人的步骤。为了简单起见,示例中只使用了两个类,但是在现实世界中,您可能需要使用大量的类和方法来制作这样的产品。在这种情况下,Facade 模式更加强大。你可以告诉你的客户使用RobotFacade
类来创建和销毁机器人,而不是像RobotBody
和RobotColor
那样调用每个类。
现在我们来看看RobotFacade
。当我使用这个类的ConstructRobot()
和DestroyRobot()
方法时,我将RobotBody
和RobotColor
组合到其中,并将任务委托给相应的组件。从现在开始,RobotBody
和RobotColor
在这个例子中可以称为子系统类。
这里是门面类。
class RobotFacade
{
RobotBody robotBody;
RobotColor robotColor;
public RobotFacade(string robotType, string color = "steel")
{
robotBody = new RobotBody(robotType);
robotColor = new RobotColor(color);
}
public void ConstructRobot()
{
Console.WriteLine("Robot creation through facade starts...");
robotBody.MakeRobotBody();
robotColor.SetColor();
Console.WriteLine();
}
public void DestroyRobot()
{
Console.WriteLine("Making an attempt to destroy one robot using the facade now.");
robotColor.RemoveColor();
robotBody.DestroyRobotBody();
Console.WriteLine();
}
}
类图
图 9-1 显示了类图。
图 9-1
类图
解决方案资源管理器视图
图 9-2 显示了程序的高层结构。从 Solution Explorer 中,您可以看到,在较高的层次上,我将子系统类与外观类和客户端代码分离开来。子系统类放在 RobotParts 文件夹中。
图 9-2
解决方案资源管理器视图
示范
下面是完整的实现。
// RobotBody.cs
using System;
namespace FacadePattern.RobotParts
{
class RobotBody
{
string robotType;
/*
* To keep a count of number of robots.
* This operation is optional for you.
*/
static int count = 0;
public RobotBody(string robotType)
{
this.robotType = robotType;
}
public void MakeRobotBody()
{
Console.WriteLine($"Constructing one {robotType} robot.");
Console.WriteLine("Robot creation finished.");
Console.WriteLine($"Total number of robot created at this moment={++count}");
}
public void DestroyRobotBody()
{
if (count > 0)
{
--count;
Console.WriteLine("Robot's destruction process is over.");
}
else
{
Console.WriteLine("All robots are destroyed.");
Console.WriteLine("Color removal operation will not continue.");
}
}
}
}
// RobotColor.cs
using System;
namespace FacadePattern.RobotParts
{
public class RobotColor
{
string color;
public RobotColor(string color)
{
this.color = color;
}
public void SetColor()
{
if (color == "steel")
{
Console.WriteLine($"The default color {color} is set for the robot.");
}
else
{
Console.WriteLine($"Painting the robot with your favourite {color} color.");
}
}
public void RemoveColor()
{
Console.WriteLine("Attempting to remove the colors from the robot.");
}
}
}
// RobotFacade.cs
using System;
namespace FacadePattern.RobotParts
{
class RobotFacade
{
RobotBody robotBody;
RobotColor robotColor;
public RobotFacade(string robotType, string color = "steel")
{
robotBody = new RobotBody(robotType);
robotColor = new RobotColor(color);
}
public void ConstructRobot()
{
Console.WriteLine("Robot creation through facade starts...");
robotBody.MakeRobotBody();
robotColor.SetColor();
Console.WriteLine();
}
public void DestroyRobot()
{
Console.WriteLine("Making an attempt to destroy one robot using the facade now.");
robotColor.RemoveColor();
robotBody.DestroyRobotBody();
Console.WriteLine();
}
}
}
// Program.cs
using System;
using FacadePattern.RobotParts;
namespace FacadePattern
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Facade Pattern Demo.***\n");
// Making a Milano robot with green color.
RobotFacade facade = new RobotFacade("Milano","green");
facade.ConstructRobot();
// Making a robonaut robot with default steel color.
facade = new RobotFacade("Robonaut");
facade.ConstructRobot();
// Destroying one robot
facade.DestroyRobot();
// Destroying another robot
facade.DestroyRobot();
// This destrcution attempt should fail.
facade.DestroyRobot();
Console.ReadLine();
}
}
}
输出
这是输出。
***Facade Pattern Demo.***
Robot creation through facade starts...
Constructing one Milano robot.
Robot creation finished.
Total number of robot created at this moment=1
Painting the robot with your favourite green color.
Robot creation through facade starts...
Constructing one Robonaut robot.
Robot creation finished.
Total number of robot created at this moment=2
The default color steel is set for the robot.
Making an attempt to destroy one robot using the facade now.
Attempting to remove the colors from the robot.
Robot's destruction process is over.
Making an attempt to destroy one robot using the facade now.
Attempting to remove the colors from the robot.
Robot's destruction process is over.
Making an attempt to destroy one robot using the facade now.
Attempting to remove the colors from the robot.
All robots are destroyed.
Color removal operation will not continue.
问答环节
9.1 使用 Facade 模式的关键 优势 有哪些?
以下是一些优点。
-
如果您的系统由许多子系统组成,那么管理这些子系统就变得很困难,并且客户端发现很难与这些子系统中的每一个单独通信。在这种情况下,外观模式非常方便。您向客户呈现了一个简化的界面,而不是复杂的子系统。这种方法还通过将客户端代码与子系统分离来支持弱耦合。
-
它还可以帮助减少客户端需要处理的对象数量。
在这个例子中,facade 类使用了 组合 。有这个必要吗?
是的。使用这种方法,您可以访问每个子系统中的预期方法。当我使用这个类的ConstructRobot()
和DestroyRobot()
方法时,我将任务委托给了相应的组件。
9.3 您现在可以直接访问每个子系统了吗?
是的,你可以。Facade 模式并不限制您这样做。在介绍 facade 类之前,我已经向您展示了这一点。但是在这种情况下,代码可能看起来很脏,并且您可能会失去与 Facade 模式相关的好处。在这个上下文中,您可以注意到,由于客户端可以直接访问子系统,所以它被称为透明外观。但是,当您限制这种使用并强制他们只能通过 RobotFacade 创建机器人时,您可以将 Facade 称为不透明 facade。
9.4 Facade 与适配器设计模式 有何不同?
在适配器模式中,您试图改变一个接口,以便您的客户机看不到接口之间的任何差异。相比之下,Facade 模式简化了界面。它为客户端提供了一个简单的交互界面(而不是复杂的子系统)。
9.5 一个 复杂子系统 应该只有一个门面。这是正确的吗?
一点也不。您可以为特定子系统创建任意数量的外观。
9.6 可以用 facade 添加新的东西或者附加代码吗?
是的,你可以。您可以看到,在将调用委托给实际组件之前,我在RobotFacade
类的ConstructRobot()
中使用了下面一行代码。
Console.WriteLine("Robot creation through facade starts...");
同样的,DestroyRobot()
在试图摧毁一个机器人之前,有如下一行。
Console.WriteLine("Making an attempt to destroy one robot using the facade now.");
9.7 与门面格局相关的 挑战 有哪些?
这里有一些挑战。
-
子系统连接到外观层。因此,您需要关注额外的编码层(增加您的代码库)。
-
当子系统的内部结构发生变化时,您也需要将变化合并到外观层中。
-
一些开发人员可能需要了解这个新层,但是他们中的一些人知道如何有效地使用子系统/API。
9.8 我能让 facade 类成为静态的吗?
在许多例子中,只有一个 facade,您可能不需要初始化 facade 类。在这些情况下,如果让 facade 类成为静态的,就很有意义。
十、享元模式
这一章涵盖了 Flyweight 模式。
GoF 定义
使用共享来有效地支持大量细粒度的对象。
概念
这种模式可能看起来很简单,但是如果您没有确定核心概念,实现可能会显得很复杂。在实现这个模式之前,让我们从一个基本但详细的解释开始。
有时你需要处理许多非常相似但又不完全相同的对象。限制是您不能创建所有的文件来减少资源和内存的使用。Flyweight 模式就是用来处理这些场景的。
现在的问题是如何去做?为了理解这一点,让我们快速回顾一下面向对象编程的基础。一个类是一个模板或蓝图,一个对象是它的一个实例。一个对象可以有状态和行为。例如,如果你熟悉足球(或在美国被称为足球,你可以说 Ronaldo 或 Beckham 是Footballer
类的对象。您可能会注意到,它们有“播放状态”或“非播放状态”这样的状态在玩耍状态下,他们可以展示不同的技能(或行为)——他们可以跑,可以踢,可以传球,等等。从面向对象编程开始,您可以提出以下问题。
-
我的对象可能有哪些状态?
-
在这些状态下,它们可以执行哪些不同的功能(行为)?
一旦你得到了这些问题的答案,你就可以继续了。现在回到享元模式。在这里你的工作是识别。
-
我的对象的状态是什么?
-
这些状态的哪一部分是可以改变的?
一旦你确定了答案,你就把状态分成两部分,称为内在的(不变的)和外在的(会变的)。现在你明白了,如果你制造的对象具有所有对象都可以共享的内在状态。对于外在部分,用户或客户需要传递信息。所以,无论何时你需要一个对象,你都可以得到具有内在状态的对象,然后你可以通过传递外在状态来动态地配置对象。遵循这种技术,您可以减少不必要的对象创建和内存使用。
现在让我们在下面的段落中验证你的知识,这是极其重要的。让我们看看 GoF 对享元是怎么说的。
flyweight 是一个可以同时在多个上下文中使用的共享对象。在每个上下文中,flyweight 充当一个独立的对象——它与未共享的对象实例没有区别。Flyweights 不能对它们运行的环境做出假设。这里的关键概念是内在和外在状态之间的区别。内在状态存储在 flyweight 中;它由独立于 flyweight 上下文的信息组成,因此可以共享。外在状态依赖于 flyweight 的上下文并随其变化,因此不能共享。客户端对象负责在需要时将外部状态传递给 flyweight。
真实世界的例子
假设你有一支笔。你可以用不同的墨水笔芯写不同的颜色。因此,在这个例子中,没有笔芯的笔可以被认为是具有内在数据的享元,而笔芯可以被认为是外在数据。
计算机世界的例子
假设在一个电脑游戏中,你有大量的参与者,他们的核心结构是相同的,但他们的外观各不相同(例如,他们可能有不同的状态、颜色、武器等等)。因此,如果你想存储所有具有所有变化/状态的对象,内存需求将是巨大的。因此,不需要存储所有的对象,您可以用这样一种方式设计应用,即您创建这些实例中的一个,这些实例的状态在对象之间没有变化,并且您的客户端可以维护剩余的变化/状态。如果您能够在设计阶段成功地实现这个概念,那么您已经在应用中遵循了 Flyweight 模式。
考虑另一个例子。假设一家公司需要为员工打印名片。在这种情况下,出发点是什么?企业可以创建一个通用模板,在该模板上已经打印了公司徽标、地址等(内部),然后公司将特定员工的信息(外部)放在卡片上。
这种模式的另一个常见用途是在文字处理程序中用图形表示字符,或者在应用中处理字符串时使用。
履行
下面的例子展示了三种不同类型车辆的用法:Car
、Bus,
、FutureVehicle
(我假设 2050 年使用)。在这个应用中,我假设客户可能想要使用这些类中的大量对象,这些对象具有他们喜欢的不同颜色。我还假设汽车(或公共汽车等)的基本结构。)不变。
当客户端请求特定的车辆时,如果应用先前创建了该类型车辆的实例,则它不会从头开始创建对象;相反,它会准备好现有的(没有color
)来满足他的需求。就在交付产品之前,它会给车辆涂上客户喜欢的color
。现在让我们看看实现策略。
首先,为 flyweights 创建一个接口。这个接口提供了接受 flyweights 的外部状态的通用方法。在我们的例子中,color
是由客户提供的;因此,这被视为一种外在状态,这就是为什么你会看到下面的代码段。
/// <summary>
/// The 'Flyweight' interface
/// </summary>
interface IVehicle
{
/*
* Client will supply the color.
* It is extrinsic state.
*/
void AboutMe(string color);
}
最常见的情况是,你看到一个工厂为客户提供飞锤。这个工厂缓存 flyweights 并提供获取它们的方法。在一个共享的 flyweight 对象中,如果需要的话,可以添加内部状态并实现方法。你也可以有不共享的飞锤。在这些情况下,您可以忽略客户端传递的外部状态。
在接下来的例子中,VehicleFactory
是为 flyweights 提供内在状态的工厂。一个Dictionary
对象存储key/value
对以存储特定类型的车辆。最初,工厂内部没有对象,但是一旦它开始接收车辆请求,它就创建车辆并缓存这些车辆以备将来使用。请注意,“创建一辆汽车”、“创建一辆公共汽车”和“创建 2050 辆汽车”是在对象创建阶段由工厂在 flyweight 对象中提供的。这些是这些车辆的固有状态,不会因产品而异。下面的代码段显示了这个工厂类。
/// <summary>
/// The factory class for flyweights.
/// </summary>
class VehicleFactory
{
private Dictionary<string, IVehicle> vehicles = new Dictionary<string, IVehicle>();
public int TotalObjectsCreated
{
get { return vehicles.Count; }
}
public IVehicle GetVehicleFromVehicleFactory(string vehicleType)
{
IVehicle vehicleCategory = null;
if (vehicles.ContainsKey(vehicleType))
{
vehicleCategory = vehicles[vehicleType];
}
else
{
switch (vehicleType)
{
case "car":
vehicleCategory = new Car("One car is created");
vehicles.Add("car", vehicleCategory);
break;
case "bus":
vehicleCategory = new Bus("One bus is created");
vehicles.Add("bus", vehicleCategory);
break;
case "future":
vehicleCategory = new FutureVehicle("Vehicle 2050 is created");
vehicles.Add("future", vehicleCategory);
break;
default:
throw new Exception("Vehicle Factory can give you cars and buses only.");
}
}
return vehicleCategory;
}
}
现在让我们来看一个具体的享元类。下面是其中的一个类(其他的都差不多)。相关注释帮助您理解AboutMe()
方法如何包含车辆的内在状态和外在状态。
/// <summary>
/// A 'ConcreteFlyweight' class called Car
/// </summary>
class Car : IVehicle
{
/*
* It is intrinsic state and
* it is independent of flyweight context.
* this can be shared.So, our factory method will supply
* this value inside the flyweight object.
*/
private string description;
/*
* Flyweight factory will supply this
* inside the flyweight object.
*/
public Car(string description)
{
this.description = description;
}
// Client will supply the color
public void AboutMe(string color)
{
Console.WriteLine($"{description} with {color} color.");
}
}
从这段代码中,您可以看到,description
是在对象创建过程中提供的(Flyweight 工厂会这样做),但是color
是由客户端提供的。在这个例子中,我使用一种叫做GetRandomColor()
的方法随机绘制颜色。因此,在Main()
中,您会看到下面的代码:
vehicle.AboutMe(GetRandomColor());
只读属性TotalObjectsCreated
计算任意给定时刻不同类型的车辆;在工厂类中理解下面的代码是非常容易的。
public int TotalObjectsCreated
{
get
{
return vehicles.Count;
}
}
最后,在这个例子中,FutureVehicle
被认为是一个非共享的 flyweight。所以,在这个类中,AboutMe(...)
方法忽略了string
参数。因此,它总是生产蓝色的车辆,并忽略客户的喜好。
// Client cannot choose color for FutureVehicle
//since it's unshared flyweight,ignoring client's input
public void AboutMe(string color)
{
Console.WriteLine($"{description} with blue color.");
}
类图
图 10-1 为类图。
图 10-1
类图
解决方案资源管理器视图
图 10-2 显示了程序各部分的高层结构。
图 10-2
解决方案资源管理器视图
演示 1
下面是完整的实现。参考评论帮助你更好的理解。
using System;
using System.Collections.Generic;//Dictionary is used here
namespace FlyweightPattern
{
/// <summary>
/// The 'Flyweight' interface
/// </summary>
interface IVehicle
{
/*
* Client will supply the color.
* It is extrinsic state.
*/
void AboutMe(string color);
}
/// <summary>
/// A 'ConcreteFlyweight' class called Car
/// </summary>
class Car : IVehicle
{
/*
* It is intrinsic state and
* it is independent of flyweight context.
* this can be shared.So, our factory method will supply
* this value inside the flyweight object.
*/
private string description;
/*
* Flyweight factory will supply this
* inside the flyweight object.
*/
public Car(string description)
{
this.description = description;
}
// Client will supply the color
public void AboutMe(string color)
{
Console.WriteLine($"{description} with {color} color.");
}
}
/// <summary>
/// A 'ConcreteFlyweight' class called Bus
/// </summary>
class Bus : IVehicle
{
/*
* It is intrinsic state and
* it is independent of flyweight context.
* this can be shared.So, our factory method will supply
* this value inside the flyweight object.
*/
private string description;
public Bus(string description)
{
this.description = description;
}
// Client will supply the color
public void AboutMe(string color)
{
Console.WriteLine($"{description} with {color} color.");
}
}
/// <summary>
/// A 'ConcreteFlyweight' class called FutureVehicle
/// </summary>
class FutureVehicle : IVehicle
{
/*
* It is intrinsic state and
* it is independent of flyweight context.
* this can be shared.So, our factory method will supply
* this value inside the flyweight object.
*/
private string description;
public FutureVehicle(string description)
{
this.description = description;
}
// Client cannot choose color for FutureVehicle
// since it's unshared flyweight,ignoring client's input
public void AboutMe(string color)
{
Console.WriteLine($"{description} with blue color.");
}
}
/// <summary>
/// The factory class for flyweights.
/// </summary>
class VehicleFactory
{
private Dictionary<string, IVehicle> vehicles = new Dictionary<string, IVehicle>();
/*
* To count different types of vehicles
* in a given moment.
*/
public int TotalObjectsCreated
{
get
{
return vehicles.Count;
}
}
public IVehicle GetVehicleFromVehicleFactory(string vehicleType)
{
IVehicle vehicleCategory = null;
if (vehicles.ContainsKey(vehicleType))
{
vehicleCategory = vehicles[vehicleType];
}
else
{
switch (vehicleType)
{
case "car":
vehicleCategory = new Car("One car is created");
vehicles.Add("car", vehicleCategory);
break;
case "bus":
vehicleCategory = new Bus("One bus is created");
vehicles.Add("bus", vehicleCategory);
break;
case "future":
vehicleCategory = new FutureVehicle("Vehicle 2050 is created");
vehicles.Add("future", vehicleCategory);
break;
default:
throw new Exception("Vehicle Factory can give you cars and buses only.");
}
}
return vehicleCategory;
}
}
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Flyweight Pattern Demo.***\n");
VehicleFactory vehiclefactory = new VehicleFactory();
IVehicle vehicle;
/*
* Now we are trying to get the 3 cars. Note that:we need not create additional cars if we have already created one of this category.
*/
for (int i = 0; i < 3; i++)
{
vehicle = vehiclefactory.GetVehicleFromVehicleFactory("car");
vehicle.AboutMe(GetRandomColor());
}
int numOfDistinctRobots = vehiclefactory.TotalObjectsCreated;
Console.WriteLine($"\n Now, total numbers of distinct vehicle object(s) is = {numOfDistinctRobots}\n");
/*
Here we are trying to get the 5 more buses.Note that: we need not create additional buses if we have already created one of this category.
*/
for (int i = 0; i < 5; i++)
{
vehicle = vehiclefactory.GetVehicleFromVehicleFactory("bus");
vehicle.AboutMe(GetRandomColor());
}
numOfDistinctRobots = vehiclefactory.TotalObjectsCreated;
Console.WriteLine($"\n Now, total numbers of distinct vehicle object(s) is = {numOfDistinctRobots}\n");
/*
Here we are trying to get the 2 future vehicles.Note that: we need not create additional future vehicle if we have already created one of this category.
*/
for (int i = 0; i < 2; i++)
{
vehicle = vehiclefactory.GetVehicleFromVehicleFactory("future");
vehicle.AboutMe(GetRandomColor());
}
numOfDistinctRobots = vehiclefactory.TotalObjectsCreated;
Console.WriteLine($"\n Now, total numbers of distinct vehicle object(s) is = {numOfDistinctRobots}\n");
Console.ReadKey();
}
private static string GetRandomColor()
{
Random r = new Random();
/*
You can supply any number of your choice in nextInt argument.we are simply checking the random number generated is an even number or an odd number. And based on that we are choosing the color. For simplicity, we'll use only two colors-red and green.
*/
int random = r.Next(100);
if (random % 2 == 0)
{
return "red";
}
else
{
return "green";
}
}
}
}
输出
下面是一个可能的输出(因为color
是随机生成的)。这是在我的机器上第一次运行的时候。
***Flyweight Pattern Demo.***
One car is created with green color.
One car is created with red color.
One car is created with green color.
Now, total numbers of distinct vehicle object(s) is = 1
One bus is created with green color.
One bus is created with red color.
One bus is created with green color.
One bus is created with red color.
One bus is created with red color.
Now, total numbers of distinct vehicle object(s) is = 2
Vehicle 2050 is created with blue color.
Vehicle 2050 is created with blue color.
Now, total numbers of distinct vehicle object(s) is = 3
这是另一个可能的输出。这是我的机器第二次运行的结果。
***Flyweight Pattern Demo.***
One car is created with red color.
One car is created with red color.
One car is created with red color.
Now, total numbers of distinct vehicle object(s) is = 1
One bus is created with red color.
One bus is created with green color.
One bus is created with red color.
One bus is created with green color.
One bus is created with red color.
Now, total numbers of distinct vehicle object(s) is = 2
Vehicle 2050 is created with blue color.
Vehicle 2050 is created with blue color.
Now, total numbers of distinct vehicle object(s) is = 3
Note
输出会有变化,因为在这个例子中我随机选择了颜色。
分析
当且仅当对象当时不可用时,应用才创建对象。此后,它将缓存该对象以供将来重用。
问答环节
你能指出单例模式 和享元模式的主要区别吗?
Singleton 帮助你最多维护一个系统中需要的对象。换句话说,一旦创建了所需的对象,就不能再创建更多的对象了。您需要重用现有的对象。
Flyweight 模式通常与重但相似的对象(其中状态不相同)有关,因为它们可能会占用大量内存。因此,您尝试创建一个较小的模板对象集,可以动态地配置这些模板对象来制作这些重对象。这些更小的可配置对象被称为享元对象。当您处理许多大型对象时,可以在应用中重用它们。这种方法有助于减少大块内存的消耗。基本上,flyweight 使一个看起来像许多,这就是为什么 GoF 声明:“一个 flyweight 是一个共享对象,可以同时在多个上下文中使用。flyweight 在每个上下文中都是一个独立的对象——它与未共享的对象实例没有什么区别。”
图 10-3 向你展示了在使用 Flyweight 之前,如何可视化 Flyweight 模式的核心概念。
图 10-3
在使用飞锤之前
图 10-4 显示了使用飞锤后的设计。
图 10-4
使用飞锤后
因此,从图 10-4 中,你可以看到当我们将配置-1 应用于 Flyweight 对象时,创建了 Heavy-Object1,同样,当我们将配置-2 应用于 Flyweight 对象时,创建了 Heavy-Object2。您可以看到,特定于实例的内容(如我们的演示 1 中的颜色)可以传递给 flyweights 来生成这些重对象。在这个例子中,flyweight 对象就像一个公共模板,可以根据需要进行配置。
10.2****多线程 有什么影响?
如果在多线程环境中使用 new 运算符创建对象,最终可能会创建多个不需要的对象。这类似于单例模式,补救措施也类似。
10.3 使用 Flyweight 设计模式有什么好处?
以下是一些优点。
-
您可以减少可同等控制的重物的内存消耗。
-
您可以减少系统中的对象总数。
-
您可以维护许多“虚拟”对象的集中状态。
10.4 使用 Flyweight 设计模式的相关挑战是什么?
这里有一些挑战。
-
在这种模式中,您需要花一些时间来配置这些 flyweights。这些配置时间会影响应用的整体性能。
-
要创建 flyweights,您需要从现有对象中提取一个公共模板类。这个额外的编程层可能很棘手,有时很难调试和维护。
10.5 能不能有一个 不可共享的 flyweight 接口 ?
是的,flyweight 接口并不强制要求它总是可共享的。因此,在某些情况下,您可能有不可共享的 flyweight,具体的 flyweight 对象作为子对象。在演示 1 中,FutureVehicle 就是为此而生的。你可以看到它总是由蓝色组成,对于这辆车,无论客户提供什么颜色(红色或绿色)作为外在状态都没有关系。
10.6 既然 flyweights 的 内在数据 相同,可以试着分享一下。这是正确的吗?
是的。请注意,“创建一辆汽车”、“创建一辆公共汽车”和“创建 2050 辆汽车”是由工厂在 flyweight(具有内在状态)对象创建阶段在 flyweight 内部提供的。
10.7 客户如何处理这些蝇量级的 外来数据 ?
当他们需要使用这个概念时,他们需要将这些信息(状态)传递给 flyweights。
10.8 外部数据不可共享。这是正确的吗?
是的。在实现这个模式之前,理解它是非常重要的。
10.9****车辆厂 在此次实施中的作用是什么?
它缓存了 flyweights,并提供了获取它们的方法。在本例中,有多个具有内在状态的对象可以共享。因此,将它们存放在一个中心位置总是一个好主意。
10.10 我可以将工厂类作为单例实现吗?
是的,你可以。事实上,在很多应用中,你可能会看到这一点。演示 2 描述了它。
演示 2
在这个例子中,VehicleFactory
工厂类是作为单例实现的。因此,您可以用下面的代码替换演示 1 中的工厂类。
/// <summary>
/// The factory class for flyweights implemented as singleton.
/// </summary>
class VehicleFactory
{
private static readonly VehicleFactory Instance = new VehicleFactory();
private Dictionary<string, IVehicle> vehicles = new Dictionary<string, IVehicle>();
private VehicleFactory()
{
vehicles.Add("car", new Car("One car is created"));
vehicles.Add("bus", new Bus("One bus is created"));
vehicles.Add("future", new FutureVehicle("Vehicle 2050 is created"));
}
public static VehicleFactory GetInstance
{
get
{
return Instance;
}
}
/*
* To count different types of vehicles
* in a given moment.
*/
public int TotalObjectsCreated
{
get
{
return vehicles.Count;
}
}
public IVehicle GetVehicleFromVehicleFactory(string vehicleType)
{
IVehicle vehicleCategory = null;
if (vehicles.ContainsKey(vehicleType))
{
vehicleCategory = vehicles[vehicleType];
return vehicleCategory;
}
else
{
throw new Exception("Currently, the vehicle factory can have cars and buses only.");
}
}
}
现在,在客户端代码中,您需要使用新的代码行来适应前面的更改,而不是使用下面的代码行(它被注释掉了)。
//VehicleFactory vehiclefactory = new VehicleFactory();
VehicleFactory vehiclefactory = VehicleFactory.GetInstance;
输出
当您使用这些新代码段运行应用时,您可能会得到(因为颜色是随机生成的)如下所示的输出。
***Flyweight Pattern Demo.***
One car is created with red color.
One car is created with red color.
One car is created with red color.
Now, total numbers of distinct vehicle object(s) is = 3
One bus is created with green color.
One bus is created with green color.
One bus is created with green color.
One bus is created with red color.
One bus is created with red color.
Now, total numbers of distinct vehicle object(s) is = 3
Vehicle 2050 is created with blue color.
Vehicle 2050 is created with blue color.
Now, total numbers of distinct vehicle object(s) is = 3
分析
注意,在这个实现中,我在开始时在构造函数中初始化了所有不同类型的车辆。因此,我一开始就使用了三个不同的车辆对象。因此,如果 2050 年我不需要任何公共汽车、汽车或交通工具,我就为这些对象浪费了内存。相反,在演示 1 中,如果这些对象中有任何一个不可用,那么工厂类会创建它并缓存它以备将来使用。所以,我投票支持演示 1,除非你修改演示 2,记住这个潜在的缺点。简而言之,无论何时使用这种模式,都要创建一个对象,填充所有必需的状态信息,并将其提供给客户机。每次客户端请求一个对象时,你的应用应该检查它是否可以重用一个现有的对象(填充了所需的状态);从而减少不必要的对象创建并节省内存消耗。
微软表示, Intern
方法使用 intern 池来搜索与字符串值相等的字符串。如果存在这样的字符串,则返回它在实习生池中的引用;否则,将对该字符串的引用添加到实习生池,然后返回该引用。英寸 NET Core 3.1,我执行下面这段代码的时候,firstString
和thirdString
都是指同一个字符串。结果,这段代码的最后一行返回 True,而当您比较firstString
和secondString
时,情况并非如此,因为它们引用的是不同的对象。
#region test for in-built flyweight pattern
string firstString = "A simple string";
string secondString = new StringBuilder().Append("A").Append(" simple").Append(" string").ToString();
string thirdString = String.Intern(secondString);
// Different references.
Console.WriteLine((Object)secondString == (Object)firstString);
// Same reference.
Console.WriteLine((Object)thirdString == (Object)firstString);
#endregion
所以,你可以说。NET Core 3.1 遵循 Flyweight 模式。
十一、组合模式
本章涵盖了组合模式。
GoF 定义
将对象组成树结构来表示部分-整体层次结构。Composite 允许客户端统一处理单个对象和对象的组合。
概念
考虑一家出售不同种类干果的商店,如腰果、枣和核桃。这些物品都有一定的价格。让我们假设你可以购买这些单独的物品中的任何一种,或者你可以购买由不同的干果物品组成的“礼品包”(或盒装物品)。在这种情况下,数据包的开销是其组成部分的总和。组合模式在类似的情况下很有用,在这种情况下,您以相同的方式处理单个部分和部分的组合,以便可以统一处理它们。
这种模式对于表示对象的部分-整体层次结构很有用。在面向对象编程中,组合对象是由一个或多个相似对象组成的对象,其中每个对象都具有相似的功能。(这也称为对象之间的“有-有”关系。)这种模式在树结构数据中很常见,当您在这样的数据结构中实现这种模式时,您不需要区分树的分支和叶节点。因此,您可以使用该模式实现这两个关键目标。
-
您可以将对象组合成一个树形结构,以代表部分整体层次结构。
-
您可以统一访问组合对象(分支)和单个对象(叶节点)。因此,您可以降低代码的复杂性,并使应用不容易出错。
真实世界的例子
除了我们前面的例子,你还可以想到一个由许多部门组成的组织。一般来说,一个组织有很多员工。这些雇员中的一些被分组以形成一个部门,这些部门可以被进一步分组以构建组织的高层结构。
计算机世界的例子
我提到过树数据结构可以遵循这个概念,其中客户端可以以相同的方式处理树叶和非树叶(或树枝)。所以,当你看到一个分层的数据时,你可以得到一个线索,组合模式可能是有用的。XML 文件是这种树结构的常见例子。
Note
当你遍历树时,你经常会用到迭代器设计模式的概念,这将在第十八章中介绍。
履行
在这个例子中,我代表一个大学组织。假设有一个校长和两个系主任(hod),一个是计算机科学与工程(CSE),一个是数学(Math)。假设在数学系,目前有两个讲师(或老师),在计算机科学与工程系,有三个讲师(老师)。该组织的树形结构如图 11-1 所示。
图 11-1
一个大学组织,有一名校长、两名主任和五名讲师/教师
我们还假设在年底,CSE 部门的一名讲师提交了辞呈。以下示例考虑了提到的所有场景。
类图
图 11-2 显示了类图。
图 11-2
类图
解决方案资源管理器视图
图 11-3 显示了程序的高层结构。
图 11-3
解决方案资源管理器视图
示范
该演示以树形结构为特色。IEmployee
是一个接口,有三个读写属性和一个叫做DisplayDetails()
的方法。看起来是这样的。
interface IEmployee
{
// To set an employee name
string Name { get; set; }
// To set an employee department
string Dept { get; set; }
// To set an employee designation
string Designation { get; set; }
// To display an employee details
void DisplayDetails();
}
从相关的注释中,很容易理解这三个属性设置了雇员的姓名、对应的部门和职务。Employee
和CompositeEmployee
具体类实现了这个接口。Employee
班级(讲师)充当一个叶节点,另一个是非叶节点。一个或多个员工可以向一个部门主管报告。因此,它被视为非叶(或分支)节点。同样,所有的 hod 都向校长报告。所以,Principal
是另一个非叶节点。
数学讲师名叫 m .乔伊和 m .鲁尼。CSE 的老师被命名为 c .萨姆,c .琼斯和 c .马里乌姆。这些讲师不监督任何人,所以他们被视为叶节点。
CompositeEmployee
类维护一个列表和另外两个名为AddEmployee(...)
和RemoveEmployee(...)
的方法。这些方法向列表中添加雇员或从列表中删除雇员。
现在浏览完整的实现,并参考支持性的评论。
using System;
/* For List<Employee> using
* the following namespace.
*/
using System.Collections.Generic;
namespace CompositePattern
{
interface IEmployee
{
// To set an employee name
string Name { get; set; }
// To set an employee department
string Dept { get; set; }
// To set an employee designation
string Designation { get; set; }
// To display an employee details
void DisplayDetails();
}
// Leaf node
class Employee : IEmployee
{
public string Name { get; set; }
public string Dept { get; set; }
public string Designation { get; set; }
// Details of a leaf node
public void DisplayDetails()
{
Console.WriteLine($"\t{Name} works in { Dept} department.Designation:{Designation}");
}
}
// Non-leaf node
class CompositeEmployee : IEmployee
{
public string Name { get; set; }
public string Dept { get; set; }
public string Designation { get; set; }
// The container for child objects
private List<IEmployee> subordinateList = new List<IEmployee>();
// To add an employee
public void AddEmployee(IEmployee e)
{
subordinateList.Add(e);
}
// To remove an employee
public void RemoveEmployee(IEmployee e)
{
subordinateList.Remove(e);
}
// Details of a composite node
public void DisplayDetails()
{
Console.WriteLine($"\n{Name} works in {Dept} department.Designation:{Designation}");
foreach (IEmployee e in subordinateList)
{
e.DisplayDetails();
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Composite Pattern Demo. ***");
#region Mathematics department
// 2 lecturers work in Mathematics department
Employee mathTeacher1 = new Employee { Name = "M.Joy", Dept = "Mathematic", Designation = "Lecturer" };
Employee mathTeacher2 = new Employee { Name = "M.Roony", Dept = "Mathematics", Designation = "Lecturer" };
// The college has a Head of Department in Mathematics
CompositeEmployee hodMaths = new CompositeEmployee { Name = "Mrs.S.Das", Dept = "Maths", Designation = "HOD-Maths" };
// Lecturers of Mathematics directly reports to HOD-Maths
hodMaths.AddEmployee(mathTeacher1);
hodMaths.AddEmployee(mathTeacher2);
#endregion
#region Computer Science department
// 3 lecturers work in Computer Sc. department
Employee cseTeacher1 = new Employee { Name = "C.Sam", Dept = "Computer Science", Designation = "Lecturer" };
Employee cseTeacher2 = new Employee { Name = "C.Jones", Dept = "Computer Science.", Designation = "Lecturer" };
Employee cseTeacher3 = new Employee { Name = "C.Marium", Dept = "Computer Science", Designation = "Lecturer" };
// The college has a Head of Department in Computer science
CompositeEmployee hodCompSc = new CompositeEmployee { Name = "Mr. V.Sarcar", Dept = "Computer Sc.", Designation = "HOD-Computer Sc." };
/* Lecturers of Computer Sc. directly reports to HOD-CSE */
hodCompSc.AddEmployee(cseTeacher1);
hodCompSc.AddEmployee(cseTeacher2);
hodCompSc.AddEmployee(cseTeacher3);
#endregion
#region Top level management
// The college also has a Principal
CompositeEmployee principal = new CompositeEmployee { Name = "Dr.S.Som", Dept = "Planning-Supervising-Managing", Designation = "Principal" };
/* Head of Departments's of Maths and Computer Science directly reports to Principal.*/
principal.AddEmployee(hodMaths);
principal.AddEmployee(hodCompSc);
#endregion
/*
* Printing the leaf-nodes and branches in the same way. i.e. in each case, we are calling DisplayDetails() method.
*/
Console.WriteLine("\nDetails of a Principal object is as follows:");
// Prints the complete structure
principal.DisplayDetails();
Console.WriteLine("\nDetails of a HOD object is as follows:");
/* Prints the details of Computer Science department */
hodCompSc.DisplayDetails();
// Leaf node
Console.WriteLine("\nDetails of an individual employee(leaf node) is as follows:");
mathTeacher1.DisplayDetails();
/*
* Suppose, one Computer Science lecturer(C.Jones)
* is leaving now from the organization.
*/
hodCompSc.RemoveEmployee(cseTeacher2);
Console.WriteLine("\nAfter the resignation of C.Jones, the organization has the following members:");
principal.DisplayDetails();
// Wait for user
Console.ReadKey();
}
}
}
输出
这是输出。
***Composite Pattern Demo. ***
Details of a Principal object is as follows:
Dr. S.Som works in Planning-Supervising-Managing department.Designation:Principal
Mrs. S.Das works in Maths department.Designation:HOD-Maths
M.Joy works in Mathematic department.Designation:Lecturer
M.Roony works in Mathematics department.Designation:Lecturer
Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc.
C.Sam works in Computer Science department.Designation:Lecturer
C.Jones works in Computer Science. department.Designation:Lecturer
C.Marium works in Computer Science department.Designation:Lecturer
Details of a HOD object is as follows:
Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc.
C.Sam works in Computer Science department.Designation:Lecturer
C.Jones works in Computer Science. department.Designation:Lecturer
C.Marium works in Computer Science department.Designation:Lecturer
Details of an individual employee(leaf node) is as follows:
M.Joy works in Mathematic department.Designation:Lecturer
After the resignation of C.Jones, the organization has the following members:
Dr. S.Som works in Planning-Supervising-Managing department.Designation:Principal
Mrs. S.Das works in Maths department.Designation:HOD-Maths
M.Joy works in Mathematic department.Designation:Lecturer
M.Roony works in Mathematics department.Designation:Lecturer
Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc.
C.Sam works in Computer Science department.Designation:Lecturer
C.Marium works in Computer Science department.Designation:Lecturer
问答环节
11.1 使用组合设计模式的优势是什么?
以下是一些优点。
-
在树状结构中,您可以统一处理组合对象(分支节点)和单个对象(叶节点)。在这个例子中,我使用了一个名为
DisplayDetails
的常用方法来打印组合对象结构(校长或部门领导)和单个对象(讲师)。 -
使用这种设计模式实现部分-整体层次结构是很常见的。
-
您可以轻松地向架构中添加新的组件,或者从架构中删除现有的组件。
11.2 使用组合设计模式有哪些挑战?
以下是一些缺点。
-
如果您想要保持子节点的顺序(例如,如果解析树被表示为组件),您可能需要特别小心。
-
如果您正在处理不可变的对象,您不能删除它们。
-
您可以轻松地添加新组件,但是经过一段时间后,维护可能会很困难。有时,您可能想要处理具有特殊组件的组合材料。这种约束可能会导致额外的开发成本,因为您可能需要实现一个动态检查机制来支持这个概念。
11.3 在这个例子中,你使用了一个列表 数据结构 。其他数据结构可以用吗?
绝对的。没有放之四海而皆准的规则。您可以自由使用您喜欢的数据结构。GoF 还确认了没有必要使用通用数据结构。
如何将 迭代器设计模式 连接到组合设计模式?
在这个例子中,如果您想要检查一个组合对象架构,您可能需要迭代对象。此外,如果您想对一些分支执行一些特殊的活动,您可能需要迭代它的叶节点和非叶节点。
11.5 在你的实现中,在这个界面中,你只定义了一个方法, DisplayDetails
。但是您使用了附加的方法来添加和移除组合类中的对象( CompositeEmployee
)。为什么不把这些方法放在接口中?
不错的观察。甚至 GoF 也讨论过这个问题。让我们看看如果在接口中放入AddEmployee(...)
和RemoveEmployee(...)
方法会发生什么。在这种情况下,叶节点需要实现这些添加和删除操作。但是这种情况下会有意义吗?答案是否定的,在这种情况下,可能会显得你失去了透明性,但我相信你更安全,因为我在叶子节点中屏蔽了无意义的操作。这就是为什么 GoF 提到这种决定涉及到安全性和透明度之间的权衡。
我想用一个抽象类来代替接口。这是允许的吗?
在大多数情况下,简单的答案是肯定的,但是您需要理解抽象类和接口之间的区别。在典型的场景中,您可能会发现其中一个比另一个更有用。在整本书中,我只给出简单易懂的例子,所以你可能看不出它们之间有什么区别。
Note
在第三章的“问答”部分,我讨论了如何在抽象类和接口之间做出选择。
十二、桥接模式
本章涵盖了桥接模式。
GoF 定义
将抽象与其实现解耦,这样两者可以独立变化。
概念
这种模式也称为手柄/主体模式。使用它,您可以通过在实现类和抽象类之间提供一个桥梁来将它们解耦。
这个桥接口使得具体类的功能独立于接口实现者类。您可以在结构上改变不同种类的类,而不会相互影响。这种模式最初可能看起来很复杂,这就是为什么在这一章中,有两种不同的实现并有很多解释。当你浏览这些例子时,这个概念会更清楚。
真实世界的例子
在软件产品开发公司中,开发团队和营销团队都扮演着至关重要的角色。营销团队做市场调查,收集客户需求。开发团队在产品中实现这些需求,以满足客户需求。一个团队中的任何变化(如运营策略)都不应对另一个团队产生直接影响。在这种情况下,营销团队在产品的客户和软件公司的开发团队之间扮演着桥梁的角色。
计算机世界的例子
GUI 框架可以使用桥模式将抽象从平台特定的实现中分离出来。例如,使用这种模式,您可以从 Linux 或 macOS 的窗口实现中分离出一个窗口抽象。
履行
假设你需要为销售不同电子产品的卖家设计一个软件。为简单起见,我们假设卖家目前在销售电视机和 DVD 播放器,他以线上和线下(在不同的展厅)两种模式销售。
在这种情况下,您可以从图 12-1 或图 12-2 所示的设计开始。
图 12-2
方法 2
图 12-1
方法 1
经过进一步分析,您发现方法 1 很混乱,很难维护。
首先,方法 2 看起来更清晰,但是如果您想要包含新的价格(例如,ThirdPartyPrice
、FestiveTimePrice
等)。),或者如果您想要包含新的电子产品(例如,空调、冰箱等)。),您面临着新的挑战,因为在这种设计中,各种元素紧密耦合。但是在真实的场景中,这种增强是经常需要的。
因此,为了将来的增强,您需要从一个松散耦合的系统开始,这样这两个层次(电子产品及其价格)中的任何一个都可以独立增长。桥接模式非常适合这种情况。因此,当您使用桥接模式时,结构可能看起来如图 12-3 所示。
图 12-3
使用桥接模式维护两个独立的层次结构
现在让我们从一个桥模式最常见的类图开始(见图 12-4 )。
图 12-4
经典的桥梁模式
在这个类图中,
-
Abstraction
定义抽象接口,维护Implementor
引用。在我的例子中,它是一个抽象类,但是非常重要的是要注意,你不应该假设你需要一个抽象类或接口来定义一个抽象。重要的是要知道这里的单词 abstraction 关于去除复杂性的方法说了什么。这些方法只是对客户端代码隐藏了它们工作的内部细节。 -
RefinedAbstraction
(一个具体类)扩展了Abstraction
定义的接口。这是客户在演示 1 中使用的。 -
定义实现类的接口。这个接口方法不必与抽象方法完全对应。通常,它包括原语操作,抽象定义了基于这些原语的高级操作。还要注意,抽象类方法和实现者方法之间不需要一对一的映射。您可以在抽象类方法中使用实现者方法的组合。演示 2 说明了这一点,也可以参考 Q & A 12.5。
-
ConcreteImplementor
(一个具体的类)实现了Implementor
接口。
在即将到来的演示中,我遵循类似的设计。供您参考,我用注释指出了实现中的所有参与者。
类图
图 12-5 显示了类图。
图 12-5
类图
解决方案资源管理器视图
图 12-6 显示了程序的高层结构。
图 12-6
解决方案资源管理器视图
演示 1
在这个例子中,ElectronicGoods
是我们的抽象类。它被放置在层级 1 中。该类定义如下。
// Abstraction
public abstract class ElectronicGoods
{
public IPrice Price { get; set; }
public string ProductType { get; set; }
public abstract void Details();
}
IPrice
接口是我们的实现者接口。它维护第二个层次结构,定义如下。
// Implementor
public interface IPrice
{
void DisplayDetails(string product);
}
Television
是覆盖Details()
方法的具体抽象类,如下所示。
// Refined Abstraction
public class Television : ElectronicGoods
{
/*
* Implementation specific:
* Delegating the task
* to the Implementor object.
*/
public override void Details()
{
Price.DisplayDetails(ProductType);
}
}
通过支持注释,您可以看到在Details()
方法中,我从另一个层次结构中调用了DisplayDetails()
方法,并传递了关于产品类型的信息。
具体的实现者(OnlinePrice, ShowroomPrice
)捕获这些信息并在DisplayDetails(...)
中使用它们。两个具体的实现是相似的。下面展示了其中的一个,供您参考。
// This is ConcreteImplementor-1
// OnlinePrice class
public class OnlinePrice : IPrice
{
public void DisplayDetails(string productType)
{
Console.Write($"\n{productType} price at online is : 2000$");
}
}
为了简单起见,我没有改变演示 1 中的价格,但是在演示 2 中,您会注意到使用这种模式的灵活性,我也改变了价格。现在进行完整的演示,如下所示。
using System;
namespace BridgePattern
{
// Abstraction
public abstract class ElectronicGoods
{
public IPrice Price { get; set; }
public string ProductType { get; set; }
public abstract void Details();
}
// Refined Abstraction
public class Television : ElectronicGoods
{
/*
* Implementation specific:
* Delegating the task
* to the Implementor object.
*/
public override void Details()
{
Price.DisplayDetails(ProductType);
}
}
// Implementor
public interface IPrice
{
void DisplayDetails(string product);
}
// This is ConcreteImplementor-1
// OnlinePrice class
public class OnlinePrice : IPrice
{
public void DisplayDetails(string productType)
{
Console.Write($"\n{productType} price at online is : 2000$");
}
}
// This is ConcreteImplementor-2
// ShowroomPrice class
public class ShowroomPrice : IPrice
{
public void DisplayDetails(string productType)
{
Console.Write($"\n{productType} price at showroom is : 3000$");
}
}
// Client code
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Bridge Pattern Demo.***");
Console.WriteLine("Verifying the market price of a television.");
ElectronicGoods eItem = new Television();
eItem.ProductType = "Sony Television";
// Verifying online price
IPrice price = new OnlinePrice();
eItem.Price = price;
eItem.Details();
// Verifying showroom price
price = new ShowroomPrice();
eItem.Price = price;
eItem.Details();
}
}
}
输出
这是输出。
***Bridge Pattern Demo.***
Verifying the market price of a television.
Sony Television price at online is : 2000$
Sony Television price at showroom is : 3000$
附加实现
我在这一章中包含了一个额外的实现,以帮助您了解使用这一模式的灵活性。在这个例子中,我使用了构造函数,而不是属性。但是在我向您展示灵活性之前,让我们假设卖方对出售的产品提供折扣。
为了适应这一点,在这个实现中,让我们在抽象类中添加下面的方法(ElectronicGoods
)。
// Additional method
public void Discount(int percentage)
{
price.GetDiscount(percentage);
}
以及实现接口中的以下方法(IPrice
)。
void GetDiscount(int percentage);
因为Discount
方法是而不是抽象的,所以Television
类或者ElectronicGoods
的任何派生类继承了这个方法。但是由于在IPrice
接口中添加了GetDiscount(int percentage)
方法,具体的实现者需要实现这个方法。下面是来自OnlinePrice
类实现者的这样一个实现。
public void GetDiscount(int percentage)
{
Console.Write($"\nAt online, you can get upto {percentage}% discount.");
}
Note
同样,这些修改只是为了提供对折扣方法的支持。你应该感觉不到原来的桥模式受到改变的影响。为了保持演示 1 简短,我没有包括这个方法。
现在是灵活性部分。让我们假设卖家想卖叫做 DVD 的电子产品。卖家有时会对所有产品打折,但在节日期间,只对 DVD 提供额外折扣。
因此,DVD 类现在需要包含另一种方法来提供双重折扣(正常折扣+附加折扣)。你不能在ElectronicGoods
抽象类中添加这个方法,因为在那种情况下,Television 类也会有你不想要的这个方法。最重要的是,尽管包含了 DVD 类,但是旧的代码结构不能改变。
桥接模式解决了这个问题。类图给了你一个线索。除此之外,请注意我是如何在 DVD 类中实现以下方法的。
// Specific method in DVD
public void DoubleDiscount()
{
// Normal discount(10%)
Discount(10);
// Festive season additional discount(5%)
Discount(5);
}
Note
你可以看到在DoubleDiscount()
方法内部,使用了ElectronicGoods
的Discount(...)
方法,所以我是按照超类抽象来编码的,它允许抽象和实现独立变化。
因为我使用了构造函数而不是属性,所以让我们先来看看变化。以下是用Details(...)
和Discount(...)
方法进行的抽象。
// Abstraction
public abstract class ElectronicGoods
{
//public IPrice Price { get; set; }
private IPrice price;
public string type;
public double cost;
public ElectronicGoods(IPrice price)
{
this.price = price;
}
public void Details()
{
price.DisplayDetails(type, cost);
}
// Additional method
public void Discount(int percentage)
{
price.GetDiscount(percentage);
}
}
现在,这是第一个精炼的抽象(Television
类)。在这个类中,没有定义新的方法,这仅仅意味着Television
类准备使用它的父类方法,并且不希望提供任何新的行为。
// Refined Abstraction-1
// Television class uses the default discount method.
public class Television : ElectronicGoods
{
public Television(IPrice price):base(price)
{
this.type = "Television";
this.cost = 2000;
}
// No additional method exists for Television
}
下面是我们第二个精炼抽象(DVD
类),是新加入的。在这个类中,定义了一个名为DoubleDiscount(...)
的新方法,这仅仅意味着客户端可以使用这个特定于 DVD 类的方法。这个方法是在超类抽象中编码的,其他层次结构不会因为这个 DVD 类的添加而受到影响。(我的意思是,由于在层级 1 中添加了 DVD 类(或任何其他类似的类),您不需要更改位于层级 2 中的ShowroomPrice
、OnlinePrice
等。即使您向抽象类添加了一些额外的方法,您也不需要对层次结构 2 进行更改。类似地,如果您在 implementor 中添加一个方法,您不需要在 hierarchy 1 中进行更改。)
Note
简而言之,这里您将“客户端使用的方法”与“这些方法是如何实现的”分开
// Refined Abstraction-2
// DVD class can give additional discount.
public class DVD : ElectronicGoods
{
public DVD(IPrice price) : base(price)
{
this.type = "DVD";
this.cost = 3000;
}
// Specic method in DVD
public void DoubleDiscount()
{
// Normal discount(10%)
Discount(10);
// Festive season additional discount(5%)
Discount(5);
}
}
对照图 12-7 所示的类图。然后直接按照完整的演示输出。对于这个修改后的实现,我没有显示 Solution Explorer 视图,因为根据前面的讨论和下面的类图,它很容易理解。
图 12-7
演示 2 的类图
类图
图 12-7 显示了修改后的类图。
演示 2
下面是完整的实现。
using System;
namespace BridgePatternDemo2
{
// Abstraction
public abstract class ElectronicGoods
{
//public IPrice Price { get; set; }
private IPrice price;
public string type;
public double cost;
public ElectronicGoods(IPrice price)
{
this.price = price;
}
public void Details()
{
price.DisplayDetails(type,cost);
}
// additional method
public void Discount(int percentage)
{
price.GetDiscount(percentage);
}
}
// Refined Abstraction-1
// Television class uses the default discount method.
public class Television : ElectronicGoods
{
public Television(IPrice price):base(price)
{
this.type = "Television";
this.cost = 2000;
}
// No additional method exists for Television
}
// Refined Abstraction-2
// DVD class can give additional discount.
public class DVD : ElectronicGoods
{
public DVD(IPrice price) : base(price)
{
this.type = "DVD";
this.cost = 3000;
}
// Specic method in DVD
public void DoubleDiscount()
{
// Normal discount(10%)
Discount(10);
// Festive season additional discount
Discount(5);
}
}
// Implementor
public interface IPrice
{
void DisplayDetails(string product, double price);
// additional method
void GetDiscount(int percentage);
}
// This is ConcreteImplementor-1
// OnlinePrice class
public class OnlinePrice : IPrice
{
public void DisplayDetails(string productType, double price)
{
Console.Write($"\n{productType} price at online is : {price}$");
}
public void GetDiscount(int percentage)
{
Console.Write($"\nAt online, you can get upto {percentage}% discount.");
}
}
// This is ConcreteImplementor-2
// ShowroomPrice class
public class ShowroomPrice : IPrice
{
public virtual void DisplayDetails(string productType, double price)
{
// Showroom price is 300$ more
Console.Write($"\n{productType} price at showroom is : {price + 300}$");
}
public void GetDiscount(int percentage)
{
Console.Write($"\nAt showroom, additional {percentage}% discount can be approved.");
}
}
// Client code
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Alternative Implementation of Bridge Pattern.***");
#region Television details
Console.WriteLine("Verifying the market price of a television.");
ElectronicGoods eItem = new Television(new OnlinePrice());
// Verifying online price details
eItem.Details();
// Giving 10% discount
eItem.Discount(10);
// Verifying showroom price
eItem = new Television(new ShowroomPrice());
eItem.Details();
// Giving 10% discount
eItem.Discount(10);
#endregion
#region DVD details
Console.WriteLine("\n\nNow checking the DVD details.");
// Verifying online price
eItem = new DVD(new OnlinePrice());
eItem.Details();
// Giving 10% discount
eItem.Discount(10);
// Verifying showroom price
eItem = new DVD(new ShowroomPrice());
eItem.Details();
Console.WriteLine("\nIn showroom, you want to give double discounts at festive season.");
Console.WriteLine("For DVD, you can get double discounts using the DoubleDiscount() method.");
//eItem.Discount();
Console.WriteLine("For example, in festive season:");
((DVD)eItem).DoubleDiscount();
#endregion
}
}
}
输出
***Alternative Implementation of Bridge Pattern.***
Verifying the market price of a television.
Television price at online is : 2000$
At online, you can get upto 10% discount.
Television price at showroom is : 2300$
At showroom, additional 10% discount can be approved.
Now checking the DVD details.
DVD price at online is : 3000$
At online, you can get upto 10% discount.
DVD price at showroom is : 3300$
In showroom, you want to give double discounts at festive season.
For DVD , you can get double discounts using the DoubleDiscount() method.
For example, in festive season:
At showroom, additional 10% discount can be approved.
At showroom, additional 5% discount can be approved.
问答环节
12.1 这种模式如何让我的 编程 生活更轻松?
本章介绍了两个具有以下主要意图的示例。
-
避免项目及其相应价格之间的紧密耦合
-
维护两个不同的层次结构,在这两个层次结构中,两者都可以扩展而不会相互影响
-
处理实现在它们之间共享的多个对象
你可以使用简单的子类化来代替这种设计。这是正确的吗?
不。通过简单的子类化,你的实现不能动态变化。您的实现可能看起来行为不同,但是它们在编译时被绑定到抽象。
12.3 我可以在抽象类中使用构造函数而不是属性吗?
是的。有些开发人员更喜欢构造函数而不是属性(或者 getter-setter 方法)。因此,我在两个演示中向您展示了这两种用法。
使用桥梁设计模式的主要优势是什么?
以下是一些优点。
-
实现不局限于抽象。
-
抽象和实现都可以独立发展。
-
具体类独立于接口实现者类。换句话说,其中一个的变化不会影响另一个。因此,您也可以用不同的方式改变抽象和实现层次。
12.5 与此模式相关的 挑战 有哪些?
整体结构可能变得复杂。这里你不直接调用一个方法。相反,抽象层将工作委托给实现层。因此,在执行操作时,您可能会注意到轻微的性能影响。
有时,桥接模式与适配器模式相混淆。(请记住,适配器模式的主要目的是只处理不兼容的接口。)
当你使用抽象类方法时,你可以结合使用实现者方法。在演示 2 中,您会看到这一点。”你能详细说明一下吗?
演示 2 中的DoubleDiscount()
方法显示了这一点,其中您调用了两次Discount()
方法。再举一个例子,假设实现者有下面的GiveThanks()
方法。
public interface IPrice
{
void DisplayDetails(string product, double price);
// Additional method
void GetDiscount(int percentage);
// Added for Q&A session
void GiveThanks();
}
具体实现者实现了该方法。假设OnlinePrice
如下实现了这个方法。
public void GiveThanks()
{
Console.Write("Thank you, please visit the site again.");
}
另一个具体的实现者ShowroomPrice
如下实现这个方法。
public void GiveThanks()
{
Console.Write("Thank you for coming. please visit the shop again.");
}
现在,在抽象内部,您可以添加这个方法(如果您愿意)。例如,您更新后的Discount
可能如下所示。
// Additional method
public void Discount(int percentage)
{
price.GetDiscount(percentage);
// Added for Q&A session
price.GiveThanks();
}
当您使用这些更改运行程序(演示 2)时,您会看到以下修改后的输出。
***Alternative Implementation of Bridge Pattern.***
Verifying the market price of a television.
Television price at online is : 2000$
At online, you can get upto 10% discount.Thank you, please visit the site again.
Television price at showroom is : 2300$
At showroom, additional 10% discount can be approved. Thank you for coming. Please visit the shop again.
Now checking the DVD details.
DVD price at online is : 3000$
At online, you can get upto 10% discount. Thank you, please visit the site again.
DVD price at showroom is : 3300$
In showroom, you want to give double discounts at festive season.
For DVD , you can get double discounts using the DoubleDiscount() method.
For example, in festive season:
At showroom, additional 10% discount can be approved. Thank you for coming. Please visit the shop again.
At showroom, additional 5% discount can be approved. Thank you for coming. Please visit the shop again.
Note
一个高级抽象方法可以包含多个实现者方法,但是客户可能不知道这一点。
十三、访问者模式
本章介绍访问者模式。
GoF 定义
表示要在对象结构的元素上执行的操作。Visitor 允许您定义一个新的操作,而不改变它所操作的元素的类。
概念
在这个模式中,您将算法从对象结构中分离出来。因此,您可以在对象上添加新的操作,而无需修改它们现有的体系结构。该模式支持打开/关闭原则(即允许扩展,但不允许修改实体,如类、函数等)。
Note
当您将这种设计模式与组合模式相结合时,您可以体验到这种设计模式的真正威力,如本章后面的实现所示。
为了理解这种模式,让我们考虑一个场景,其中有一个名为Number
的抽象类,如下所示。
/// <summary>
/// Abstract class- Number
/// </summary>
abstract class Number
{
private int numberValue;
private string type;
public Number(string type, int number)
{
this.type = type;
this.numberValue = number;
}
// I want to restrict the change in original data
// So, no setter is present here.
public int NumberValue
{
get
{
return numberValue;
}
}
public string TypeInfo
{
get
{
return type;
}
}
public abstract void SomeMethod();
}
从Number
派生出两个具体的类SmallNumber
和BigNumber
,定义如下。
/// <summary>
/// Concrete class-SmallNumber
/// </summary>
class SmallNumber : Number
{
public SmallNumber(string type, int number) : base(type, number)
{ }
public override void SomeMethod()
{
// Some code
}
}
/// <summary>
/// Concrete class-BigNumber
/// </summary>
class BigNumber : Number
{
public BigNumber(string type, int number) : base(type, number)
{ }
public override void SomeMethod
{
// Some code
}
}
这种继承层次很容易理解。现在让我们来看一段你和顾客之间的假想对话。
客户:我希望您创建一个设计,其中每个具体的类都有一个增加数值的方法。
你:那容易。我将在 Number 类中引入一个公共方法,结果是每个具体的类都可以获得该方法。
顾客:等等。我希望您使用一个递增数字的方法,但是在每次调用SmallNumber
类中的方法时,它应该将数字递增 1,对于BigNumber
类,它应该将数字递增 10。
你:那不成问题。我可以在Number
类中定义一个抽象方法,在每个派生类中,你可以不同地实现它。
顾客:我没问题。
您可以一次性接受这个客户请求,但是如果您的客户经常要求类似的请求,您是否有可能在每个类中引入这样的方法,特别是当整个代码结构非常复杂的时候?还有,在一个树形结构中,如果只是一个分支节点,你能想象这些变化对其他节点的影响吗?
这一次你可能会明白问题所在,可能会想出一些办法来对付你那些善变的顾客。访问者模式可以在这种情况下帮助你。您可以在演示 1 中看到这样的实现。
真实世界的例子
想象一个出租车预订的场景。当出租车到达你家门口,你进入车内,出租车司机控制交通。他可以通过一条你不熟悉的路线带你去目的地,最糟糕的情况下,还可以更改目的地(由于访客模式使用不当而产生)。
计算机世界的例子
当公共 API 需要支持插件操作时,这种模式非常有用。然后,客户端可以在不修改源代码的情况下对一个类(使用访问类)执行它们想要的操作。
履行
让我们继续讨论访问者模式。你可以看到图 13-1 中的类图。它向您提示了我在接下来的演示中是如何实现它的。我引入了一个新的层次结构,其中,在顶层,有一个名为IVisitor
的接口,带有两个名为VisitBigNumbers(..)
和VisitSmallNumbers(..)
的方法。看起来是这样的。
interface IVisitor
{
// A visit operation for SmallNumber class
void VisitSmallNumbers(SmallNumber number);
// A visit operation for BigNumber class
void VisitBigNumbers(BigNumber number);
}
Note
代替使用不同的名称(VisitSmallNumbers(..),VisitBigNumbers(...))对于这些方法,您可以使用相同的方法(例如,VisitNumbers(...))通过使用方法重载。在问答环节,我讨论了在这个例子中使用不同名称的原因。
IncrementNumberVisitor
实现这个接口方法,如下所示。
class IncrementNumberVisitor : IVisitor
{
public void VisitSmallNumbers(SmallNumber number)
{
Number currentNumber = number as Number;
/*
I do not want (infact I can't change because it's readonly now) to modify the original data. So, I'm making a copy of it before I use it.
*/
int temp = currentNumber.NumberValue;
// For SmallNumber's incrementing by 1
Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I use it as:{++temp} for rest of my code.");
// Remaining code, if any
}
public void VisitBigNumbers(BigNumber number)
{
Number currentNumber = number as Number;
/*
* I do not want (infact I can't change because it's readonly now)
* to modify the original data.
* So, I'm making a copy of it before I use it.
*/
int temp = currentNumber.NumberValue;
// For BigNumber's incrementing by 10
Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I convert it as:{temp+10} for rest of my code.");
// Remaining code, if any
}
}
值得注意的一点是,我不想修改原始数据。因此,在Number
类中,您只能看到 getter 方法。这是因为我假设一旦你从具体的Number
类中获得数据,你可以用不同的方式使用它,但是你不允许改变原始数据。(这是一个更好的做法,但这是可选的)。
在这个例子中,我维护了一个名为numberList
的List
数据结构,它用不同类型的数字初始化一个对象结构。因此,在演示 1 中,您会得到以下代码段。
class NumberCollection
{
List<Number> numberList = new List<Number>();
// List contains both SmallNumber's and BigNumber's
public NumberCollection()
{
numberList.Add(new SmallNumber("small-1", 10));
numberList.Add(new SmallNumber("small-2", 20));
numberList.Add(new SmallNumber("small-3", 30));
numberList.Add(new BigNumber("big-1", 200));
numberList.Add(new BigNumber("big-2", 150));
numberList.Add(new BigNumber("big-3", 70));
}
// remaining code
同样,您可以用这种方式初始化列表,或者一旦您初始化了一个空列表,您可以使用AddNumberToList(...)
方法在客户端代码中提供列表的元素。类似地,您可以使用RemoveNumberFromList(...)
方法从列表中删除一个元素。在演示 1 中,我没有使用这些方法,但是我保留了它们供您参考。所以,注意以下方法。
public void AddNumberToList(Number number)
{
numberList.Add(number);
}
public void RemoveNumberFromList(Number number)
{
numberList.Remove(number);
}
现在我们来看最重要的部分。在Number
类中,您会看到下面一行。
public abstract void Accept(IVisitor visitor);
来自Number
的具体派生类根据需要覆盖它。例如,SmallNumber
会按如下方式覆盖它。
public override void Accept(IVisitor visitor)
{
visitor.VisitSmallNumbers(this);
}
并且BigNumber
实现如下。
public override void Accept(IVisitor visitor)
{
visitor.VisitBigNumbers(this);
}
您可以看到,在Accept
方法中,您可以传递一个“特定的访问者对象”,它反过来可以跨类调用适当的方法。SmallNumber
和BigNumber
类都通过这种方法暴露自己(这里封装受到了损害)。现在客户机与访问者进行交互,您可以在访问者层次结构中添加新方法。因此,在客户端代码中,您会注意到如下代码段。
NumberCollection numberCollection = new NumberCollection();
// some other code
// ....
IncrementNumberVisitor incrVisitor = new IncrementNumberVisitor();
// Visitor is visiting the list
Console.WriteLine("IncrementNumberVisitor is about to visit the list:");
numberCollection.Accept(incrVisitor);
类图
图 13-1 为类图。这一次,我希望您在类图中显示完整的方法签名,因此,为了在一个公共位置容纳所有内容,参与者的大小变得比通常要小。
图 13-1
类图
解决方案资源管理器视图
图 13-2 显示了程序的高层结构。
图 13-2
解决方案资源管理器视图
演示 1
这是完整的代码。
using System;
using System.Collections.Generic;
namespace VisitorPattern
{
/// <summary>
/// Abstract class- Number
/// </summary>
abstract class Number
{
private int numberValue;
private string type;
public Number(string type, int number)
{
this.type = type;
this.numberValue = number;
}
//I want to restrict the change in original data
//So, no setter is present here.
public int NumberValue
{
get
{
return numberValue;
}
}
public string TypeInfo
{
get
{
return type;
}
}
public abstract void Accept(IVisitor visitor);
}
/// <summary>
/// Concrete class-SmallNumber
/// </summary>
class SmallNumber : Number
{
public SmallNumber(string type, int number) : base(type, number)
{ }
public override void Accept(IVisitor visitor)
{
visitor.VisitSmallNumbers(this);
}
}
/// <summary>
/// Concrete class-BigNumber
/// </summary>
class BigNumber : Number
{
public BigNumber(string type, int number) : base(type, number)
{ }
public override void Accept(IVisitor visitor)
{
visitor.VisitBigNumbers(this);
}
}
class NumberCollection
{
List<Number> numberList = new List<Number>();
//List contains both SmallNumber's and BigNumber's
public NumberCollection()
{
numberList.Add(new SmallNumber("small-1", 10));
numberList.Add(new SmallNumber("small-2", 20));
numberList.Add(new SmallNumber("small-3", 30));
numberList.Add(new BigNumber("big-1", 200));
numberList.Add(new BigNumber("big-2", 150));
numberList.Add(new BigNumber("big-3", 70));
}
public void AddNumberToList(Number number)
{
numberList.Add(number);
}
public void RemoveNumberFromList(Number number)
{
numberList.Remove(number);
}
public void DisplayList()
{
Console.WriteLine("Current list is as follows:");
foreach (Number number in numberList)
{
Console.Write(number.NumberValue+"\t");
}
Console.WriteLine();
}
public void Accept(IVisitor visitor)
{
foreach (Number n in numberList)
{
n.Accept(visitor);
}
}
}
/// <summary>
/// The Visitor interface.
/// GoF suggests to make visit opearation for each concrete class of /// ConcreteElement (in our example,SmallNumber and BigNumber) in the /// object structure
/// </summary>
interface IVisitor
{
//A visit operation for SmallNumber class
void VisitSmallNumbers(SmallNumber number);
//A visit operation for BigNumber class
void VisitBigNumbers(BigNumber number);
}
/// <summary>
/// A concrete visitor-IncrementNumberVisitor
/// </summary>
class IncrementNumberVisitor : IVisitor
{
public void VisitSmallNumbers(SmallNumber number)
{
Number currentNumber = number as Number;
/*
I do not want( infact I can't change because it's readonly now) to modify the original data. So, I'm making a copy of it before I use it.
*/
int temp = currentNumber.NumberValue;
//For SmallNumber's incrementing by 1
Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I use it as:{++temp} for rest of my code.");
//Remaining code, if any
}
public void VisitBigNumbers(BigNumber number)
{
Number currentNumber = number as Number;
/*
I do not want( infact I can't change because it's readonly now) to modify the original data. So, I'm making a copy of it before I use it.
*/
int temp = currentNumber.NumberValue;
//For BigNumber's incrementing by 10
Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I convert it as:{temp+10} for rest of my code.");
//Remaining code, if any
}
}
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Visitor Pattern Demo***\n");
NumberCollection numberCollection = new NumberCollection();
//Showing the current list
numberCollection.DisplayList();
IncrementNumberVisitor incrVisitor = new IncrementNumberVisitor();
//Visitor is visiting the list
Console.WriteLine("IncrementNumberVisitor is about to visit the list:");
numberCollection.Accept(incrVisitor);
//Showing the current list
numberCollection.DisplayList();
Console.ReadLine();
}
}
}
输出
这是输出。
***Visitor Pattern Demo***
Current list is as follows:
10 20 30 200 150 70
IncrementNumberVisitor is about to visit the list:
small-1 is 10; I use it as:11 for rest of my code.
small-2 is 20; I use it as:21 for rest of my code.
small-3 is 30; I use it as:31 for rest of my code.
big-1 is 200; I convert it as:210 for rest of my code.
big-2 is 150; I convert it as:160 for rest of my code.
big-3 is 70; I convert it as:80 for rest of my code.
Current list is as follows:
10 20 30 200 150 70
问答环节
13.1 什么时候应该考虑实现访问者设计模式?
这里有一些要考虑的用例。
-
您需要向一组对象添加新的操作,而不改变它们对应的类。这是实现访问者模式的主要目的。当运营经常变化时,这种方法可以成为你的救星。
-
如果需要更改各种操作的逻辑,只需通过访问者实现即可。
这种模式有什么缺点吗?
这种模式有一些缺点。
-
我前面提到过,封装不是它的主要关注点。因此,您可以使用访问者来打破封装的力量。
-
如果您需要频繁地向现有架构添加新的具体类,那么访问者层次结构将变得难以维护。例如,假设您想在原来的层次结构中添加另一个具体的类。在这种情况下,您需要相应地修改 visitor 类的层次结构。
13.3 你为什么说一个 visitor 类会违反 封装 ?
请注意,在Accept
方法中,您可以传递一个“特定的访问者对象”,它反过来可以跨类调用适当的方法。SmallNumber
和BigNumber
类都通过这种方法暴露自己,这里封装性受到了损害。
此外,在许多情况下,您可能会看到访问者需要在一个复合结构中四处移动,以从其中收集信息,然后它可以使用这些信息进行修改。(尽管在演示 1 中,我不允许这种修改)。所以,当你提供这种支持时,你违背了封装的核心目标。
13.4 为什么这种模式会损害封装?
在这里,您对一组也可能是异构的对象执行一些操作。但是您的约束是您不能改变它们对应的类。因此,您的访问者需要一种方法来访问这些对象的成员。为了满足这一要求,您需要向访问者公开信息。
13.5 在演示 1 中,我看到在 visitor 接口中,你是 而不是 使用方法重载 的概念。例如,您编写了如下的接口方法。
// A visit operation for SmallNumber class
void VisitSmallNumbers(SmallNumber number);
// A visit operation for BigNumber class
void VisitBigNumbers(BigNumber number);
在我看来,你可以使用类似下面这样的东西。
// A visit operation for SmallNumber class
void VisitNumbers(SmallNumber number);
// A visit operation for BigNumber class
void VisitNumbers(BigNumber number);
这是正确的吗?
接得好。是的,你可以这样做,但是我想让你注意到这些方法在做不同的工作(一个是将 int 增加 1,另一个是增加 10)。通过使用不同的名称,当你浏览代码时,我试图在Number
类层次结构中区分它们。
在《Java 设计模式(a press,2018)一书中,我使用了你提到的方法。你只需要记住这些接口方法应该只针对特定的类,比如SmallNumber
或者BigNumber
。
在演示 2 中,我将访问者模式与组合模式相结合,使用了重载方法。
13.6 假设在演示 1 中,我增加了 Number
的另一个具体子类叫做 UndefinedNumber
。我应该如何进行?我应该在访问者界面中使用另一个特定的方法吗?
没错。您需要定义一个特定于这个新类的新方法。因此,您的接口可能如下所示(这里使用了方法重载)。
interface IVisitor
{
// A visit operation for SmallNumber class
void VisitNumbers(SmallNumber number);
// A visit operation for BigNumber class
void VisitNumbers(BigNumber number);
// A visit operation for UndefinedNumber class
void VisitNumbers(UndefinedNumber number);
}
然后,您需要在具体的 visitor 类中实现这个新方法。
假设,我需要在现有架构中支持新的操作。我应该如何处理访问者模式?
对于每个新操作,创建一个新的 Visitor 子类,并在其中实现操作。然后,按照我在前面的例子中向您展示的方式访问您现有的结构。例如,如果您想要调查SmallNumber
类实例的int
值是否大于 10,以及对于BigNumber
类,它们是否大于 100 的方法。对于这个需求,您可以添加一个新的具体类,InvestigateNumberVisitor,
,它继承自IVisitor
,定义如下。
/// <summary>
/// Another concrete visitor-InvestigateNumberVisitor
/// </summary>
class InvestigateNumberVisitor : IVisitor
{
public void VisitSmallNumbers(SmallNumber number)
{
Number currentNumber = number as Number;
int temp = currentNumber.NumberValue;
// Checking whether the number is greater than 10 or not
string isTrue = temp > 10 ? "Yes" : "No";
Console.WriteLine($"Is {currentNumber.TypeInfo} greater than 10 ? {isTrue}");
}
public void VisitBigNumbers(BigNumber number)
{
Number currentNumber = number as Number;
int temp = currentNumber.NumberValue;
// Checking whether the number is greater than 100 or not
string isTrue = temp > 100 ? "Yes" : "No";
Console.WriteLine($"Is {currentNumber.TypeInfo} greater than 100 ? {isTrue}");
}
}
现在,在客户端代码中,您可以添加下面的代码段来检查它是否正常工作。
// Visitor-2
InvestigateNumberVisitor investigateVisitor = new InvestigateNumberVisitor();
// Visitor is visiting the list
Console.WriteLine("InvestigateNumberVisitor is about to visit the list:");
numberCollection.Accept(investigateVisitor);
一旦你在demonstration 1,
中添加了这些段,使用如下的客户端代码。
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Visitor Pattern Demo2.***\n");
NumberCollection numberCollection = new NumberCollection();
// Showing the current list
numberCollection.DisplayList();
// Visitor-1
IncrementNumberVisitor incrVisitor = new IncrementNumberVisitor();
// Visitor is visiting the list
Console.WriteLine("IncrementNumberVisitor is about to visit the list:");
numberCollection.Accept(incrVisitor);
// Visitor-2
InvestigateNumberVisitor investigateVisitor = new InvestigateNumberVisitor();
// Visitor is visiting the list
Console.WriteLine("InvestigateNumberVisitor is about to visit the list:");
numberCollection.Accept(investigateVisitor);
Console.ReadLine();
}
}
运行该程序时,您可以获得以下输出。
***Visitor Pattern Demo2.***
Current list is as follows:
10 20 30 200 150 70
IncrementNumberVisitor is about to visit the list:
Original data:10; I use it as:11
Original data:20; I use it as:21
Original data:30; I use it as:31
Original data:200; I use it as:210
Original data:150; I use it as:160
Original data:70; I use it as:80
InvestigateNumberVisitor is about to visit the list:
Is small-1 greater than 10 ? No
Is small-2 greater than 10 ? Yes
Is small-3 greater than 10 ? Yes
Is big-1 greater than 100 ? Yes
Is big-2 greater than 100 ? Yes
Is big-3 greater than 100 ? No
您可以从 Apress 网站下载这个修改示例的完整代码。我将它合并到名为 VisitorPatternDemo2 的名称空间中。
我看到你正在用 SmallNumber 和 BigNumber 的对象初始化 numberList。创建这样的结构是强制性的吗?
不。我做了一个容器,帮助客户一次就能顺利访问。在另一个不同的版本中,您可以看到,在遍历列表之前,您首先初始化一个空列表,然后在客户端代码中添加(或移除)元素。
要理解前一行,您可以参考演示 2,在演示 2 中,我只在客户端代码中创建了容器类。
一起使用访问者模式和组合模式
在演示 1 中,您看到了访问者设计模式的一个示例,并且在问答会话中,您经历了它的一个扩展版本。现在我将向您展示另一个实现,但是这一次,我将它与组合模式结合起来。
让我们考虑一下第十一章中的复合设计模式的例子。在这个例子中,有一个学院有两个不同的系。每个系都有一名系主任和多名教授/讲师。所有的 hod 都向学院的校长报告。
图 13-3 显示了本例的树形结构。学院的结构和第十一章中描述的一样。数学讲师/教师是 M. Joy 和 M. Roony,CSE 教师是 C. Sam、C. Jones 和 C. Marium。这些讲师不监管任何人,所以在树形图中被当作叶节点。S. Som 博士是校长,职位最高。两个 HOD(s . Das 夫人(HOD-Math)和 V. Sarcar 先生(HOD-Comp。Sc)向负责人报告。hod 和 principal 是非叶节点。
图 13-3
复合设计示例的树结构
现在假设学院的校长想提升一些员工。假设教学经验是晋升的唯一标准,但高级教师(分支节点)和初级教师(叶节点)之间的标准有所不同,具体如下:对于初级教师,晋升的最低标准是 12 年,高级教师是 15 年。
如果您理解示范 1,您会意识到晋升标准在将来可能会改变,并且可能会有来自上级的额外要求。因此,访问者模式非常适合满足当前的需求。这就是为什么在接下来的例子中,你会看到一个新的属性和一个新的方法被添加到了Employee
接口;支持性的评论应该很容易理解。
// Newly added for this example
// To set years of Experience
double Experience { get; set; }
// Newly added for this example
void Accept(IVisitor visitor);
按照演示 1 中的设计,让我们用名为VisitEmployee(...)
的方法制作一个名为IVisitor
的访问者接口,它有两个重载版本。这是访问者的层次结构。
/// <summary>
/// Visitor interface
/// </summary>
interface IVisitor
{
// To visit leaf nodes
void VisitEmployees(Employee employee);
// To visit composite nodes
void VisitEmployees(CompositeEmployee employee);
}
/// <summary>
/// Concrete visitor class-PromotionCheckerVisitor
/// </summary>
class PromotionCheckerVisitor : IVisitor
{
string eligibleForPromotion = String.Empty;
public void VisitEmployees(CompositeEmployee employee)
{
//We'll promote them if experience is greater than 15 years
eligibleForPromotion = employee.Experience > 15 ? "Yes" : "No";
Console.WriteLine($"\t{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");
}
public void VisitEmployees(Employee employee)
{
//We'll promote them if experience is greater than 12 years
eligibleForPromotion = employee.Experience > 12 ? "Yes" : "No";
Console.WriteLine($"\t{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");
}
}
这一次,我在客户机代码中制作容器(一个列表数据结构,称为参与者)。当访问者从这个学院结构中收集必要的详细信息时,它可以显示符合晋升条件的候选人,这就是包含以下代码段的原因。
Console.WriteLine("\n***Visitor starts visiting our composite structure***\n");
IVisitor visitor = new PromotionCheckerVisitor();
//Visitor is traversing the participant list
foreach ( IEmployee emp in participants)
{
emp.Accept(visitor);
}
访问者从原始的学院结构中一次一个地收集数据,而不对其进行任何修改。一旦收集过程结束,访问者分析数据以显示预期的结果。为了直观地理解这一点,你可以跟随图 13-4 到 13-8 中的箭头。校长在组织的最高层,所以你可以假设他没有得到提升。
第一步
图 13-4 为步骤 1。
图 13-4
第一步
第二步
图 13-5 为步骤 2。
图 13-5
第二步
第三步
图 13-6 为步骤 3。
图 13-6
第三步
第四步
图 13-7 为步骤 4。
图 13-7
第四步
第五步
图 13-8 为步骤 5。
图 13-8
第五步
等等...
我在演示 1 中遵循了类似的设计,代码示例建立在第十一章中唯一的演示之上。为了简洁起见,我在这个例子中没有包括类图和解决方案浏览器视图。所以,直接通过下面的实现。
演示 2
下面是实现。
using System;
using System.Collections.Generic;
namespace VisitorWithCompositePattern
{
interface IEmployee
{
//To set an employee name
string Name { get; set; }
//To set an employee department
string Dept { get; set; }
//To set an employee designation
string Designation { get; set; }
//To display an employee details
void DisplayDetails();
//Newly added for this example
//To set years of Experience
double Experience { get; set; }
//Newly added for this example
void Accept(IVisitor visitor);
}
//Leaf node
class Employee : IEmployee
{
public string Name { get; set; }
public string Dept { get; set; }
public string Designation { get; set; }
public double Experience { get; set; }
//Details of a leaf node
public void DisplayDetails()
{
Console.WriteLine($"{Name} works in { Dept} department.Designation:{Designation}.Experience : {Experience} years.");
}
public void Accept(IVisitor visitor)
{
visitor.VisitEmployees(this);
}
}
//Non-leaf node
class CompositeEmployee : IEmployee
{
public string Name { get; set; }
public string Dept { get; set; }
public string Designation { get; set; }
public double Experience { get; set; }
//The container for child objects
//private List<IEmployee> subordinateList = new List<IEmployee>();
//Making it public now
public List<IEmployee> subordinateList = new List<IEmployee>();
//To add an employee
public void AddEmployee(IEmployee e)
{
subordinateList.Add(e);
}
//To remove an employee
public void RemoveEmployee(IEmployee e)
{
subordinateList.Remove(e);
}
//Details of a composite node
public void DisplayDetails()
{
Console.WriteLine($"\n{Name} works in {Dept} department.Designation:{Designation}.Experience : {Experience} years.");
foreach (IEmployee e in subordinateList)
{
e.DisplayDetails();
}
}
public void Accept(IVisitor visitor)
{
visitor.VisitEmployees(this);
}
}
/// <summary>
/// Visitor interface
/// </summary>
interface IVisitor
{
//To visit leaf nodes
void VisitEmployees(Employee employee);
//To visit composite nodes
void VisitEmployees(CompositeEmployee employee);
}
/// <summary>
/// Concrete visitor class-PromotionCheckerVisitor
/// </summary>
class PromotionCheckerVisitor : IVisitor
{
string eligibleForPromotion = String.Empty;
public void VisitEmployees(CompositeEmployee employee)
{
/*
We'll promote them if experience is greater than 15 years.
*/
eligibleForPromotion = employee.Experience > 15 ? "Yes" : "No";
Console.WriteLine($"{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");
}
public void VisitEmployees(Employee employee)
{
/*
We'll promote them if experience is greater
than 12 years.
*/
eligibleForPromotion = employee.Experience > 12 ? "Yes" : "No";
Console.WriteLine($"{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Visitor Pattern with Composite Pattern Demo. ***");
#region Mathematics department
//2 lecturers work in Mathematics department
Employee mathTeacher1 = new Employee { Name = "M.Joy", Dept = "Mathematic", Designation = "Lecturer" ,Experience=13.7};
Employee mathTeacher2 = new Employee { Name = "M.Roony", Dept = "Mathematics", Designation = "Lecturer", Experience = 6.5 };
//The college has a Head of Department in Mathematics
CompositeEmployee hodMaths = new CompositeEmployee { Name = "Mrs.S.Das", Dept = "Maths", Designation = "HOD-Maths", Experience = 14 };
//Lecturers of Mathematics directly reports to HOD-Maths
hodMaths.AddEmployee(mathTeacher1);
hodMaths.AddEmployee(mathTeacher2);
#endregion
#region Computer Science department
//3 lecturers work in Computer Sc. department
Employee cseTeacher1 = new Employee { Name = "C.Sam", Dept = "Computer Science", Designation = "Lecturer", Experience = 10.2 };
Employee cseTeacher2 = new Employee { Name = "C.Jones", Dept = "Computer Science.", Designation = "Lecturer", Experience = 13.5 };
Employee cseTeacher3 = new Employee { Name = "C.Marium", Dept = "Computer Science", Designation = "Lecturer", Experience = 7.3 };
//The college has a Head of Department in Computer science
CompositeEmployee hodCompSc = new CompositeEmployee { Name = "Mr. V.Sarcar", Dept = "Computer Sc.", Designation = "HOD-Computer Sc.", Experience = 16.5 };
//Lecturers of Computer Sc. directly reports to HOD-CSE
hodCompSc.AddEmployee(cseTeacher1);
hodCompSc.AddEmployee(cseTeacher2);
hodCompSc.AddEmployee(cseTeacher3);
#endregion
#region Top level management
//The college also has a Principal
CompositeEmployee principal = new CompositeEmployee { Name = "Dr.S.Som", Dept = "Planning-Supervising-Managing", Designation = "Principal", Experience = 21 };
/*
Head of Departments's of Maths and Computer Science directly reports to Principal.
*/
principal.AddEmployee(hodMaths);
principal.AddEmployee(hodCompSc);
#endregion
/*
Printing the leaf-nodes and branches in the same way i.e. in each case, we are calling DisplayDetails() method.
*/
Console.WriteLine("\nDetails of a college structure is as follows:");
//Prints the complete structure
principal.DisplayDetails();
List<IEmployee> participants = new List<IEmployee>();
//For employees who directly reports to Principal
foreach (IEmployee e in principal.subordinateList)
{
participants.Add(e);
}
//For employees who directly reports to HOD-Maths
foreach (IEmployee e in hodMaths.subordinateList)
{
participants.Add(e);
}
//For employees who directly reports to HOD-Comp.Sc
foreach (IEmployee e in hodCompSc.subordinateList)
{
participants.Add(e);
}
Console.WriteLine("\n***Visitor starts visiting our composite structure***\n");
IVisitor visitor = new PromotionCheckerVisitor();
/*
Principal is already holding the highest position.
We are not checking whether he is eligible
for promotion or not.
*/
//principal.Accept(visitor);
//Visitor is traversing the participant list
foreach ( IEmployee emp in participants)
{
emp.Accept(visitor);
}
//Wait for user
Console.ReadKey();
}
}
}
输出
这是输出。有些部分以粗体显示,表明访问者能够成功完成其工作。
***Visitor Pattern with Composite Pattern Demo. ***
Details of a college structure is as follows:
Dr.S.Som works in Planning-Supervising-Managing department.Designation:Principal.Experience : 21 years.
Mrs.S.Das works in Maths department.Designation:HOD-Maths.Experience : 14 years.
M.Joy works in Mathematic department.Designation:Lecturer.Experience : 13.7 years.
M.Roony works in Mathematics department.Designation:Lecturer.Experience : 6.5 years.
Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc..Experience : 16.5 years.
C.Sam works in Computer Science department.Designation:Lecturer.Experience : 10.2 years.
C.Jones works in Computer Science. department.Designation:Lecturer.Experience : 13.5 years.
C.Marium works in Computer Science department.Designation:Lecturer.Experience : 7.3 years.
***Visitor starts visiting our composite structure***
Mrs.S.Das from Maths is eligible for promotion? :No
Mr. V.Sarcar from Computer Sc. is eligible for promotion? :Yes
M.Joy from Mathematic is eligible for promotion? :Yes
M.Roony from Mathematics is eligible for promotion? :No
C.Sam from Computer Science is eligible for promotion? :No
C.Jones from Computer Science. is eligible for promotion? :Yes
C.Marium from Computer Science is eligible for promotion? :No
十四、观察者模式
本章涵盖了观察者模式。
GoF 定义
定义对象之间的一对多依赖关系,这样当一个对象改变状态时,它的所有依赖对象都会得到通知并自动更新。
概念
在这个模式中,有许多观察者(对象)在观察一个特定的主体(也是一个对象)。观察者希望在对象内部发生变化时得到通知。所以,他们注册了这个科目。当他们对该主题失去兴趣时,他们就从该主题中注销。有时这种模型被称为发布者-订阅者(发布-订阅)模型。整个想法可以总结如下:使用这个模式,一个对象(subject)可以同时向多个观察者(一组对象)发送通知。观察者可以决定如何响应通知,并且可以根据通知执行特定的操作。
您可以用下面的图表来可视化这些场景。
在步骤 1 中,三个观察者请求从一个对象那里得到通知(见图 14-1 )。
图 14-1
第一步
在步骤 2 中,主体可以同意请求;换句话说,连接建立(见图 14-2 )。
图 14-2
第二步
在步骤 3 中,主题向注册用户发送通知(参见图 14-3 )。
图 14-3
第三步
在步骤 4(可选)中,observer2 不希望获得进一步的通知并请求注销自己(或者主题由于某些特定原因不希望将 observer2 保留在其通知列表中,并且他注销了 observer2)。因此,受试者和观察者 2 之间的连接已经断开(见图 14-4 )。
图 14-4
第四步
在第 5 步中,从现在开始,只有观察器 1 和观察器 3 从对象那里得到通知(见图 14-5 )。
图 14-5
第五步
真实世界的例子
想想一个在社交媒体上有很多粉丝的名人。这些追随者中的每一个都想从他们最喜爱的名人那里获得所有最新的更新。所以,他们追随名人直到兴趣减退。当他们失去兴趣时,他们就不再追随那个名人了。把这些粉丝或追随者想象成观察者,把名人想象成主体。
计算机世界的例子
让我们考虑计算机科学中一个简单的基于 UI 的例子。此用户界面连接到某个数据库。用户可以通过 UI 执行查询,在搜索数据库后,返回结果。使用这种模式,您可以将 UI 与数据库隔离开来。如果数据库发生变化,应该通知 UI,以便它可以相应地更新它的显示。
为了简化这个场景,假设您是组织中负责维护数据库的人。每当数据库发生更改时,您都希望收到通知,以便在必要时采取措施。在这种情况下,您可以注意以下几点。
-
您可以在任何 eventdriven 软件中看到这种模式的存在。像 C# 这样的现代语言有按照这种模式处理这些事件的内置支持。这些构造让你的生活更轻松。
-
如果你熟悉。NET 框架,你看到在 C# 中,你有泛型
System.IObservable<T>
和System.IObserver<T>
接口,其中泛型类型参数提供通知。
履行
对于这个例子,我创建了四个观察者(Roy, Kevin, Bose
和Jacklin)
以及两个主题(Celebrity-1 and Celebrity-2)
)。一个 subject(在我们的例子中是Celebrity
)维护一个所有注册用户的列表。当主题中的标志值发生变化时,观察者会收到通知。
最初,三个观察者(Roy、Kevin 和 Bose)注册自己以获得来自名人 1 的通知。所以,在最初阶段,他们都收到了通知。但后来,凯文对名人 1 失去了兴趣。当名人 1 号意识到这一点时,他将凯文从他的观察名单中移除。此时,只有 Roy 和 Bose 在接收通知(当标志值为 50 时)。但是凯文后来改变了主意,想要再次获得通知,所以名人 1 再次注册了他。这就是为什么当名人-1 将标志值设置为 100 时,三个观察者都收到了他的通知。
后来你看到了一个名人,名字叫名人-2。罗伊和杰克林登记在他的观察名单上。因此,当名人-2 将标志值设置为 500 时,罗伊和杰克林都收到了通知。
让我们看看代码。下面是IObserver
接口,它有一个Update(...)
方法。
interface IObserver
{
void Update(ICelebrity subject);
}
两个具体的类——ObserverType1
和ObserverType2
——向您展示了您可以拥有不同类型的观察者。这些类如下实现了IObserver
接口。
// ObserverType1
class ObserverType1 : IObserver
{
string nameOfObserver;
public ObserverType1(String name)
{
this.nameOfObserver = name;
}
public void Update(ICelebrity celeb)
{
Console.WriteLine($"{nameOfObserver} has received an alert from {celeb.Name}.Updated value is: {celeb.Flag}");
}
}
// ObserverType2
class ObserverType2 : IObserver
{
string nameOfObserver;
public ObserverType2(String name)
{
this.nameOfObserver = name;
}
public void Update(ICelebrity celeb)
{
Console.WriteLine($"{nameOfObserver} notified.Inside {celeb.Name}, the updated value is: {celeb.Flag}");
}
}
主题接口(ICelebrity
)包含三个方法,分别叫做Register(...), Unregister(...)
、NotifyRegisteredUsers()
,很容易理解。这些方法分别注册一个观察器、注销一个观察器和通知所有已注册的观察器。下面是ICelebrity
界面。
interface ICelebrity
{
// Name of Subject
string Name { get; }
int Flag { get; set; }
// To register
void Register(IObserver o);
// To Unregister
void Unregister(IObserver o);
// To notify registered users
void NotifyRegisteredUsers();
}
Celebrity
具体类实现了ICelebrity
接口。重要的一点是,这个具体的类维护一个注册用户列表。您可以在这个类中看到下面一行代码。
List<IObserver> observerList = new List<IObserver>();
Note
在这种模式的一些例子中,您可能会看到一个细微的变化,其中使用了一个抽象类来代替接口(ICelebrity
),并且列表(observerList
)在抽象类中维护。两种变化都可以。您可以实现您喜欢的方法。
我在Celebrity
类中使用了一个构造函数。构造函数如下。
public Celebrity(string name)
{
this.name = name;
}
我对不同的名人使用这个构造函数。因此,在客户端代码中,您会看到以下带有注释的行。
Console.WriteLine("Working with first celebrity now.");
ICelebrity celebrity = new Celebrity("Celebrity-1");
// some other code
// Creating another celebrity
ICelebrity celebrity2 =新名人("名人-2 ");
最后,我在Celebrity
类中使用了一个表达式体属性。你可以在这段代码中看到。
//public string Name
//{
// get
// {
// return name;
// }
//}
// Or, simply use expression bodied
// properties(C# v6.0 onwards)
public string Name => name;
Note
如果您的 C# 版本早于 6.0,那么您可以使用注释代码块。同样的评论也适用于本书中类似的代码。
剩下的代码很容易理解。如果你想的话,跟随支持的评论。
类图
图 14-6 显示了类图。
图 14-6
类图
解决方案资源管理器视图
图 14-7 显示了程序的高层结构。
图 14-7
解决方案资源管理器视图
示范
这是完整的演示。
using System;
// We have used List<Observer> here
using System.Collections.Generic;
namespace ObserverPattern
{
interface IObserver
{
void Update(ICelebrity subject);
}
class ObserverType1 : IObserver
{
string nameOfObserver;
public ObserverType1(String name)
{
this.nameOfObserver = name;
}
public void Update(ICelebrity celeb)
{
Console.WriteLine($"{nameOfObserver} has received an alert from {celeb.Name}. Updated value is: {celeb.Flag}");
}
}
class ObserverType2 : IObserver
{
string nameOfObserver;
public ObserverType2(String name)
{
this.nameOfObserver = name;
}
public void Update(ICelebrity celeb)
{
Console.WriteLine($"{nameOfObserver} notified.Inside {celeb.Name}, the updated value is: {celeb.Flag}");
}
}
interface ICelebrity
{
// Name of Subject
string Name { get; }
int Flag { get; set; }
// To register
void Register(IObserver o);
// To Unregister
void Unregister(IObserver o);
// To notify registered users
void NotifyRegisteredUsers();
}
class Celebrity : ICelebrity
{
List<IObserver> observerList = new List<IObserver>();
private int flag;
public int Flag
{
get
{
return flag;
}
set
{
flag = value;
// Flag value changed. So notify observer(s).
NotifyRegisteredUsers();
}
}
private string name;
public Celebrity(string name)
{
this.name = name;
}
//public string Name
//{
// get
// {
// return name;
// }
//}
// Or, simply use expression bodied
// properties(C#6.0 onwards)
public string Name => name;
// To register an observer.
public void Register(IObserver anObserver)
{
observerList.Add(anObserver);
}
// To unregister an observer.
public void Unregister(IObserver anObserver)
{
observerList.Remove(anObserver);
}
// Notify all registered observers.
public void NotifyRegisteredUsers()
{
foreach (IObserver observer in observerList)
{
observer.Update(this);
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Observer Pattern Demonstration.***\n");
// We have 4 observers - 2 of them are ObserverType1, 1 is of // ObserverType2
IObserver myObserver1 = new ObserverType1("Roy");
IObserver myObserver2 = new ObserverType1("Kevin");
IObserver myObserver3 = new ObserverType2("Bose");
IObserver myObserver4 = new ObserverType2("Jacklin");
Console.WriteLine("Working with first celebrity now.");
ICelebrity celebrity = new Celebrity("Celebrity-1");
// Registering the observers - Roy, Kevin, Bose
celebrity.Register(myObserver1);
celebrity.Register(myObserver2);
celebrity.Register(myObserver3);
Console.WriteLine(" Celebrity-1 is setting Flag = 5.");
celebrity.Flag = 5;
/*
Kevin doesn't want to get further notification.
So, unregistering the observer(Kevin)).
*/
Console.WriteLine("\nCelebrity-1 is removing Kevin from the observer list now.");
celebrity.Unregister(myObserver2);
// No notification is sent to Kevin this time. He has // unregistered.
Console.WriteLine("\n Celebrity-1 is setting Flag = 50.");
celebrity.Flag = 50;
// Kevin is registering himself again
celebrity.Register(myObserver2);
Console.WriteLine("\n Celebrity-1 is setting Flag = 100.");
celebrity.Flag = 100;
Console.WriteLine("\n Working with another celebrity now.");
// Creating another celebrity
ICelebrity celebrity2 = new Celebrity("Celebrity-2");
// Registering the observers-Roy and Jacklin
celebrity2.Register(myObserver1);
celebrity2.Register(myObserver4);
Console.WriteLine("\n --Celebrity-2 is setting Flag value as 500.--");
celebrity2.Flag = 500;
Console.ReadKey();
}
}
}
输出
这是输出。
***Observer Pattern Demonstration.***
Working with first celebrity now.
Celebrity-1 is setting Flag = 5.
Roy has received an alert from Celebrity-1\. Updated value is: 5
Kevin has received an alert from Celebrity-1\. Updated value is: 5
Bose notified.Inside Celebrity-1, the updated value is: 5
Celebrity-1 is removing Kevin from the observer list now.
Celebrity-1 is setting Flag = 50.
Roy has received an alert from Celebrity-1\. Updated value is: 50
Bose notified.Inside Celebrity-1, the updated value is: 50
Celebrity-1 is setting Flag = 100.
Roy has received an alert from Celebrity-1\. Updated value is: 100
Bose notified.Inside Celebrity-1, the updated value is: 100
Kevin has received an alert from Celebrity-1\. Updated value is: 100
Working with another celebrity now.
--Celebrity-2 is setting Flag value as 500.--
Roy has received an alert from Celebrity-2\. Updated value is: 500
Jacklin notified.Inside Celebrity-2, the updated value is: 500
问答环节
14.1 如果只有一个观察者,那么我不需要设置界面。这是正确的吗?
是的。但是如果你想遵循纯面向对象的编程准则,你可能总是更喜欢接口(或者抽象类)而不是具体的类。除了这一点之外,通常有多个观察者,您在契约之后实现它们。这就是你从这种设计中受益的地方。
14.2 你能有不同类型的观察者吗?
是的。在真实世界的场景中思考这个问题。当任何人对组织的数据库进行重要更改时,来自不同部门的多组人员可能希望了解该更改(例如您的老板和数据库的所有者,他们在不同的级别工作)并相应地采取行动。因此,您可能需要在应用中为不同类型的观察器提供支持。这就是为什么在这一章中,我向你展示了一个例子,涉及多名观察员和多名名人。
14.3 你能在运行时添加或删除观察者吗?
是的。请注意,在程序开始时,为了获得通知,Kevin 注册了自己。后来,他注销,然后重新注册。
在我看来,观察者模式和责任链模式有相似之处(见第 章第 22 )。这是正确的吗?
在观察者模式中,所有注册用户同时收到通知;但是在责任链模式中,链中的对象被一个接一个地通知,直到一个对象完全处理通知(或者,到达链的末端)。图 14-8 和图 14-9 总结了不同之处。
图 14-9
责任链模式
图 14-8
观察者模式
在图 14-9 中,我假设观察者 3 能够完全处理通知。所以,它是链条的末端节点。在这种情况下,您还需要记住,如果通知到达了链的末端,但是没有人正确地处理它,您可能需要采取特殊的操作。
14.5 该模型是否支持 一对多关系 ?
是的,GoF 定义证实了这一点。由于一个主题可以向多个观察者发送通知,这种依赖关系描述了一对多的关系。
14.6 有现成的构造可用(例如, System.IObservable<T>
)。你为什么不使用它们,而是自己写代码呢?
你不能改变现成的功能,但我相信当你尝试自己实现这个概念时,你会更好地理解现成的结构。
另一个需要注意的要点是,当你使用系统时。可观测的
public interface IObservable<out T>
public interface IObserver<in T>
这仅仅意味着你也需要熟悉 C# 中的协方差和逆变。起初,这些概念似乎很难。在我的书《高级 C# 入门》(Apress,2020)中,我用代码示例详细讨论了这些概念。
14.7 观察者模式的主要优势是什么?
以下是一些关键优势。
-
主体(我们例子中的名人)和他们的注册用户(观察者)组成了一个松散耦合的系统。他们不需要明确地相互了解。
-
在通知列表中添加或删除观察者时,不需要对主题进行更改。
-
此外,您可以在运行时独立地添加或删除观察器。
14.8 观察者模式的主要挑战是什么?
当您实现(或使用)这个模式时,这里有一些关键的挑战。
-
毫无疑问,在 C# 中处理事件时,内存泄漏是最大的问题(也称为失效监听器问题)。在这种情况下,自动垃圾收集器可能并不总是对您有所帮助。
-
通知的顺序不可靠。
十五、策略模式
本章涵盖了策略模式。它也被称为策略模式。
GoF 定义
定义一系列算法,封装每一个算法,并使它们可以互换。策略让算法独立于使用它的客户端而变化。
概念
客户端可以在运行时从一组算法中动态选择一个算法。这种模式还提供了一种使用所选算法的简单方法。
你知道一个对象可以有状态和行为。其中一些行为可能会因类的对象而异。这种模式侧重于在特定时间与对象相关联的变化行为。
在我们的例子中,您会看到一个Vehicle
类。您可以使用该类创建一个车辆对象。创建车辆对象后,可以向该对象添加和设置行为。在客户端代码中,您也可以用新行为替换当前行为。最有趣的是,您会看到,由于行为是可以改变的,定义行为的是而不是;它只是将任务委托给车辆引用的对象。整体实现可以让你概念更清晰。
真实世界的例子
在一场足球比赛中,如果 A 队在比赛快结束时以 1 比 0 领先 B 队,A 队不会进攻,而是防守以保持领先。与此同时,B 队全力以赴去扳平比分。
计算机世界的例子
假设您有一个备份内存插槽。如果您的主内存已满,但您需要存储更多数据,您可以使用备份内存插槽。如果您没有这个备份内存插槽,并且您试图将额外的数据存储到您的主内存中,这些数据将被丢弃(当主内存已满时)。在这些情况下,您可能会得到异常,或者您可能会遇到一些特殊的行为(基于程序的架构)。因此,在存储数据之前,运行时检查是必要的。然后你就可以继续了。
履行
在这个实现中,我只关注车辆行为的变化。在实现中,您可以看到,一旦创建了一个车辆对象,它就与一个InitialBehavior,
相关联,这个InitialBehavior,
简单地声明在这种状态下,车辆不能做任何特殊的事情。但是一旦你设置了一个FlyBehavior
,车辆就能飞起来。当你设定FloatBehavior
时,它可以浮动。所有变化的行为都在一个单独的层级中维护。
/// <summary>
/// Abstract Behavior
/// </summary>
public abstract class VehicleBehavior
{
public abstract void AboutMe(string vehicle);
}
/// <summary>
/// Floating capability
/// </summary>
class FloatBehavior : VehicleBehavior
{
public override void AboutMe(string vehicle)
{
Console.WriteLine($"My {vehicle} can float now.");
}
}
/// <summary>
/// Flying capability
/// </summary>
class FlyBehavior : VehicleBehavior
{
public override void AboutMe(string vehicle)
{
Console.WriteLine($"My {vehicle} can fly now.");
}
}
/// <summary>
/// Initial behavior. Cannot do anything special.
/// </summary>
class InitialBehavior : VehicleBehavior
{
public override void AboutMe(string vehicle)
{
Console.WriteLine($"My {vehicle} is just born.It cannot do anything special.");
}
}
在许多例子中,你会看到一个叫做上下文类的术语。Vehicle
是本演示中的上下文类。该类定义如下。
/// <summary>
/// Context class-Vehicle
/// </summary>
public class Vehicle
{
VehicleBehavior behavior;
string vehicleType;
public Vehicle(string vehicleType)
{
this.vehicleType = vehicleType;
// Setting the initial behavior
this.behavior = new InitialBehavior();
}
/*
* It's your choice. You may prefer to use a setter
* method instead of using a constructor.
* You can call this method whenever we want
* to change the "vehicle behavior" on the fly.
*/
public void SetVehicleBehavior(VehicleBehavior behavior)
{
this.behavior = behavior;
}
/*
This method will help us to delegate the behavior to
the object referenced by vehicle.You do not know about the object type, but you simply know that this object can tell something about it, i.e. "AboutMe()" method
*/
public void DisplayAboutMe()
{
behavior.AboutMe(vehicleType);
}
}
您可以看到,在构造函数内部,我设置了初始行为,稍后可以使用SetVehicleBehavior(...)
方法对其进行修改。DisplayAboutMe()
将任务委托给特定的对象。
类图
图 15-1 显示了类图的重要部分。
图 15-1
类图
解决方案资源管理器视图
图 15-2 显示了程序的高层结构。
图 15-2
解决方案资源管理器视图
示范
下面是实现。
using System;
namespace StrategyPattern
{
/// <summary>
/// Abstract Behavior
/// </summary>
public abstract class VehicleBehavior
{
public abstract void AboutMe(string vehicle);
}
/// <summary>
/// Floating capability
/// </summary>
class FloatBehavior : VehicleBehavior
{
public override void AboutMe(string vehicle)
{
Console.WriteLine($"My {vehicle} can float now.");
}
}
/// <summary>
/// Flying capability
/// </summary>
class FlyBehavior : VehicleBehavior
{
public override void AboutMe(string vehicle)
{
Console.WriteLine($"My {vehicle} can fly now.");
}
}
/// <summary>
/// Initial behavior.Cannot do anything special.
/// </summary>
class InitialBehavior : VehicleBehavior
{
public override void AboutMe(string vehicle)
{
Console.WriteLine($"My {vehicle} is just born.It cannot do anything special.");
}
}
/// <summary>
/// Context class-Vehicle
/// </summary>
public class Vehicle
{
VehicleBehavior behavior;
string vehicleType;
public Vehicle(string vehicleType)
{
this.vehicleType = vehicleType;
//Setting the initial behavior
this.behavior = new InitialBehavior();
}
/*
* It's your choice. You may prefer to use a setter
* method instead of using a constructor.
* You can call this method whenever we want
* to change the "vehicle behavior" on the fly.
*/
public void SetVehicleBehavior(VehicleBehavior behavior)
{
this.behavior = behavior;
}
/*
This method will help us to delegate the behavior to
the object referenced by vehicle.You do not know about the object type, but you simply know that this object can tell something about it, i.e. "AboutMe()" method
*/
public void DisplayAboutMe()
{
behavior.AboutMe(vehicleType);
}
}
/// <summary>
/// Client code
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Strategy Pattern Demo.***\n");
Vehicle context = new Vehicle("Aeroplane");
context.DisplayAboutMe();
Console.WriteLine("Setting flying capability to vehicle.");
context.SetVehicleBehavior(new FlyBehavior());
context.DisplayAboutMe();
Console.WriteLine("Changing the vehicle behavior again.");
context.SetVehicleBehavior(new FloatBehavior());
context.DisplayAboutMe();
Console.ReadKey();
}
}
}
输出
这是输出。
***Strategy Pattern Demo.***
My Aeroplane is just born.It cannot do anything special.
Setting flying capability to vehicle.
My Aeroplane can fly now.
Changing the vehicle behavior again.
My Aeroplane can float now.
问答环节
在我看来,你专注于改变行为让一切都变得复杂了。此外,我不明白为什么我需要上下文类。您可以简单地使用继承机制并继续。你能解决这些问题吗?
如果一个行为对于所有子类型都是通用的,那么使用继承是没问题的,例如,你可以创建一个抽象类,将通用行为放入其中,这样所有的子类都可以获得通用行为。但是,当行为可以在对象之间变化,并且使用继承来维护它们很困难时,策略的真正力量就显现出来了。
例如,假设你从不同的行为开始,你把它们放在一个抽象类中,如下所示。
public abstract class Vehicle
{
public abstract void AboutMe();
public abstract void FloatBehavior();
public abstract void FlyBehavior();
public virtual void DefaultJob()
{
Console.WriteLine("By default, I float.");
}
}
现在假设Boat
和Aeroplane
是从它继承的两个具体类。您知道一个Boat
对象不应该飞行,所以在Boat
类中,您可以简单地如下重写FlyBehavior
。
public override void FlyBehavior()
{
throw new NotImplementedException();
}
同样的,Aeroplane
物体也不应该浮在水中(正常情况下)。所以,在Aeroplane
类中,你可以如下重写FloatBehavior
。
public override void FloatBehavior()
{
throw new NotImplementedException();
}
现在考虑一下,像这样的对象有很多变化的行为。这种维护可能是开销。
除此之外,让我们考虑一种具有特殊功能的特殊车辆。如果你只是把这些特殊的特性放在抽象类中,所有其他的 vehicle 对象都会继承这些特性并需要实现它们。但这还没有结束。进一步,假设在Boat
类上有一个约束,简单地说它不能有任何这样的特殊行为。现在您遇到了一个死锁情况。如果实现这个特殊的方法,就违反了约束。如果您不实现它,系统架构就会崩溃,因为语言构造要求您实现该行为。(或者,您需要用abstract
关键字标记该类,但同时,请记住您不能从抽象类创建实例。)
为了克服这一点,我可以创建一个单独的继承层次结构,用一个接口来保存所有的专用特性,如果需要的话,我的类可以实现这个接口。但是,这可能部分解决了问题,因为接口可能包含多个方法,而您的类可能只需要实现其中的一个。最后,在任何一种情况下,整体维护都变得很困难。除此之外,特殊的行为可能会改变,在这种情况下,您需要跟踪实现这些行为的所有类。
在这种情况下,上下文类充当了救世主的角色。比如对于Boat
类对象,客户端不设置 fly 行为,或者对于Aeroplane
类对象,客户端不设置 float 行为;他仅仅知道特定车辆的预期行为。所以,如果你愿意,你可以防止客户错误地给车辆设置不正确的行为。
为了简化这一点,context 类为变化的行为保存一个引用变量,并将任务委托给适当的行为类。这就是为什么您会在我们的Vehicle
上下文类中看到下面的片段。
public class Vehicle
{
VehicleBehavior behavior;
//Some other code
/*
* It's your choice. You may prefer to use a setter
* method instead of using a constructor.
* You can call this method whenever we want
* to change the "vehicle behavior" on the fly.
*/
public void SetVehicleBehavior(VehicleBehavior behavior)
{
this.behavior = behavior;
}
//Some other code
}
对于这个例子,“has-a”关系比“is-a”关系更合适,这也是大多数设计模式鼓励复合而不是继承的主要原因之一。
15.2 使用策略设计模式的主要优势是什么?
以下是一些关键优势。
-
这种设计模式使你的类独立于算法。在这里,一个类在运行时动态地将算法委托给策略对象(封装了算法)。因此,算法的选择在编译时不受限制。
-
维护您的代码库更容易。
-
它很容易扩展。
这方面可以参考问答 15.1 的回答。
15.3 与策略设计模式相关的主要挑战是什么?
缺点可以总结如下。
-
添加上下文类会导致应用中存在更多的对象。
-
应用的用户必须了解不同的策略;否则,输出可能会让他们大吃一惊。
十六、模板方法模式
本章涵盖了模板方法模式。
GoF 定义
在操作中定义算法的框架,将一些步骤推迟到子类。模板方法允许子类在不改变算法结构的情况下重新定义算法的某些步骤。
概念
使用这种模式,您可以从算法的最小或基本结构开始。然后你将一些责任委托给子类。因此,派生类可以在不改变算法流程的情况下重新定义算法的某些步骤。
简单地说,这种设计模式在实现多步算法但允许通过子类定制时非常有用。
真实世界的例子
当你点比萨饼时,餐馆的厨师可以使用基本的机制来准备比萨饼,但他可能允许你选择最终的材料。例如,顾客可以选择不同的配料,如培根、洋葱、额外的奶酪、蘑菇等。因此,就在送披萨之前,厨师可以包括这些选择。
计算机世界的例子
假设你被雇佣来设计一个在线工程学位课程。你知道,一般来说,课程的第一学期对所有课程都是一样的。对于随后的学期,你需要根据学生选择的课程在申请中添加新的论文或科目。
当您希望避免应用中的重复代码,但允许子类更改基类工作流的某些特定细节,以便为应用带来不同的行为时,模板方法模式是有意义的。(但是,您可能不希望完全覆盖基方法来对子类进行彻底的更改。这样,模式不同于简单的多态。)
履行
假设每个工科学生需要在最初几个学期通过数学考试并展示软技能(如沟通技能、人员管理技能等等)才能获得学位。后来,你根据他们选择的道路(计算机科学或电子)在他们的课程中添加特殊的论文。
为此,在抽象类BasicEngineering,
中定义了一个模板方法DisplayCourseStructure()
,如下所示。
/// <summary>
/// Basic skeleton of actions/steps
/// </summary>
public abstract class BasicEngineering
{
//The following method(step) will NOT vary
private void Math()
{
Console.WriteLine("1.Mathematics");
}
//The following method(step) will NOT vary
private void SoftSkills()
{
Console.WriteLine("2.SoftSkills");
}
/*
The following method will vary.It will be
overridden by derived classes.
*/
public abstract void SpecialPaper();
//The "Template Method"
public void DisplayCourseStructure()
{
//Common Papers:
Math();
SoftSkills();
//Specialized Paper:
SpecialPaper();
}
}
注意,BasicEngineering
的子类不能改变DisplayCourseStructure()
方法的流程,但是它们可以覆盖SpecialPaper()
方法以包含特定于课程的细节,并使最终的课程列表彼此不同。
名为ComputerScience
和Electronics
的具体类是BasicEngineering,
的子类,它们借此机会覆盖了SpecialPaper()
方法。下面的代码段展示了来自ComputerScience
类的这样一个例子。
//The concrete derived class-ComputerScience
public class ComputerScience : BasicEngineering
{
public override void SpecialPaper()
{
Console.WriteLine("3.Object-Oriented Programming");
}
}
类图
图 16-1 显示了类图的重要部分。
图 16-1
类图
解决方案资源管理器视图
图 16-2 显示了程序的高层结构。
图 16-2
解决方案资源管理器视图
演示 1
下面是实现。
using System;
namespace TemplateMethodPattern
{
/// <summary>
/// Basic skeleton of actions/steps
/// </summary>
public abstract class BasicEngineering
{
//The following method(step) will NOT vary
private void Math()
{
Console.WriteLine("1.Mathematics");
}
//The following method(step) will NOT vary
private void SoftSkills()
{
Console.WriteLine("2.SoftSkills");
}
/*
The following method will vary.It will be
overridden by derived classes.
*/
public abstract void SpecialPaper();
//The "Template Method"
public void DisplayCourseStructure()
{
//Common Papers:
Math();
SoftSkills();
//Specialized Paper:
SpecialPaper();
}
}
//The concrete derived class-ComputerScience
public class ComputerScience : BasicEngineering
{
public override void SpecialPaper()
{
Console.WriteLine("3.Object-Oriented Programming");
}
}
//The concrete derived class-Electronics
public class Electronics : BasicEngineering
{
public override void SpecialPaper()
{
Console.WriteLine("3.Digital Logic and Circuit Theory");
}
}
//Client code
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Template Method Pattern Demonstration-1.***\n");
BasicEngineering bs = new ComputerScience();
Console.WriteLine("Computer Science course includes the following subjects:");
bs.DisplayCourseStructure();
Console.WriteLine();
bs = new Electronics();
Console.WriteLine("Electronics course includes the following subjects:");
bs.DisplayCourseStructure();
Console.ReadLine();
}
}
}
输出
这是输出。
***Template Method Pattern Demonstration-1.***
Computer Science course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Object-Oriented Programming
Electronics course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Digital Logic and Circuit Theory
问答环节
在这种模式中,子类可以根据他们的需要简单地重新定义方法。这是正确的吗?
是的。
16.2 抽象类 BasicEngineering
中,只有一个方法是抽象的,其他两个方法都是具体的方法。这背后的原因是什么?
这是一个只有三个方法的简单例子,您希望子类只覆盖这里的SpecialPaper()
方法。其他方法是两个课程共有的,它们不需要被子类覆盖。
16.3 假设你想在 BasicEngineering
类中添加更多的方法,但是当且仅当你的子类需要这些方法时,你才想使用它们;否则,你忽略它们。这种情况在一些博士项目中很常见,在这些项目中有些课程是必修的,但是如果一个学生有一定的资格,他可能不需要参加这些课程的讲座。可以用模板方法模式设计这种情况吗?
是的,你可以。基本上,你想使用一个钩子,这是一个可以帮助你控制算法流程的方法。
为了展示这种设计的一个例子,现在我在BasicEngineering
中增加了一个叫做IncludeAdditionalPaper()
的方法。我们假设,默认情况下,这门学科包含在课程列表中,但电子专业的学生可以选择退出这门课程。
修改后的BasicEngineering
类现在看起来像下面这样(注意指出重要变化的粗线)。
/// <summary>
/// Basic skeleton of actions/steps
/// </summary>
public abstract class BasicEngineering
{
//The following method(step) will NOT vary
private void Math()
{
Console.WriteLine("1.Mathematics");
}
//The following method(step) will NOT vary
private void SoftSkills()
{
Console.WriteLine("2.SoftSkills");
}
/*
The following method will vary.It will be
overridden by derived classes.
*/
public abstract void SpecialPaper();
//The "Template Method"
public void DisplayCourseStructure()
{
//Common Papers:
Math();
SoftSkills();
//Specialized Paper:
SpecialPaper();
//Include an additional subject if required.
if (IsAdditionalPaperNeeded())
{
IncludeAdditionalPaper();
}
}
private void IncludeAdditionalPaper()
{
Console.WriteLine("4.Compiler Design.");
}
//A hook method.
//By default,an additional subject is needed
public virtual bool IsAdditionalPaperNeeded()
{
return true;
}
}
由于电子类不需要包含附加方法,因此定义如下:
//The concrete derived class-Electronics
public class Electronics : BasicEngineering
{
public override void SpecialPaper()
{
Console.WriteLine("3.Digital Logic and Circuit Theory");
}
//Using the hook method now.
//Additional paper is not needed for Electronics.
public override bool IsAdditionalPaperNeeded()
{
return false;
}
}
现在让我们来看一下程序和输出。
演示 2
下面是修改后的实现。关键变化以粗体显示。
using System;
namespace TemplateMethodPattern
{
/// <summary>
/// Basic skeleton of actions/steps
/// </summary>
public abstract class BasicEngineering
{
//The following method(step) will NOT vary
private void Math()
{
Console.WriteLine("1.Mathematics");
}
//The following method(step) will NOT vary
private void SoftSkills()
{
Console.WriteLine("2.SoftSkills");
}
/*
The following method will vary.It will be
overridden by derived classes.
*/
public abstract void SpecialPaper();
//The "Template Method"
public void DisplayCourseStructure()
{
//Common Papers:
Math();
SoftSkills();
//Specialized Paper:
SpecialPaper();
//Include an additional subject if required.
if (IsAdditionalPaperNeeded())
{
IncludeAdditionalPaper();
}
}
private void IncludeAdditionalPaper()
{
Console.WriteLine("4.Compiler Design.");
}
//A hook method.
//By default,an additional subject is needed.
public virtual bool IsAdditionalPaperNeeded()
{
return true;
}
}
//The concrete derived class-ComputerScience
public class ComputerScience : BasicEngineering
{
public override void SpecialPaper()
{
Console.WriteLine("3.Object-Oriented Programming");
}
//Not tested the hook method.
//An additional subject is needed
}
//The concrete derived class-Electronics
public class Electronics : BasicEngineering
{
public override void SpecialPaper()
{
Console.WriteLine("3.Digital Logic and Circuit Theory");
}
//Using the hook method now.
//Additional paper is not needed for Electronics.
public override bool IsAdditionalPaperNeeded()
{
return false;
}
}
//Client code
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Template Method Pattern Demonstration-2.***\n");
BasicEngineering bs = new ComputerScience();
Console.WriteLine("Computer Science course includes the following subjects:");
bs.DisplayCourseStructure();
Console.WriteLine();
bs = new Electronics();
Console.WriteLine("Electronics course includes the following subjects:");
bs.DisplayCourseStructure();
Console.ReadLine();
}
}
}
输出
这是修改后的输出。
***Template Method Pattern Demonstration-2.***
Computer Science course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Object-Oriented Programming
4.Compiler Design.
Electronics course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Digital Logic and Circuit Theory
Note
你可能更喜欢另一种方法。例如,您可以在BasicEngineering
中直接包含名为IncludeAdditionalPaper()
的默认方法。之后,您可以覆盖Electronics
类中的方法,并使方法体为空。但是这种方法与前面的方法相比并没有看起来更好。
看起来这个模式类似于 Builder 模式。这是正确的吗?
不。不要忘记核心意图;模板方法模式是一种行为设计模式,而构建器是一种创造设计模式。在构建者模式中,客户/顾客是老板。他们可以控制算法的顺序。在模板方法模式中,您(或开发人员)是老板。您将代码放在一个中心位置(例如,本例中的抽象类BasicEngineering.cs
),并且您对执行流程拥有绝对的控制权,这是客户端无法更改的。例如,您可以看到数学和软技能总是出现在顶部,遵循模板方法DisplayCourseStructure()
中的执行顺序。客户端需要遵守这个流程。
如果您改变了模板方法中的流程,其他参与者也将遵循新的流程。
使用模板方法设计模式的主要优势是什么?
以下是一些关键优势。
-
你可以控制算法的流程。客户端无法更改它们。
-
常见操作集中在一个位置。例如,在一个抽象类中,子类可以只重定义变化的部分,这样就可以避免多余的代码。
16.6 与模板方法设计模式相关的主要挑战是什么?
缺点可以总结如下。
-
客户端代码不能指导步骤的顺序。如果您想要这种类型的功能,请使用构建器模式。
-
子类可以覆盖父类中定义的方法
(换句话说,在父类中隐藏原始定义),这可能违反 Liskov 替换原则,该原则基本上说,如果 S 是 T 的子类型,那么 T 类型的对象可以用 S 类型的对象替换。
-
子类越多,意味着代码越分散,维护越困难。
16.7 如果一个子类试图覆盖基础工程中的其他父方法,会发生什么?
这种模式建议不要这样做。当您使用这种模式时,您不应该完全覆盖所有的父方法来给子类带来根本性的改变。这样,它不同于简单的多态。
16.8 这种模式与策略模式 有何不同?
你找到了一个好的切入点。是的,策略和模板方法模式有相似之处。在策略中,您可以使用委托来改变整个算法;然而,模板方法模式建议您使用继承来改变算法中的某些步骤,但是算法的整体流程是不变的。
十七、命令模式
本章介绍了命令模式。
GoF 定义
将请求封装为一个对象,从而允许您用不同的请求、队列或日志请求参数化客户端,并支持可撤销的操作。
概念
使用这种模式,您可以封装一个方法调用过程。这里,一个对象可以通过某种明确的方法调用一个操作,而不用担心如何执行这个操作。这种模式是那些仅仅通过阅读描述通常很难理解的模式之一。当您看到实现时,这个概念会变得更加清晰。所以,请跟我一起继续阅读,直到看到演示 1。
一般来说,这里有四个术语很重要:调用方、客户端、命令方和接收方,具体如下。
-
命令对象由接收者执行的动作组成。
-
一个命令对象可以以特定于接收者的类的方式调用接收者的方法。然后接收器开始处理作业(或动作)。
-
命令对象被单独传递给调用者对象来调用命令。invoker 对象包含具体化的方法,通过这些方法,客户端可以执行工作,而不用担心目标接收者如何执行实际的工作。
-
客户端对象保存调用程序对象和命令对象。客户端只作出决定(即执行哪些命令),然后将命令传递给调用程序对象来执行。
真实世界的例子
当你在画一幅画的时候,你可能需要重画(撤销)它的一些部分来使它变得更好。
计算机世界的例子
通常,您可以在编辑器或集成开发环境(IDE)的菜单系统中观察到这种模式。例如,您可以使用命令模式来支持撤销、多次撤销或软件应用中的类似操作。
微软在 Windows 演示基础(WPF)中使用了这种模式。出现在 Visual Studio 杂志 ( https://visualstudiomagazine.com/articles/2012/04/10/command-pattern-in-net.aspx
)上的一篇 2012 年的文章详细描述了它。命令模式非常适合处理 GUI 交互。它工作得非常好,微软已经将其紧密集成到 Windows 演示基础(WPF)堆栈中。最重要的部分是来自 System.Windows.Input
名称空间的 ICommand
接口。任何实现了 ICommand
接口的类都可以通过通用的 WPF
控件来处理键盘或鼠标事件。这种链接既可以在 XAML
中完成,也可以在代码隐藏中完成。
另外,如果你熟悉 Java 或者 Swing,你看到Action
也是一个命令对象。
履行
在这个例子中,RemoteControl
是 Invoker 类。GameStartCommand
和GameStartCommand
是表示命令的具体类。这两个类实现了公共接口ICommand
,
,如下所示(相关注释说明了每个方法的用途)。
public interface ICommand
{
// To execute a command
void Execute();
// To undo last command execution
void Undo();
}
Game
是接收器类,其定义如下。
public class Game
{
string gameName;
public Game(string name)
{
this.gameName = name;
}
public void Start()
{
Console.WriteLine($"{gameName} is on.");
}
public void DisplayScore()
{
Console.WriteLine("The score is changing time to time.");
}
public void Finish()
{
Console.WriteLine($"---The game of {gameName} is over.---");
}
}
当客户端使用一个GameStopCommand
命令并对一个Invoker
对象调用ExecuteCommand
方法时,如下所示。
invoker.ExecuteCommand();
目标接收者(本例中的Game
类对象)只执行以下动作。
game.Finish();
但是当客户端使用一个GameStartCommand
命令并使用如下代码调用一个Invoker
对象上的ExecuteCommand
方法时。
invoker.ExecuteCommand();
目标接收者(本例中的Game
类对象)执行以下一组动作。
game.Start();
game.DisplayScore();
所以,你可以看到一个命令不需要只执行一个动作;相反,根据您的需要,您可以在目标接收者上执行一系列操作,并将它们封装在一个命令对象中。
Points to Note
本章中的例子展示了撤销操作的简单演示。撤消的实现取决于规范,在某些情况下可能很复杂。对于演示 1,我简单地假设撤销调用只是撤销上一个成功执行的命令。GameStartCommand
和GameStopCommand
类的Execute()
和Undo()
方法正在做相反的事情。也就是说,当客户端使用GameStopCommand
调用撤销操作时,游戏重启并显示分数(在本例中是一个简单的控制台消息)。但是如果客户端使用GameStartCommand
调用撤销操作,游戏会立即停止。这类似于打开一盏灯,关掉同样的灯;或者将一个数加到一个目标数上,作为相反的情况,再次从结果数中减去相同的数。
最后,看看下面的代码段,这是我如何创建一个命令对象的。
Game gameName = new Game("Golf");
// Command to start the game
GameStartCommand gameStartCommand = new GameStartCommand(gameName);
我将命令设置为调用程序,并使用它的ExecuteCommand()
方法来执行命令。后来,我又撤销了这个。我保留了控制台消息以帮助您理解。
Console.WriteLine("**Starting the game and performing undo immediately.**");
invoker.SetCommand(gameStartCommand);
invoker.ExecuteCommand();
// Performing undo operation
Console.WriteLine("\nUndoing the previous command now.");
invoker.UndoCommand();
类图
图 17-1 为类图。
图 17-1
类图
解决方案资源管理器视图
图 17-2 显示了程序的高层结构。
图 17-2
解决方案资源管理器视图
演示 1
这是完整的程序。
using System;
namespace CommandPattern
{
/// <summary>
/// Receiver Class
/// </summary>
public class Game
{
string gameName;
public Game(string name)
{
this.gameName = name;
}
public void Start()
{
Console.WriteLine($"{gameName} is on.");
}
public void DisplayScore()
{
Console.WriteLine("The score is changing time to time.");
}
public void Finish()
{
Console.WriteLine($"---The game of {gameName} is over.---");
}
}
/// <summary>
/// The command interface
/// </summary>
public interface ICommand
{
// To execute a command
void Execute();
// To undo last command execution
void Undo();
}
/// <summary>
/// GameStartCommand
/// </summary>
public class GameStartCommand : ICommand
{
private Game game;
public GameStartCommand(Game game)
{
this.game = game;
}
public void Execute()
{
game.Start();
game.DisplayScore();
}
public void Undo()
{
Console.WriteLine("Undoing start command.");
game.Finish();
}
}
/// <summary>
/// GameStopCommand
/// </summary>
public class GameStopCommand : ICommand
{
private Game game;
public GameStopCommand(Game game)
{
this.game = game;
}
public void Execute()
{
Console.WriteLine("Finishing the game.");
game.Finish();
}
public void Undo()
{
Console.WriteLine("Undoing stop command.");
game.Start();
game.DisplayScore();
}
}
/// <summary>
/// Invoker class
/// </summary>
public class RemoteControl
{
ICommand commandToBePerformed, lastCommandPerformed;
public void SetCommand(ICommand command)
{
this.commandToBePerformed = command;
}
public void ExecuteCommand()
{
commandToBePerformed.Execute();
lastCommandPerformed = commandToBePerformed;
}
public void UndoCommand()
{
// Undo the last command executed
lastCommandPerformed.Undo();
}
}
/// <summary>
/// Client code
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Command Pattern Demonstration***\n");
/* Client holds both the Invoker and Command Objects */
RemoteControl invoker = new RemoteControl();
Game gameName = new Game("Golf");
// Command to start the game
GameStartCommand gameStartCommand = new GameStartCommand(gameName);
// Command to stop the game
GameStopCommand gameStopCommand = new GameStopCommand(gameName);
Console.WriteLine("**Starting the game and performing undo immediately.**");
invoker.SetCommand(gameStartCommand);
invoker.ExecuteCommand();
// Performing undo operation
Console.WriteLine("\nUndoing the previous command now.");
invoker.UndoCommand();
Console.WriteLine("\n**Starting the game again.Then stopping it and undoing the stop operation.**");
invoker.SetCommand(gameStartCommand);
invoker.ExecuteCommand();
// Stop command to finish the game
invoker.SetCommand(gameStopCommand);
invoker.ExecuteCommand();
// Performing undo operation
Console.WriteLine("\nUndoing the previous command now.");
invoker.UndoCommand();
Console.ReadKey();
}
}
}
输出
这是输出。
***Command Pattern Demonstration***
**Starting the game and performing undo immediately.**
Golf is on.
The score is changing time to time.
Undoing the previous command now.
Undoing start command.
---The game of Golf is over.---
**Starting the game again.Then stopping it and undoing the stop operation.**
Golf is on.
The score is changing time to time.
Finishing the game.
---The game of Golf is over.---
Undoing the previous command now.
Undoing stop command.
Golf is on.
The score is changing time to time.
问答环节
17.1 GoF 定义从“封装请求”开始在演示 1 中,你是如何实现 封装 的?
命令对象包含一组针对特定接收器的操作。当您设置命令并在 invoker 对象上调用ExecuteCommand()
时,预期的动作在接收者端执行。从外面看,没有其他物体知道这是如何发生的;他们只知道如果他们调用ExecuteCommand()
,他们的请求就会被处理。
遵循 GoF 定义,你如何参数化其他有不同请求的对象?
注意,我首先在 invoker 中设置了GameStartCommand
,后来,我用GameStopCommand.Invoker
对象替换了它,在两种情况下都简单地调用了ExecuteCommand()
。
在这个例子中,你只和一个接收者打交道。你如何处理多个接收者?
在这个例子中,Game 是 receiver 类,但是没有人限制您创建一个新的类,并遵循演示 1 中所示的实现。另外,请注意,您使用下面的代码行创建了一个Game
类对象。
Game gameName = new Game("Golf");
由于Game
类构造函数接受一个字符串参数,您也可以传递一个不同的值并创建一个不同的对象。以下代码段是一个示例。
Console.WriteLine("\nPlaying another game now.(Optional for you)");
gameName = new Game("Soccer");
// Command to start the game
gameStartCommand = new GameStartCommand(gameName);
// Command to stop the game
gameStopCommand = new GameStopCommand(gameName);
// Starting the game
invoker.SetCommand(gameStartCommand);
invoker.ExecuteCommand();
// Stopping the game
invoker.SetCommand(gameStopCommand);
invoker.ExecuteCommand();
The previous code segment can generate the following output as expected:
Playing another game now.(Optional for you)
Soccer is on.
The score is changing time to time.
Finishing the game.
---The game of Soccer is over.---
17.4 我可以忽略 invoker 对象吗?
很多时候,程序员试图在面向对象编程(OOP)中封装数据和相应的方法。但是你发现在命令模式中,你在尝试封装命令对象。换句话说,您正在从不同的角度实现封装。
我之前告诉过你,当调用 invoker 对象的ExecuteCommand()
时,预期的动作在接收者端执行。从外面看,没有其他物体知道它是如何发生的;他们只知道如果他们调用ExecuteCommand(),
,他们的请求就会被处理。因此,简单地说,一个调用程序包含一些明确的方法,通过这些方法,客户端可以执行一项工作,而不用担心实际的工作在接收端是如何执行的。
当您需要处理一组复杂的命令时,这种方法很有意义。
让我们再看一遍条款。您创建命令对象,并将其传递给一些接收者来访问它们,然后通过调用命令对象的方法的调用者来执行这些命令(例如,本例中的ExecuteCommand
)。对于一个简单的用例,这个 invoker 类不是强制性的。例如,考虑这样一种情况,其中一个命令对象只有一个方法要执行,并且您正试图免除调用程序来调用该方法。但是,当您想要跟踪日志文件(或队列)中的一系列命令时,调用程序可能会发挥重要作用。
你为什么要跟踪这些日志?
您可能希望创建撤消或重做操作。
17.6 指挥模式的主要优势是什么?
以下是一些优点。
-
创建和最终执行的请求是分离的。客户端可能不知道调用者如何执行操作。
-
您可以创建宏命令(这些是多个命令的序列,可以一起调用。例如,对于宏命令,您可以创建一个类,该类具有一个接受命令列表的构造函数。在它的
Execute()
方法中,您可以使用for
循环/foreach
循环依次调用这些命令中的Execute()
。 -
可以在不影响现有系统的情况下添加新命令。
-
最重要的是,您可以支持急需的撤销(和重做)操作。
-
应该注意的是,一旦您简单地创建了一个命令对象,并不意味着计算会立即开始。您可以将它安排在以后,或者将它们放在作业队列中,以后再执行。此外,通过使用线程池,您可以在多线程环境中异步执行它们。(异步编程在本书第二十七章讨论。)
17.7 指挥模式面临哪些挑战?
以下是一些缺点。
-
为了支持更多的命令,您需要创建更多的类。因此,随着时间的推移,维护可能会很困难。
-
当出现错误情况时,如何处理错误或决定如何处理返回值变得很棘手。客户可能想知道这些。但是这里您将命令与客户端代码解耦,所以这些情况很难处理。在调用者可以在不同的线程中运行的多线程环境中,这一挑战变得非常重要。
17.8 在演示 1 中,您只撤销了最后一个命令?有什么方法可以实现“撤销全部”吗?此外,您如何记录请求?
问得好。您可以简单地维护一个可以存储命令的堆栈,然后您可以简单地从堆栈中弹出项目并调用它的undo()
方法。在第十九章(在 Memento 模式上,类似于这个模式),我进一步讨论了撤销和各种实现。现在,让我向您展示一个简单的例子,在这个例子中,您可以撤销所有以前的命令。演示 2 就是为此而做的。它是对演示 1 的简单修改,因此省略了类图和解决方案资源管理器视图;可以直接跳转到实现中。
你问了另一个关于如何记录请求的问题。在演示 2 中,当我维护列表来存储执行的命令时,我使用这个列表来支持使用单个方法调用“撤销所有命令”。同一个列表可以作为您可以在控制台中打印的命令历史。或者,您可以创建一个单独的文件来保存每次命令执行时的详细信息。如有必要,稍后您可以检索该文件进行详细查看。
修改的实现
这个例子向你展示了一种调用多个撤销操作的方法。对 invoker 类做了一些小的修改。我维护一个列表来存储所有执行的命令。每当一个命令被执行时,它都会被添加到列表中,稍后当我调用UndoAll()
时,我可以简单地迭代这个列表并调用相应的撤销操作。调用者以粗体显示主要变化,如下所示。
/// <summary>
/// Invoker class
/// </summary>
public class RemoteControl
{
ICommand commandToBePerformed, lastCommandPerformed;
List<ICommand> savedCommands = new List<ICommand>();
public void SetCommand(ICommand command)
{
this.commandToBePerformed = command;
}
public void ExecuteCommand()
{
commandToBePerformed.Execute();
lastCommandPerformed = commandToBePerformed;
savedCommands.Add(commandToBePerformed);
}
public void UndoCommand()
{
// Undo the last command executed
lastCommandPerformed.Undo();
}
public void UndoAll()
{
for (int i = savedCommands.Count; i > 0; i--)
{
// Get a restore point and call Undo()
savedCommands[i - 1].Undo();
}
}
}
Game
类现在没有Start()
方法;相反,它有两个新方法叫做UpLevel()
和DownLevel()
,如下所示。
public void UpLevel()
{
++level;
Console.WriteLine("Level upgraded.");
}
public void DownLevel()
{
--level;
Console.WriteLine("Level downgraded.");
}
UpLevel()
方法升级游戏等级。DownLevel()
方法做相反的事情,所以它被用在GameStartCommand
类的Undo
操作中。为了达到我的主要目的(向您展示“撤销全部”),我不需要这个例子中的GameStopCommand
类,所以为了使这个例子简短,我也省略了那个类。最后,我做了一个简单的假设,当游戏等级设置为 0 时(即处于出生状态),如果你执行Undo()
,游戏就会停止。剩下的代码很容易理解,现在可以开始演示 2 了。
演示 2
这是完整的程序。
using System;
using System.Collections.Generic;
namespace CommandPatternDemonstration2
{
// Receiver Class
public class Game
{
string gameName;
public int level;
public Game(string name)
{
this.gameName = name;
level = -1;
Console.WriteLine($"Game started.");
}
public void DisplayLevel()
{
Console.WriteLine($"Current level is set to {level}.");
}
public void UpLevel()
{
++level;
Console.WriteLine("Level upgraded.");
}
public void DownLevel()
{
--level;
Console.WriteLine("Level downgraded.");
}
public void Finish()
{
Console.WriteLine($"---The game of {gameName} is over.---");
}
}
public interface ICommand
{
void Execute();
void Undo();
}
/// <summary>
/// GameStartCommand
/// </summary>
public class GameStartCommand : ICommand
{
private Game game;
public GameStartCommand(Game game)
{
this.game = game;
}
public void Execute()
{
game.UpLevel();
game.DisplayLevel();
}
public void Undo()
{
if (game.level > 0)
{
game.DownLevel();
game.DisplayLevel();
}
else
{
game.Finish();
}
}
}
/// <summary>
/// Invoker class
/// </summary>
public class RemoteControl
{
ICommand commandToBePerformed, lastCommandPerformed;
List<ICommand> savedCommands = new List<ICommand>();
public void SetCommand(ICommand command)
{
this.commandToBePerformed = command;
}
public void ExecuteCommand()
{
commandToBePerformed.Execute();
lastCommandPerformed = commandToBePerformed;
savedCommands.Add(commandToBePerformed);
}
public void UndoCommand()
{
// Undo the last command executed
lastCommandPerformed.Undo();
}
public void UndoAll()
{
for (int i = savedCommands.Count; i > 0; i--)
{
// Get a restore point and call Undo()
savedCommands[i - 1].Undo();
}
}
}
/// <summary>
/// Client code
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Command Pattern Demonstration2***\n");
// Client holds both the Invoker and Command Objects
RemoteControl invoker = new RemoteControl();
Game gameName = new Game("Golf");
// Command to start the game
GameStartCommand gameStartCommand = new GameStartCommand(gameName);
Console.WriteLine("**Starting the game and upgrading the level 3 times.**");
invoker.SetCommand(gameStartCommand);
invoker.ExecuteCommand();
invoker.ExecuteCommand();
invoker.ExecuteCommand();
// Performing undo operation(s) one at a time
//invoker.UndoCommand();
//invoker.UndoCommand();
//invoker.UndoCommand();
Console.WriteLine("\nUndoing all the previous commands at one shot.");
invoker.UndoAll();
Console.ReadKey();
}
}
}
输出
这是新的输出。
***Command Pattern Demonstration2***
Game started.
**Starting the game and upgrading level 3 times.**
Level upgraded.
Current level is set to 0.
Level upgraded.
Current level is set to 1.
Level upgraded.
Current level is set to 2.
Undoing all the previous commands at one shot.
Level downgraded.
Current level is set to 1.
Level downgraded.
Current level is set to 0.
---The game of Golf is over.---
十八、迭代器模式
本章涵盖了迭代器模式。
GoF 定义
提供一种方法来顺序访问聚合对象的元素,而不暴露其底层表示。
概念
迭代器通常用于遍历容器(或对象集合)以访问其元素,而不知道数据在内部是如何存储的。当您需要以标准和统一的方式遍历不同种类的集合对象时,它非常有用。图 18-1 显示了一个迭代器模式的示例和最常见的图表。
图 18-1
迭代器模式的示例图
参与者描述如下。
-
迭代器是访问或遍历元素的接口。
-
具体迭代器实现了
Iterator
接口方法。它还可以跟踪聚合遍历中的当前位置。 -
聚合定义了一个可以创建
Iterator
对象的接口。 -
混凝土骨料实现了
Aggregate
接口。它返回一个ConcreteIterator
的实例。
Points to Note
-
它经常用于遍历树状结构的节点。在许多例子中,您可能会注意到迭代器模式和组合模式。
-
迭代器的作用不仅限于遍历。这个角色可以改变以支持各种需求。例如,您可以用各种方式过滤元素。
-
客户端看不到实际的遍历机制。客户端程序只使用公共迭代器方法。
-
迭代器和枚举器的概念已经存在很久了。枚举器根据一个标准产生下一个元素,而使用迭代器,你从起点到终点循环一个序列。
-
将 foreach 迭代器应用于由枚举器生成的集合是一种常见的做法。然后,您可以获取该值并将其应用到循环体中。
真实世界的例子
假设有两家公司:A 公司和 b 公司。A 公司存储其员工记录(即每个员工的姓名、地址、工资明细等。)在链表数据结构中。B 公司将其员工数据存储在一个数组中。一天,两家公司决定合并成一家大公司。迭代器模式在这种情况下非常方便,因为您不需要从头开始编写代码。在这种情况下,您可以使用一个公共接口来访问两家公司的数据。因此,您可以简单地调用这些方法,而无需重写代码。
考虑另一个例子。假设你的公司决定根据员工的表现提升他们。所以,所有的经理聚在一起,为晋升制定一个共同的标准。然后,他们一个接一个地遍历员工的记录,以标记潜在的晋升候选人。
你也可以考虑不同领域的例子。例如,当您将歌曲存储在您喜欢的音频设备(例如,MP3 播放器)或移动设备中时,您可以通过各种按钮按压或滑动动作来迭代它们。基本思想是为您提供一种机制,以便您可以平滑地迭代您的列表。
计算机世界的例子
浏览以下两个要点。这些是迭代器模式的常见例子。
-
C# 拥有在 Visual Studio 2005 中引入的迭代器。在这个上下文中经常使用
foreach
语句。要了解关于这些内置功能的更多信息,请参考https://docs.microsoft.com/en-us/dotnet/csharp/iterators
。 -
如果你熟悉 Java,可能用过 Java 内置的
Iterator
接口,java.util.Iterator
。这种模式用于像java.util.Iterator
或java.util.Enumeration
这样的接口。
履行
类似于我们现实世界的例子,让我们假设有一个学院有两个部门:科学和艺术。艺术系使用数组数据结构来维护其课程细节,但科学系使用链表数据结构来保持不变。行政部门不干涉一个部门如何维护这些细节。它只是对从每个部门获取数据感兴趣,并希望统一访问这些数据。现在假设您是行政部门的成员,在一个新的会话开始时,您想使用迭代器来宣传课程表。让我们看看如何在接下来的演示中实现它。
让我们假设您有一个名为IIterator
,
的迭代器,它在接下来的例子中充当公共接口,它目前支持四个基本方法:First(), Next(), CurrentItem()
和IsCollectionEnds()
,如下所示。
-
在开始遍历数据结构之前,
First()
方法将指针重置为指向第一个元素。 -
Next()
方法返回容器中的下一个元素。 -
CurrentItem()
方法返回迭代器在特定时间指向的容器的当前元素。 -
IsCollectionEnds()
验证下一个元素是否可用于进一步处理。所以,这个方法帮助你决定你是否已经到达了你的容器的末端。
这些方法在每个ScienceIterator
和ArtsIterator
类中实现。您将看到CurrentItem()
方法在ScienceIterator
和ArtIterator
类中有不同的定义。同样,为了打印课程表,我只使用了其中的两种方法:IsCollectionEnds()
和Next()
。如果您愿意,可以尝试剩下的两种方法,First()
和currentItem()
。我提到了这四种方法,并为它们提供了一些示例实现,因为它们在迭代器模式实现中非常常见。这些示例实现也可以帮助您理解这些示例。
Point to Note
如果你只考虑理科或文科,程序的代码长度可以减半。但是我保留了它们,向您展示迭代器模式可以帮助您在不知道数据在内部是如何存储的情况下进行遍历。对于科学,主题存储在一个链表中,但是对于艺术,主题存储在一个数组中。不过,通过使用这种模式,您可以以统一的方式遍历和打印主题。
类图
图 18-2 显示了类图。
图 18-2
类图
解决方案资源管理器视图
图 18-3 显示了程序的高层结构。这是一个很大的程序,很难在一个屏幕截图中容纳所有的内容,所以我只扩展了科学部门的细节。
图 18-3
解决方案资源管理器视图
演示 1
下面是实现。
using System;
using System.Collections.Generic;
using System.Linq;
namespace IteratorPattern
{
#region Iterator
public interface IIterator
{
// Reset to first element
void First();
// Get next element
string Next();
// End of collection check
bool IsCollectionEnds();
// Retrieve Current Item
string CurrentItem();
}
/// <summary>
/// ScienceIterator
/// </summary>
public class ScienceIterator : IIterator
{
private LinkedList<string> Subjects;
private int position;
public ScienceIterator(LinkedList<string> subjects)
{
this.Subjects = subjects;
position = 0;
}
public void First()
{
position = 0;
}
public string Next()
{
return Subjects.ElementAt(position++);
}
public bool IsCollectionEnds()
{
if (position < Subjects.Count)
{
return false;
}
else
{
return true;
}
}
public string CurrentItem()
{
return Subjects.ElementAt(position);
}
}
/// <summary>
/// ArtsIterator
/// </summary>
public class ArtsIterator : IIterator
{
private string[] Subjects;
private int position;
public ArtsIterator(string[] subjects)
{
this.Subjects = subjects;
position = 0;
}
public void First()
{
position = 0;
}
public string Next()
{
//Console.WriteLine("Currently pointing to the subject: "+ this.CurrentItem());
return Subjects[position++];
}
public bool IsCollectionEnds()
{
if (position >= Subjects.Length)
{
return true;
}
else
{
return false;
}
}
public string CurrentItem()
{
return Subjects[position];
}
}
#endregion
#region Aggregate
public interface ISubjects
{
IIterator CreateIterator();
}
public class Science : ISubjects
{
private LinkedList<string> Subjects;
public Science()
{
Subjects = new LinkedList<string>();
Subjects.AddFirst("Mathematics");
Subjects.AddFirst("Computer Science");
Subjects.AddFirst("Physics");
Subjects.AddFirst("Electronics");
}
public IIterator CreateIterator()
{
return new ScienceIterator(Subjects);
}
}
public class Arts : ISubjects
{
private string[] Subjects;
public Arts()
{
Subjects = new[] { "English", "History", "Geography", "Psychology" };
}
public IIterator CreateIterator()
{
return new ArtsIterator(Subjects);
}
}
#endregion
/// <summary>
/// Client code
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Iterator Pattern Demonstration.***");
// For Science
ISubjects subjects= new Science();
IIterator iterator = subjects.CreateIterator();
Console.WriteLine("\nScience subjects :");
Print(iterator);
// For Arts
subjects = new Arts();
iterator = subjects.CreateIterator();
Console.WriteLine("\nArts subjects :");
Print(iterator);
Console.ReadLine();
}
public static void Print(IIterator iterator)
{
while (!iterator.IsCollectionEnds())
{
Console.WriteLine(iterator.Next());
}
}
}
}
输出
这是输出。
***Iterator Pattern Demonstration.***
Science subjects :
Electronics
Physics
Computer Science
Mathematics
Arts subjects :
English
History
Geography
Psychology
Note
您可以在一个实现中使用两种或多种不同的数据结构来展示这种模式的强大功能。您已经看到,在前面的演示中,我将First (), Next(), IsCollectionEnds(), and CurrentItem()
方法用于不同的实现,这些实现因其内部数据结构而异。
注释代码中还显示了CurrentItem()
的一种用法。如果您想测试它,您可以取消注释该行。
演示 2
现在让我们看看另一个实现,它使用了C#
对迭代器模式的内置支持。我使用了IEnumerable
接口,所以不需要定义自定义迭代器。但是要使用这个接口,你需要在程序的开头包含下面一行。
using System.Collections;
如果您看到 Visual Studio 中的定义,它描述了以下内容。
//
// Summary:
// Exposes an enumerator, which supports a simple iteration over a // non-generic collection.
[NullableContextAttribute(1)]
public interface IEnumerable
{
//
// Summary:
// Returns an enumerator that iterates through a collection.
//
// Returns:
// An System.Collections.IEnumerator object that can be used to iterate
// through the collection.
IEnumerator GetEnumerator();
}
因此,您可以很容易地预测每个具体的迭代器需要实现GetEnumerator()
方法。在下面的实现(演示 2)中,两个具体的迭代器都将其定义如下。
public IEnumerator GetEnumerator()
{
foreach( string subject in Subjects)
{
yield return subject;
}
}
你可能会对yield return
感到好奇。微软在 https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield
讨论。
在语句中使用 yield 上下文关键字时,表示出现该关键字的方法、操作符或 get 访问器是迭代器。当您为自定义集合类型实现IEnumerable和IEnumerator模式时,使用 yield 定义迭代器可以消除对显式额外类(保存枚举状态的类,例如参见IEnumerator
)的需求。
使用 yield return 语句一次返回一个元素。迭代器方法返回的序列可以通过使用foreach语句或 LINQ 查询来消耗。foreach 循环的每次迭代都调用 iterator 方法。当在迭代器方法中到达 yield return 语句时,返回 expression,并保留代码中的当前位置。下次调用迭代器函数时,从该位置重新开始执行。??
这些评论不言自明。简而言之,GetEnumerator
的foreach
可以记住上一个yield return
之后的位置,并可以给你下一个值。在接下来的演示中,剩余的代码很容易理解。由于整体概念和意图与演示 1 相似,现在可以直接跳到演示 2。下面是完整的实现。
using System;
using System.Collections;
using System.Collections.Generic;
namespace SimpleIterator
{
public class Arts : IEnumerable
{
private string[] Subjects;
public Arts()
{
Subjects = new[] { "English", "History", "Geography", "Psychology" };
}
public IEnumerator GetEnumerator()
{
foreach (string subject in Subjects)
{
yield return subject;
}
}
}
public class Science : IEnumerable
{
private LinkedList<string> Subjects;
public Science()
{
Subjects = new LinkedList<string>();
Subjects.AddFirst("Mathematics");
Subjects.AddFirst("Computer Science");
Subjects.AddFirst("Physics");
Subjects.AddFirst("Electronics");
}
public IEnumerator GetEnumerator()
{
foreach (string subject in Subjects)
{
yield return subject;
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Iterator Pattern.A simple demonstration using built-in constructs.***");
Arts artsPapers = new Arts();
Console.WriteLine("\nArts subjects are as follows:");
/*
Consume values from the
collection's GetEnumerator()
*/
foreach (string subject in artsPapers)
{
Console.WriteLine(subject);
}
Science sciencePapers = new Science();
Console.WriteLine("\nScience subjects are as follows:");
/*
Consume values from the
collection's GetEnumerator()
*/
foreach (string subject in sciencePapers)
{
Console.WriteLine(subject);
}
}
}
}
输出
这是输出。
***Iterator Pattern.A simple demonstration using built-in constructs.***
Arts subjects are as follows:
English
History
Geography
Psychology
Science subjects are as follows:
Electronics
Physics
Computer Science
Mathematics
问答环节
18.1 迭代器模式是用来做什么的?
下面讨论它的一些用法。
-
你可以遍历一个对象结构而不知道它的内部细节。因此,如果您有一个不同子集合的集合(例如,您的容器混合了数组、列表、链表等等),您仍然可以遍历整个集合,并以一种通用的方式处理元素,而不需要知道它们之间的内部细节或差异。
-
您可以用不同的方式遍历集合。如果设计得当,多个遍历也可以并行进行。
18.2 与此模式相关的关键 挑战 有哪些?
您必须确保在遍历过程中没有发生意外的修改。
但是要应对前面提到的挑战,你可以简单地做个备份,然后继续。我说得对吗?
进行备份并在以后重新检查是一项成本高昂的操作。
18.4 在代码中,我看到一个区域名为 Aggregate
。这个名字背后有什么原因吗?
一个集合定义了一个接口来创建一个Iterator
对象。我采用了 GoF 书里的名字。
在整个讨论中,你都谈到了收藏。什么是收藏?
在 C# 中,当您管理(或创建)一组相关的对象时,您有以下选择。
-
可以考虑数组。
-
可以考虑收藏。
在许多情况下,集合是首选,因为它们可以动态增长或收缩。在某些集合中,您甚至可以为对象分配键,以便在以后的阶段使用这些键更有效地检索它们。(例如,字典就是这样一个集合,通常用于快速查找。)最后,集合是一个类,所以在向它添加元素之前,需要创建实例。这里有一个例子。
LinkedList<string> Subjects = new LinkedList<string>();
Subjects.AddLast("Maths");
Subjects.AddLast("Comp. Sc.");
Subjects.AddLast("Physics");
在这个例子中,我没有使用AddFirst()
方法,而是使用了AddLast()
方法作为变体。这两种方法都可用,并且内置在 C# 中。AddLast()
方法在LinkedList<T>,
的末尾添加节点,而AddFirst()
方法在LinkedList<T>.
的开头添加节点
在这个实现中,你可以简单地考虑使用科学或艺术科目来演示迭代器模式的实现,并减少代码大小。这是正确的吗?
是的,我之前提到过。但是当您使用两种不同的数据结构时,您可能会看到迭代器设计模式的真正威力。所以,我把它们都留在了这里。
十九、备忘录模式
这一章涵盖了备忘录模式。
GoF 定义
在不违反封装的情况下,捕获并具体化一个对象的内部状态,以便该对象可以在以后恢复到这个状态。
概念
单词 memento 是对过去事件的提醒。通过遵循面向对象的方法,您还可以跟踪(或保存)对象的状态。因此,每当您想要将对象恢复到它以前的状态时,您可以考虑使用这种模式。
在这种模式中,您通常会看到三个参与者:备忘录、发起人和看管人(通常用作客户)。工作流程可以概括如下:发起者对象有一个内部状态,客户端可以在其中设置一个状态。为了保存发起者的当前内部状态,客户(或看护者)向其请求备忘录。客户端还可以将备忘录(它持有的)传递回发起者以恢复先前的状态。通过遵循正确的方法,这些保存和恢复操作不会违反封装。
真实世界的例子
您可以在有限状态机的状态中看到 Memento 模式的经典示例。这是一个数学模型,但它最简单的应用之一是十字转门。一个十字转门有一些旋转臂,最初是锁定的。当你穿过它的时候(比如放一些硬币进去),锁是开着的,手臂可以转动。一旦你通过,手臂回到锁定状态。
计算机世界的例子
在绘图应用中,您可能需要恢复到较旧的状态。此外,在数据库事务中,您可能需要回滚一些特定的事务。备忘录模式可以用在这些场景中。
履行
以下是 GoF 的一些重要建议。
-
备忘录保存了发起者的内部状态。
-
只有发起者应该创建备忘录。稍后,它可以使用备忘录来恢复先前的内部状态。
-
看守类是备忘录的容器。这个类用于保存备忘录,但是它从不操作或检查备忘录的内容。管理员可以从发起者那里得到备忘录。
Note
在这种模式中,发起者看到的是宽接口,而管理者看到的是窄接口。管理员不允许对备忘录做任何改动。因此,memento 对象应该用作不透明对象。
memento 设计模式可以使用不同的技术实现不同的实现。在本章中,您将看到两个演示。演示 1 相对简单易懂。但是在演示 2 中有所改进。在这两个实现中,我没有使用单独的看守类;相反,我使用客户机代码来扮演看管者的角色。
在演示 1 中,看护者拿着一个Originator
物体,并向其索要备忘录。它将备忘录保存在一个列表中。因此,您会在客户端中看到下面几行代码。
Originator originatorObject = new Originator();
Memento currentMemento;
IList<Memento> savedStates = new List<Memento>();
/*
Adding a memento the list. This memento stores
the current state of the Originator.
*/
savedStates.Add(originatorObject.CurrentMemento());
memento 类非常简单,它有一个简单的 getter-setter 来获取或设置发起者的state
。类如下。
class Memento
{
private string state;
public string State
{
get
{
return state;
}
set
{
state = value;
}
}
}
Note
从 C# 3.0 开始,您可以通过使用自动属性(如公共字符串状态{ get 设置;}.
除了状态之外,Originator
类还有一个构造函数和两个名为CurrentMemento()
和RestoreMemento(...)
的方法。第一种是响应看管人的请求提供备忘录,定义如下。
public Memento CurrentMemento()
{
myMemento = new Memento();
myMemento.State = state;
return myMemento;
}
第二种方法将发起者恢复到以前的状态。这种状态包含在来自管理员的备忘录(作为方法参数出现)中。管理员可以发送它之前保存的备忘录。该方法定义如下。
public void RestoreMemento(Memento restoreMemento)
{
this.state = restoreMemento.State;
Console.WriteLine($"Restored to state : {state}");
}
剩下的代码很简单,但是请参考注释以获得更好的理解。
类图
图 19-1 为类图。
图 19-1
类图
解决方案资源管理器视图
图 19-2 显示了程序的高层结构。
图 19-2
解决方案资源管理器视图
演示 1
下面是实现。
using System;
using System.Collections.Generic;
namespace MementoPattern
{
/// <summary>
/// Memento class
/// As per GoF:
/// 1.A Memento object stores the snapshot of Originator's /// internal state.
/// 2.Ideally,only the originator that created a memento is /// allowed to access it.
/// </summary>
class Memento
{
private string state;
public string State
{
get
{
return state;
}
set
{
state = value;
}
}
/*
C#3.0 onwards, you can use
automatic properties as follows:
public string State { get; set; }
*/
}
/// <summary>
/// Originator class
/// As per GoF:
/// 1.It creates a memento that contains a snapshot of
/// its current internal state.
/// 2.It uses a memento to restore its internal state.
/// </summary>
class Originator
{
private string state;
Memento myMemento;
public Originator()
{
//Creating a memento with born state.
state = "Snapshot #0.(Born state)";
Console.WriteLine($"Originator's current state is: {state}");
}
public string State
{
get { return state; }
set
{
state = value;
Console.WriteLine($"Originator's current state is: {state}");
}
}
/*
Originator will supply the memento
(which contains it's current state)
in respond to caretaker's request.
*/
public Memento CurrentMemento()
{
myMemento = new Memento();
myMemento.State = state;
return myMemento;
}
// Back to an old state (Restore)
public void RestoreMemento(Memento restoreMemento)
{
this.state = restoreMemento.State;
Console.WriteLine($"Restored to state : {state}");
}
}
/// <summary>
/// The 'Caretaker' class.
/// As per GoF:
/// 1.This class is responsible for memento's safe-keeping.
/// 2.Never operates or Examines the content of a Memento.
/// Additional notes( for your reference):
/// The originator object has an internal state, and a client can set a /// state in it.A client(or, caretaker) requests a memento from the /// originator to save the current internal state of the originator). /// It can also pass a memento back to the originator to restore it /// to a previous state that the memento holds in it.This enables to save /// and restore the internal state of an originator without violating its /// encapsulation.
/// </summary>
class Client
{
static Originator originatorObject;
static Memento currentMemento;
static void Main(string[] args)
{
Console.WriteLine("***Memento Pattern Demonstration-1.***\n");
//Originator is initialized.The constructor will create a born state.
originatorObject = new Originator();
//Memento currentMemento;
IList<Memento> savedStates = new List<Memento>();
/*
Adding a memento the list.This memento stores
the current state of the Origintor.
*/
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #1.
originatorObject.State = "Snapshot #1";
//Adding this memento as a restore point
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #2.
originatorObject.State = "Snapshot #2";
//Adding this memento as a restore point
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #3.
originatorObject.State = "Snapshot #3";
//Adding this memento as a restore point
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #4\. It is not added as a restore point.
originatorObject.State = "Snapshot #4";
//Available restore points
Console.WriteLine("\nCurrently available restore points are :");
foreach (Memento m in savedStates)
{
Console.WriteLine(m.State);
}
//Undo's
//Roll back starts...
Console.WriteLine("\nPerforming undo's now.");
for (int i = savedStates.Count; i > 0; i--)
{
//Get a restore point
currentMemento = savedStates[i - 1];
originatorObject.RestoreMemento(currentMemento);
}
//Redo's
Console.WriteLine("\nPerforming redo's now.");
for (int i = 1; i < savedStates.Count; i++)
{
currentMemento = savedStates[i];
originatorObject.RestoreMemento(currentMemento);
}
// Wait for user
Console.ReadKey();
}
}
}
输出
这是输出。
***Memento Pattern Demonstration-1.***
Originator's current state is: Snapshot #0.(Born state)
Originator's current state is: Snapshot #1
Originator's current state is: Snapshot #2
Originator's current state is: Snapshot #3
Originator's current state is: Snapshot #4
Currently available restore points are :
Snapshot #0.(Born state)
Snapshot #1
Snapshot #2
Snapshot #3
Performing undo's now.
Restored to state : Snapshot #3
Restored to state : Snapshot #2
Restored to state : Snapshot #1
Restored to state : Snapshot #0.(Born state)
Performing redo's now.
Restored to state : Snapshot #1
Restored to state : Snapshot #2
Restored to state : Snapshot #3
分析
使用这个程序的概念,您可以使用三种不同的撤销操作,如下所示。
-
您可以回到上一个还原点。
-
您可以返回到指定的还原点(直接使用 index 属性)。例如,要直接返回到快照#2,可以使用下面几行代码:
//Directly going back to Snapshot #2 currentMemento = savedStates[2]; originatorObject.RestoreMemento(currentMemento);
-
您可以恢复所有还原点(使用一个
for
循环和一个索引属性显示)
Note
如果应用使用 Memento 模式,并且有一个可变引用类型的状态,您可能会看到深度复制技术的实现将状态存储在 Memento 对象中。你在第二章学到了深度复制。
问答环节
在前面的例子中,你能使用一个非泛型版本吗,比如 ArrayList?
我喜欢听从专家的建议,他们通常更喜欢通用版本而不是非通用版本。这就是为什么我喜欢数据结构,比如List
、Dictionary
等等,而不是它们的对应物,比如ArrayList
和HashTable
。我在我早期的两本书里详细讨论了泛型:交互式 C# (Apress,2017)和高级 C# 入门(Apress,2020)。
使用 Memento 设计模式的主要优势是什么?
以下是一些优点。
-
最大的优点是您可以随时丢弃不需要的更改,并将它们恢复到预期的或稳定的状态。
-
您不会损害与参与此模型的关键对象相关联的封装。
-
你可以保持很高的凝聚力。
-
它提供了一种简单的恢复技术。
19.3 备忘录设计模式的主要挑战是什么?
以下是一些缺点。
-
拥有更多备忘录需要更多的存储空间。此外,它们给看护者增加了额外的负担。
-
前一点增加了维护成本。
-
您不能忽略保存这些状态所花费的时间,这会降低应用的整体性能。
请注意,在 C# 或 Java 等语言中,开发人员可能更喜欢使用序列化/反序列化技术,而不是直接实现 Memento 设计模式。这些技术各有利弊,但是您可以在应用中结合使用这两种技术。
我很困惑。为了支持 撤销操作 ,我应该使用哪种模式——Memento 还是 Command?
GoF 说这些是相关的模式。这主要取决于你想如何处理这种情况。假设你正在给一个整数加 25。在此添加操作之后,您可以通过执行反向操作来撤消它。简单来说,50 + 25 = 75,所以 75–25 = 50。在这种类型的操作中,您不需要存储以前的状态。
但是考虑一种情况,您需要在操作之前存储对象的状态。在这种情况下,您使用 Memento。例如,在绘画应用中,通过在执行命令之前存储对象列表,可以避免撤销某些绘画操作的成本。这个存储的列表可以作为备忘录,您可以将这个列表与相关的命令一起保存。类似的概念也适用于一个长期运行的游戏应用,它有多个级别,您可以在其中保存您最后的性能级别。因此,应用可以使用这两种模式来支持撤销操作。
最后,您必须记住,在 memento 模式中存储 Memento 对象是强制性的,这样您就可以恢复到以前的状态。在命令模式中,没有必要存储命令。一旦你执行一个命令,它的工作就完成了。如果您不支持“撤销”操作,您可能根本不会对存储这些命令感兴趣。
我明白管理员不应该在备忘录上做手术。所以,演示 1 没问题。但是我看到在客户端代码中,我可以使用下面几行代码创建一个 Memento 对象并设置一个状态,没有人阻止我。这是正确的吗?
//For Q&A session only(Shouldn't be used)
currentMemento = new Memento();
currentMemento.State = "Arbitrary state set by caretaker";
接得好。这是演示 1 的潜在缺点。对于管理员类,试着记住 GoF 中的以下几点。
-
这个班负责备忘录的保管。
-
它从不操作或检查备忘录的内容。
在演示 2 中,我注意到了这几点。所以,穿过它;这是一个相对复杂的例子。
修改的实现
在这个例子中,我试图阻止从客户端代码直接访问备忘录。以下是一些重要的变化。
-
Memento
类有一个私有构造函数。因此,这个类不能使用外部的new
操作符初始化。 -
Memento
类嵌套在Originator
类中,放在一个单独的文件中(Originator.cs
)。我还制作了Memento
级internal
。 -
为了适应这些变化,
CurrentMemento()
方法修改如下:public Memento CurrentMemento() { //Code segment used in Demonstration-1 //myMemento = new Memento();//error now //myMemento.State = state; //return myMemento; //Modified code for Demonstration-2 return new Memento(this.State); }
看守者(客户端)与演示 1 非常相似,除了这一次,您需要使用发起者。备忘录而不是Memento
。现在我们来看演示 2。
类图
图 19-3 显示了修改后的类图。(请注意,关联线可以连接到最外面的形状,但不能连接到 Visual Studio 类图中的嵌套类型。)
图 19-3
演示 2 的类图
解决方案资源管理器视图
图 19-4 显示了修改后的程序高层结构。
图 19-4
演示 2 的解决方案浏览器视图
演示 2
下面是修改后的实现。
//Originator.cs
using System;
namespace MementoPatternDemo2
{
/// <summary>
/// Originator class
/// As per GoF:
/// 1.It creates a memento that contains a snapshot of its current /// internal state.
/// 2.It uses a memento to restore its internal state.
/// </summary>
class Originator
{
private string state;
//Memento myMemento;//not needed now
public Originator()
{
//Creating a memento with born state.
state = "Snapshot #0.(Born state)";
Console.WriteLine($"Originator's current state is: {state}");
}
public string State
{
get { return state; }
set
{
state = value;
Console.WriteLine($"Originator's current state is: {state}");
}
}
/*
Originator will supply the memento
(which contains it's current state)
in respond to caretaker's request.
*/
public Memento CurrentMemento()
{
//Code segment used in Demonstration-1
//myMemento = new Memento();//error now, because of private constructor
//myMemento.State = state;
//return myMemento;
//Modified code for Demonstration-2
return new Memento(this.State);
}
// Back to an old state (Restore)
public void RestoreMemento(Memento restoreMemento)
{
this.state = restoreMemento.State;
Console.WriteLine($"Restored to state : {state}");
}
/// <summary>
/// Memento class
/// As per GoF:
/// 1.A Memento object stores the snapshot of Originator's internal /// state.
/// 2.Ideally,only the originator that created a memento is allowed /// to access it.
/// </summary>
internal class Memento
{
private string state;
//Now Memento class cannot be initialized outside
private Memento() { }
public Memento(string state)
{
this.state = state;
}
public string State
{
get
{
return state;
}
set
{
state = value;
}
}
}
}
}
//Client.cs
using System;
using System.Collections.Generic;
namespace MementoPatternDemo2
{
class Client
{
static Originator originatorObject;
static Originator.Memento currentMemento;
static void Main(string[] args)
{
Console.WriteLine("***Memento Pattern Demonstration-2.***");
Console.WriteLine("Originator (with nested internal class 'Memento') is maintained in a separate file.\n");
//Originator is initialized.The constructor will create a //born state.
originatorObject = new Originator();
//Cannot create memento inside client code now
//currentMemento = new Originator.Memento();//error:inaccessible
//currentMemento.State = "test";//Also error, because previous line cannot be used
IList<Originator.Memento> savedStates = new List<Originator.Memento>();
/*
Adding a memento the list.This memento stores
the current state of the Origintor.
*/
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #1.
originatorObject.State = "Snapshot #1";
//Adding this memento as a restore point
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #2.
originatorObject.State = "Snapshot #2";
//Adding this memento as a restore point
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #3.
originatorObject.State = "Snapshot #3";
//Adding this memento as a restore point
savedStates.Add(originatorObject.CurrentMemento());
//Snapshot #4\. It is not added as a restore point.
originatorObject.State = "Snapshot #4";
//Available restore points
Console.WriteLine("\nCurrently available restore points are :");
foreach (Originator.Memento m in savedStates)
{
Console.WriteLine(m.State);
}
//Undo's
//Roll back starts...
Console.WriteLine("\nPerforming undo's now.");
for (int i = savedStates.Count; i > 0; i--)
{
//Get a restore point
currentMemento = savedStates[i - 1];
originatorObject.RestoreMemento(currentMemento);
}
//Redo's
Console.WriteLine("\nPerforming redo's now.");
for (int i = 1; i < savedStates.Count; i++)
{
currentMemento = savedStates[i];
originatorObject.RestoreMemento(currentMemento);
}
// Wait for user
Console.ReadKey();
}
}
}
输出
这是输出。您可以看到,除了最初的控制台消息之外,演示 1 和演示 2 的输出是相同的,但是从程序上来说,我在这个示例中加入了更多的约束。
***Memento Pattern Demonstration-2.***
Originator (with nested internal class 'Memento') is maintained in a separate file.
Originator's current state is: Snapshot #0.(Born state)
Originator's current state is: Snapshot #1
Originator's current state is: Snapshot #2
Originator's current state is: Snapshot #3
Originator's current state is: Snapshot #4
Currently available restore points are :
Snapshot #0.(Born state)
Snapshot #1
Snapshot #2
Snapshot #3
Performing undo's now.
Restored to state : Snapshot #3
Restored to state : Snapshot #2
Restored to state : Snapshot #1
Restored to state : Snapshot #0.(Born state)
Performing redo's now.
Restored to state : Snapshot #1
Restored to state : Snapshot #2
Restored to state : Snapshot #3
二十、状态模式
本章介绍了状态模式。
GoF 定义
允许对象在其内部状态改变时改变其行为。该对象看起来会改变它的类。
概念
GoF 的定义很容易理解。它简单地说明了一个对象可以根据它的当前状态改变它的行为。
假设您正在处理一个代码库快速增长的大规模应用。结果,情况变得复杂,您可能需要引入许多 if-else 块/switch 语句来保护各种条件。状态模式适合这样的环境。它允许您的对象基于它们的当前状态表现出不同的行为,并且您可以用不同的类定义特定于状态的行为。
在这种模式中,您根据应用的可能状态进行思考,并相应地分离代码。理想情况下,每个状态都独立于其他状态。您跟踪这些状态,并且您的代码根据当前状态的行为做出响应。例如,假设您正在电视机(TV)上观看一个节目。现在,如果您按下电视遥控器上的静音按钮,电视的状态会发生变化。但是如果电视已经处于关闭模式,则没有变化。
因此,基本思想是,如果您的代码可以跟踪应用的当前状态,您就可以集中任务,分离您的代码,并相应地做出响应。
真实世界的例子
考虑一个网络连接的场景,比如 TCP 连接。一个对象可以处于各种状态;例如,连接可能刚刚建立,连接可能已关闭,或者对象正在通过连接进行侦听。当这个连接收到来自其他对象的请求时,它会根据其当前状态做出响应。
交通信号或电视的功能是状态模式的其他例子。例如,如果电视已经处于开机模式,您可以更换频道。如果它处于关闭模式,它不响应频道改变请求。
计算机世界的例子
TCP 连接的例子就属于这一类。考虑另一个例子。假设您有一个作业处理系统,可以一次处理一定数量的作业。当一个新的作业出现时,系统要么处理该作业,要么发出信号表明它正忙于处理当时能够处理的最大数量的作业。这个忙信号仅仅表明它的作业处理能力总数已经达到,新的作业请求不能立即完成。
履行
这个例子模拟了与电视相关的功能,它有一个控制面板来支持开、关和静音操作。为简单起见,假设在任何给定时间,电视处于以下三种状态中的任何一种:开、关或静音。下面显示了一个名为 IPossibleStates 的接口。
interface IPossibleStates
{
//Users can press any of these buttons-On, Off or Mute
void PressOnButton(TV context);
void PressOffButton(TV context);
void PressMuteButton(TV context);
}
三个具体的类——On
、Off
和Mute
——实现了这个接口。基本功能可以描述如下。最初,电视处于关闭状态。因此,当您按下控制面板上的“开”按钮时,电视将进入“开”状态,如果您按下“静音”按钮,电视将进入静音状态。
假设您在电视处于关闭状态时按下关闭按钮;如果您在电视处于打开状态时按下 On 按钮;或者,如果您在电视处于静音模式时按下静音按钮,电视的状态不会改变。电视可以从打开状态或静音状态进入关闭状态(当您按下关闭按钮时)。图 20-1 是反映所有可能场景的状态图。
图 20-1
电视的不同状态
Points to Remember
-
在该图中,我没有将任何状态标记为最终状态,尽管在图 20-1 中,我切换到关闭电视。
-
为了使设计更简单,假设如果在电视处于关闭状态时按下关闭(或静音)键;或者如果您在电视处于打开状态时按下 On 按钮;或者,如果您在电视处于静音模式时按下静音按钮,电视的状态不会改变。但在现实世界中,遥控器的工作方式可能会有所不同。例如,如果电视当前处于打开状态,您按下静音按钮,电视将进入静音模式;如果再次按下静音按钮,电视可能会返回到打开状态。因此,您可能需要相应地更新您的程序逻辑。
电视有一个控制面板,支持开、关和静音操作。所以,在 TV 类内部,有三种方法:ExecuteOffButton()
,
ExecuteOnButton()
,和ExecuteMuteButton()
如下。
public void ExecuteOffButton()
{
Console.WriteLine("You pressed Off button.");
//Delegating the state behavior
currentState.PressOffButton(this);
}
public void ExecuteOnButton()
{
Console.WriteLine("You pressed On button.");
//Delegating the state behavior
currentState.PressOnButton(this);
}
public void ExecuteMuteButton()
{
Console.WriteLine("You pressed Mute button.");
//Delegating the state behavior
currentState.PressMuteButton(this);
}
我授权国家行为。例如,当您按下ExecuteMuteButton()
时,控件会根据电视机的当前状态调用PressMuteButton(...)
。
现在让我们跟随类图。
类图
图 20-2 显示了类图的重要部分。
图 20-2
类图
解决方案资源管理器视图
图 20-3 显示了程序的高层结构。
图 20-3
解决方案资源管理器视图
示范
下面是完整的实现。
using System;
namespace StatePattern
{
interface IPossibleStates
{
//Users can press any of these buttons-On, Off or Mute
void PressOnButton(TV context);
void PressOffButton(TV context);
void PressMuteButton(TV context);
}
//Subclasses does not contain any local state.
//Only one unique instance of IPossibleStates is required.
/// <summary>
/// Off state behavior
/// </summary>
class Off : IPossibleStates
{
public Off()
{
Console.WriteLine("---TV is Off now.---\n");
}
//TV is Off now, user is pressing On button
public void PressOnButton(TV context)
{
Console.WriteLine("TV was Off.Going from Off to On state.");
context.CurrentState = new On();
}
//TV is Off already, user is pressing Off button again
public void PressOffButton(TV context)
{
Console.WriteLine("TV was already in Off state.So, ignoring this opeation.");
}
//TV is Off now, user is pressing Mute button
public void PressMuteButton(TV context)
{
Console.WriteLine("TV was already off.So, ignoring this operation.");
}
}
/// <summary>
/// On state behavior
/// </summary>
class On : IPossibleStates
{
public On()
{
Console.WriteLine("---TV is On now.---\n");
}
//TV is On already, user is pressing On button again
public void PressOnButton(TV context)
{
Console.WriteLine("TV is already in On state.Ignoring repeated on button press operation.");
}
//TV is On now, user is pressing Off button
public void PressOffButton(TV context)
{
Console.WriteLine("TV was on.So,switching off the TV.");
context.CurrentState = new Off();
}
//TV is On now, user is pressing Mute button
public void PressMuteButton(TV context)
{
Console.WriteLine("TV was on.So,moving to silent mode.");
context.CurrentState = new Mute();
}
}
/// <summary>
/// Mute state behavior
/// </summary>
class Mute : IPossibleStates
{
public Mute()
{
Console.WriteLine("---TV is in Mute mode now.---\n");
}
/*
Users can press any of these buttons at this state-On, Off or Mute.TV is in mute, user is pressing On button.
*/
public void PressOnButton(TV context)
{
Console.WriteLine("TV was in mute mode.So, moving to normal state.");
context.CurrentState = new On();
}
//TV is in mute, user is pressing Off button
public void PressOffButton(TV context)
{
Console.WriteLine("TV was in mute mode. So, switching off the TV.");
context.CurrentState = new Off();
}
//TV is in mute already, user is pressing mute button again
public void PressMuteButton(TV context)
{
Console.WriteLine(" TV is already in Mute mode, so, ignoring this operation.");
}
}
/// <summary>
/// TV is the context class
/// </summary>
class TV
{
private IPossibleStates currentState;
public IPossibleStates CurrentState
{
get
{
return currentState;
}
/*
Usually this value will be set by the class that implements the interface "IPossibleStates"
*/
set
{
currentState = value;
}
}
public TV()
{
//Starting with Off state
this.currentState = new Off();
}
public void ExecuteOffButton()
{
Console.WriteLine("You pressed Off button.");
//Delegating the state behavior
currentState.PressOffButton(this);
}
public void ExecuteOnButton()
{
Console.WriteLine("You pressed On button.");
//Delegating the state behavior
currentState.PressOnButton(this);
}
public void ExecuteMuteButton()
{
Console.WriteLine("You pressed Mute button.");
//Delegating the state behavior
currentState.PressMuteButton(this);
}
}
/// <summary>
/// Client code
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***State Pattern Demo***\n");
//TV is initialized with Off state.
TV tv = new TV();
Console.WriteLine("User is pressing buttons in the following sequence:");
Console.WriteLine("Off->Mute->On->On->Mute->Mute->Off\n");
//TV is already in Off state
tv.ExecuteOffButton();
//TV is already in Off state, still pressing the Mute button
tv.ExecuteMuteButton();
//Making the TV on
tv.ExecuteOnButton();
//TV is already in On state, pressing On button again
tv.ExecuteOnButton();
//Putting the TV in Mute mode
tv.ExecuteMuteButton();
//TV is already in Mute, pressing Mute button again
tv.ExecuteMuteButton();
//Making the TV off
tv.ExecuteOffButton();
// Wait for user
Console.Read();
}
}
}
输出
这是输出。
***State Pattern Demo***
---TV is Off now.---
User is pressing buttons in the following sequence:
Off->Mute->On->On->Mute->Mute->Off
You pressed Off button.
TV was already in Off state.So, ignoring this opeation.
You pressed Mute button.
TV was already off.So, ignoring this operation.
You pressed On button.
TV was Off.Going from Off to On state.
---TV is On now.---
You pressed On button.
TV is already in On state.Ignoring repeated on button press operation.
You pressed Mute button.
TV was on.So,moving to silent mode.
---TV is in Mute mode now.---
You pressed Mute button.
TV is already in Mute mode, so, ignoring this operation.
You pressed Off button.
TV was in mute mode. So, switching off the TV.
---TV is Off now.---
问答环节
你能详细说明这种模式在现实世界中是如何工作的吗?
心理学家已经多次证明了这样一个事实,即人类在放松的心情下可以发挥出最佳水平。然而,在相反的情况下,当他们的头脑充满紧张时,他们不能产生伟大的结果。这就是为什么他们总是建议你在放松的心情下工作。所以,同样的工作,可以是享受的,也可以是无聊的,看你现在的心情。
你可以再想想我们的演示例子。假设你想看你最喜欢的球队获胜时刻的电视直播。要观看和享受这一时刻,您需要先打开电视。如果此时电视无法正常工作,无法处于打开状态,您就无法享受这一时刻。所以,如果你想通过你的电视享受这一刻,首要的标准就是电视要把它的状态从关变成开。当对象的内部状态改变时,如果您想在对象中设计类似的行为改变,状态模式是很有帮助的。
在这个例子中,你只考虑了电视 的三种状态:开、关和静音。可以有许多其他状态;例如,可能存在处理连接问题或不同显示条件的状态。你为什么忽略了这些问题?
直截了当的回答是,为了简单起见,我忽略了这些状态。如果系统中状态的数量显著增加,那么维护系统就变得很困难(这是与这种设计模式相关的关键挑战之一)。但是如果你理解这个实现,你可以很容易地添加任何你想要的状态。
我注意到 GoF 在他们著名的著作中为国家模式和策略模式 描绘了一个相似的结构。我对此感到困惑。
是的,结构是相似的,但是你需要记住他们的意图是不同的。当你使用策略模式时,你得到了一个子类化的更好的选择。在状态设计模式中,不同类型的行为可以封装在一个状态对象中,并且上下文被委托给这些状态中的任何一个。当上下文的内部状态改变时,它的行为也会改变。因此,状态模式可以被认为是策略模式的动态版本。
在某些情况下,状态模式还可以帮助您避免许多if
条件。例如,如果电视处于关闭状态,它就不能进入静音状态。从这个状态,它只能进入 On 状态。因此,如果您不喜欢状态设计模式,您可能需要像这样编写代码。
class TV
{
//Some code before
public void ExecuteOnButton()
{
if(currentState==Off )
{
Console.WriteLine("You pressed On button. Going from Off to OnState");
//Some code after
}
if(currentState==On )
{
Console.WriteLine("You pressed On button. TV is already in on state. So, ignoring this opeation.");
//Some code after
}
else
{
Console.WriteLine("TV was on. Moving into mute mode now.");
}
//Some code after
}
您需要对不同种类的按钮按压重复这些检查(例如,对于ExecuteOffButton()
和ExecuteMuteButton()
方法,您需要重复这些检查并相应地编程)。所以,如果你不从状态的角度考虑,随着时间的推移,用大量的if-else
处理不同的条件是非常具有挑战性的,当代码库持续增长时,这可能会很困难。
在你的例子中,你是如何实现 开/关原理 的?
这些 TV 状态中的每一个都被关闭进行修改,但是您可以向 TV 类添加一个新的状态。
20.5 策略模式和状态模式有什么共同特征?
状态模式可以被认为是一种动态策略模式。这两种模式都促进了组合和委托。
在我看来,这些状态对象就像单态对象一样。这是正确的吗?
是的,这是一个很好的观察。在这个例子中,IPossibleStates 的具体子类不包含任何本地状态,因此,在这个应用中,只有一个 state 实例在工作。大多数时候,这种模式的行为是相似的。
20.7 为什么使用上下文作为方法参数?你能在这样的陈述中避免它们吗?
void PressOnButton(TV context);
利用上下文,我在保存状态。此外,IPossibleStates 的具体子类不包含任何本地状态。因此,在这个应用中,只有一个状态实例在工作。所以,这个结构帮助你评估你是在不同的状态之间变化,还是已经处于相同的状态。注意输出。这些上下文帮助您获得如下输出。
"You pressed Mute button.
TV was already off.So, ignoring this operation."
20.8 状态设计模式有哪些利弊?
优点如下。
-
您已经看到,通过遵循打开/关闭原则,您可以轻松地添加新状态和扩展状态的行为。此外,状态行为可以毫无争议地扩展。例如,在这个实现中,您可以为 TV 类添加新的状态和新的行为,而无需更改 TV 类本身。
-
它减少了
if-else
语句。换句话说,条件复杂性降低了。(参见对问题 20.3 的回答。)
使用这种模式有一个缺点。
- 状态模式也被称为状态的对象,因此您可以假设更多的状态需要更多的代码,并且明显的副作用是维护更加困难。
在这些实现中,TV 是一个具体的类。在这种情况下,你为什么不编程接口?
我假设 TV 类不会改变,所以忽略了这一部分以减少程序的代码量。但是是的,你总是可以从一个界面开始,例如,ITv,
,你可以在其中定义合同。
20.10 在 TV 类的构造函数中,你正在用一个关闭的状态初始化电视。所以,状态和上下文类都可以触发状态转换?
是的。
二十一、中介模式
本章涵盖了中介模式。
GoF 定义
定义一个封装一组对象如何交互的对象。Mediator 通过防止对象显式地相互引用来促进松散耦合,并允许您独立地改变它们的交互。
概念
中介是一组对象通过其进行通信的中介,但它们不能直接相互引用。中介负责控制和协调它们之间的交互。因此,您可以减少不同对象之间的直接互连数量。因此,使用这种模式,您可以减少应用中的耦合。
真实世界的例子
当飞机需要起飞时,会进行一系列的验证。这些类型的验证确认所有组件和单个零件(可能相互依赖)都处于完美状态。
另一个例子是当不同飞机的飞行员(他们正在接近或离开终端区域)与机场塔台通信时。他们不明确地与不同航空公司的其他飞行员交流。他们只是把他们的状态发送给控制塔。这些塔发送信号来确认谁可以起飞(或降落)。你必须注意,这些塔并不控制整个飞行。它们仅在端子区域实施约束。
计算机世界的例子
当客户端处理业务应用时,您可能需要实现一些约束。例如,假设您有一个表单,客户需要提供他们的用户 id 和密码来访问他们的帐户。在同一表单中,您可能需要提供其他必填字段,如电子邮件 ID、通信地址、年龄等。让我们假设您正在应用如下的约束。
首先,检查用户提供的用户 ID 是否有效。如果是有效的 id,则仅启用密码字段。提供这两个字段后,您可能需要检查用户是否提供了电子邮件 ID。让我们进一步假设,在提供了所有这些信息(有效的用户 id、密码、格式正确的电子邮件 ID 等等)之后,您的 Submit 按钮被启用。换句话说,如果用户提供了有效的用户 id、密码、有效的电子邮件 ID 和其他必需的详细信息,则 Submit 按钮被启用。您还可以确保用户 ID 是一个整数,因此如果用户错误地在该字段中提供了任何字符,提交按钮将保持禁用模式。在这种情况下,中介模式变得非常方便。
简而言之,当一个程序由许多类组成,并且逻辑分布在它们之间时,代码变得更难阅读和维护。在这些场景中,如果您想要对系统的行为进行新的更改,这可能会很困难,除非您使用中介模式。
履行
维基百科描述了 Mediator 模式,如图 21-1 (摘自 GoF)。
图 21-1
中介模式示例
参与者描述如下。
-
Mediator
:定义了提供Colleague
对象间通信的接口。 -
ConcreteMediator
:它知道并维护Colleague
对象的列表。它实现了Mediator
接口,并协调了Colleague
对象之间的通信。 -
Colleague
:定义与其他同事沟通的接口。 -
ConcreteColleague(s)
:一个具体的同事必须实现Colleague
接口。这些对象通过中介相互通信。
在演示 1 中,我用 AbstractFriend
和Friend
代替了Colleague
和ConcreteColleague(s)
。(是的,你可以假设是友好的环境。)在这个例子中,有三个参与者,分别是 Amit、Sohel 和 Joseph,他们通过聊天服务器相互通信。在这种情况下,聊天服务器扮演中介者的角色。
在下面的例子中,IMediator
是接口,用易于理解的注释定义。
interface IMediator
{
// To register a friend
void Register(AbstractFriend friend);
// To send a message from one friend to another friend
void Send(AbstractFriend fromFriend, AbstractFriend toFriend,string msg);
// To display currently registered objects/friends.
void DisplayDetails();
}
ConcreteMediator
类实现了这个接口,这个类维护注册参与者的列表。所以,在这个类中,你还会看到下面几行代码。
// List of friends
List<AbstractFriend> participants = new List<AbstractFriend>();
除此之外,中介只允许注册用户相互通信并成功发布消息。因此,ConcreteMediator class
中的Send()
方法检查发送者和接收者是否都是注册用户。该方法定义如下。
public void Send(AbstractFriend fromFriend, AbstractFriend toFriend,string msg)
{
// Verifying whether the sender is a registered user or not.
if (participants.Contains(fromFriend))
{
// Verifying whether the receiver is a registered user or not.
if (participants.Contains(toFriend))
{
Console.WriteLine($"\n[{fromFriend.Name}] posts: {msg}Last message posted {DateTime.Now}");
System.Threading.Thread.Sleep(1000);
// Target receiver will receive this message.
toFriend.ReceiveMessage(fromFriend, msg);
}
// Target receiver is NOT a registered user
else
{
Console.WriteLine($"\n{fromFriend.Name}, you cannot send message to {toFriend.Name} because he is NOT a registered user.");
}
}
// Message sender is NOT a registered user
else
{
Console.WriteLine($"\nAn outsider named {fromFriend.Name} of [{fromFriend.GetType()}] is trying to send a message to {toFriend.Name}.");
}
}
在这个例子中,有另一个继承层次,其中我使用了AbstractFriend
作为一个抽象类,这样你就不能直接实例化它。相反,你可以从继承自AbstractFriend
的具体类Friend
或Stranger,
中实例化对象。这个继承层次结构如下。
/// <summary>
/// AbstractFriend class
/// Making it an abstract class, so that you cannot instantiate it directly.
/// </summary>
abstract class AbstractFriend
{
IMediator mediator;
// Using auto property
public string Name { get; set; }
// Constructor
public AbstractFriend(IMediator mediator)
{
this.mediator = mediator;
}
public void SendMessage(AbstractFriend toFriend,string msg)
{
mediator.Send(this,toFriend, msg);
}
public void ReceiveMessage(AbstractFriend fromFriend, string msg)
{
Console.WriteLine($"{this.Name} has received a message from {fromFriend.Name} saying: {msg} ");
}
}
/// <summary>
/// Friend class
/// </summary>
class Friend : AbstractFriend
{
// Constructor
public Friend(IMediator mediator)
: base(mediator)
{
}
}
/// <summary>
/// Another class called Stranger
/// </summary>
class Stranger : AbstractFriend
{
// Constructor
public Stranger(IMediator mediator)
: base(mediator)
{
}
}
Note
遵循基本中介模式的核心架构,我使用了两个不同的具体类来演示这样一个事实,即您应该而不是假设通信对象应该只来自同一个类。
在客户端代码中,您会看到以下参与者:两个来自Friend
类,一个来自Stranger
类。
// 3 persons-Amit,Sohel,Joseph
// Amit and Sohel from Friend class
Friend friend1 = new Friend(mediator);
friend1.Name = "Amit";
Friend friend2 = new Friend(mediator);
friend2.Name = "Sohel";
// Joseph is from Stranger class
Stranger stranger1 = new Stranger(mediator);
stranger1.Name = "Joseph";
这些人可以通过聊天服务器进行交流。因此,在传递消息之前,他们首先向聊天服务器注册,如下所示。
// Registering the participants
mediator.Register(friend1);
mediator.Register(friend2);
mediator.Register(stranger1);
在节目的最后,我介绍了两个人:托德和杰克。托德是一个Friend
类对象,杰克是一个Stranger
类对象。但是它们都没有向中介对象注册;所以中介不允许他们向期望的对象发送消息。
如果 Jack 在发送消息之前向中介注册,就可以正确地发送消息,如下所示。
mediator.Register(stranger1); // Disabled in Demonstration1
stranger1.SendMessage(friend3,"Hello friend...");
同样的评论也适用于Todd
。
类图
图 21-2 显示了类图的重要部分。
图 21-2
类图
解决方案资源管理器视图
图 21-3 显示了程序的高层结构。
图 21-3
解决方案资源管理器视图
演示 1
这是完整的演示。
using System;
using System.Collections.Generic;
namespace MediatorPattern
{
interface IMediator
{
// To register a friend
void Register(AbstractFriend friend);
// To send a message from one friend to another friend
void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg);
// To display currently registered objects/friends.
void DisplayDetails();
}
// ConcreteMediator
class ConcreteMediator : IMediator
{
// List of friends
List<AbstractFriend> participants = new List<AbstractFriend>();
public void Register(AbstractFriend friend)
{
participants.Add(friend);
}
public void DisplayDetails()
{
Console.WriteLine("Current list of registered participants is as follows:");
foreach (AbstractFriend friend in participants)
{
Console.WriteLine($"{friend.Name}");
}
}
/*
The mediator allows only registered users
to communicate each other and post messages
successfully. So, the following method
checks whether both the sender and receiver
are registered users or not.
*/
public void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg)
{
// Verifying whether the sender is a registered user or not
if (participants.Contains(fromFriend))
{
/* Verifying whether the receiver is a registered user or not */
if (participants.Contains(toFriend))
{
Console.WriteLine($"\n[{fromFriend.Name}] posts: {msg}Last message posted {DateTime.Now}");
System.Threading.Thread.Sleep(1000);
/* Target receiver will receive this message.*/
toFriend.ReceiveMessage(fromFriend, msg);
}
else
{
Console.WriteLine($"\n{fromFriend.Name}, you cannot send message to {toFriend.Name} because he is NOT a registered user.");
}
}
// Message sender is NOT a registered user.
else
{
Console.WriteLine($"\nAn outsider named {fromFriend.Name} of [{fromFriend.GetType()}] is trying to send a message to {toFriend.Name}.");
}
}
}
/// <summary>
/// AbstractFriend class
/// Making it an abstract class, so that you cannot instantiate it directly.
/// </summary>
abstract class AbstractFriend
{
IMediator mediator;
// Using auto property
public string Name { get; set; }
// Constructor
public AbstractFriend(IMediator mediator)
{
this.mediator = mediator;
}
public void SendMessage(AbstractFriend toFriend, string msg)
{
mediator.Send(this, toFriend, msg);
}
public void ReceiveMessage(AbstractFriend fromFriend, string msg)
{
Console.WriteLine($"{this.Name} has received a message from {fromFriend.Name} saying: {msg} ");
}
}
/// <summary>
/// Friend class
/// </summary>
class Friend : AbstractFriend
{
// Constructor
public Friend(IMediator mediator)
: base(mediator)
{
}
}
/// <summary>
/// Another class called Stranger
/// </summary>
class Stranger : AbstractFriend
{
// Constructor
public Stranger(IMediator mediator)
: base(mediator)
{
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Mediator Pattern Demonstration.***\n");
IMediator mediator = new ConcreteMediator();
//AbstractFriend afriend = new AbstractFriend(mediator);//error
// 3 persons-Amit, Sohel, Joseph
// Amit and Sohel from Friend class
Friend friend1 = new Friend(mediator);
friend1.Name = "Amit";
Friend friend2 = new Friend(mediator);
friend2.Name = "Sohel";
// Joseph is from Stranger class
Stranger stranger1 = new Stranger(mediator);
stranger1.Name = "Joseph";
// Registering the participants
mediator.Register(friend1);
mediator.Register(friend2);
mediator.Register(stranger1);
// Displaying the participant's list
mediator.DisplayDetails();
Console.WriteLine("Communication starts among participants...");
friend1.SendMessage(friend2, "Hi Sohel, can we discuss the mediator pattern?");
friend2.SendMessage(friend1, "Hi Amit, Yup, we can discuss now.");
stranger1.SendMessage(friend1, " How are you?");
// Another friend who does not register to the mediator
Friend friend4 = new Friend(mediator);
friend4.Name = "Todd";
/*
Todd is NOT a registered user.
So,he cannot send this message to Joseph.
*/
friend4.SendMessage(stranger1, "Hello Joseph...");
/*
Todd is NOT a registered user.
So,he cannot receive this message from Amit.
*/
friend1.SendMessage(friend4, "Hello Todd...");
// An outsider person tries to participate
Stranger stranger2 = new Stranger(mediator);
stranger2.Name = "Jack";
//mediator.Register(stranger1);
// This message cannot reach Joseph, because Jack
// is not the registered user.
stranger2.SendMessage(stranger1, "Hello friend...");
// Wait for user
Console.Read();
}
}
}
输出
这是输出。
***Mediator Pattern Demonstration.***
Current list of registered participants is as follows:
Amit
Sohel
Joseph
Communication starts among participants...
[Amit] posts: Hi Sohel, can we discuss the mediator pattern?Last message posted 15-05-2020 11:13:08
Sohel has received a message from Amit saying: Hi Sohel, can we discuss the mediator pattern?
[Sohel] posts: Hi Amit, Yup, we can discuss now. Last message posted 15-05-2020 11:13:09
Amit has received a message from Sohel saying: Hi Amit, Yup, we can discuss now.
[Joseph] posts: How are you? Last message posted 15-05-2020 11:13:10
Amit has received a message from Joseph saying: How are you?
An outsider named Todd of [MediatorPattern.Friend] is trying to send a message to Joseph.
Amit, you cannot send message to Todd because he is NOT a registered user.
An outsider named Jack of [MediatorPattern.Stranger] is trying to send a message to Joseph.
分析
请注意,只有注册用户才能相互通信并成功发布消息。调解人不允许任何外人进入系统。(注意输出的最后几行)。
Point to Remember
你不应该假设总是应该有一对一的沟通。这是因为 GoF 声明中介用一对多交互代替了多对多交互。但在这一章中,我假设所有的消息都是私有的,不应该广播给所有人;因此,我举了一个例子,其中中介只将消息发送给预期的接收者。只有当外人试图在聊天服务器中发布消息时,中介才会广播消息以警告其他人。
问答环节
你为什么要把事情复杂化?在前面的例子中,每个参与者都可以彼此直接对话,而您可以绕过中介。这是正确的吗?
在这个例子中,您只有三个注册的参与者,中介只允许他们互相通信。因此,似乎只有三个参与者,他们可以直接相互交流。但是考虑一个更复杂的场景,让我们给这个应用添加另一个约束,它声明当且仅当目标参与者仅处于在线模式(这是聊天服务器的常见场景)时,参与者才可以向目标参与者发送消息。如果不使用中介者模式,仅仅检查参与者是否是有效用户是不够的;除此之外,您还需要在发布消息之前检查目标收件人的在线状态。而如果参与人数不断增长,你能想象系统的复杂程度吗?因此,中介可以将您从这种场景中解救出来,因为您可以将所有验证标准放在中介中。图 21-4 和 21-5 更好地描绘了这一场景。
图 21-5
案例 2:有调解人
图 21-4
情况 1:不使用调解器
修改的实现
在修改后的示例中,如果一个参与者和另一个参与者都是注册用户,并且接收者仅在线,则他们可以向另一个参与者发送消息。中介负责将消息发送到正确的目的地,但是在它发送消息之前,参与者的在线状态是已知的。
图 21-5 暗示在类似的场景中,中介可以检查所有对象的状态并维护发送消息的逻辑。所以,我们来修改一下程序。请注意,我为每个参与者添加了一个州。因此,您可以在AbstractFriend
类中看到这个新的代码段。
// New property for Demonstration 2
public string Status { get; set; }
演示 2
下面是修改后的实现。
using System;
using System.Collections.Generic;
namespace MediatorPatternModifiedDemo
{
interface IMediator
{
// To register a friend
void Register(AbstractFriend friend);
// To send a message from one friend to another friend
void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg);
// To display currently registered objects/friends.
void DisplayDetails();
}
// ConcreteMediator
class ConcreteMediator : IMediator
{
// List of friends
List<AbstractFriend> participants = new List<AbstractFriend>();
public void Register(AbstractFriend friend)
{
participants.Add(friend);
}
public void DisplayDetails()
{
Console.WriteLine("Current list of registered participants is as follows:");
foreach (AbstractFriend friend in participants)
{
Console.WriteLine($"{friend.Name}");
}
}
/*
The mediator allows only registered users
to communicate with each other and post messages
successfully. So, the following method
checks whether both the sender and receiver
are registered users or not.
*/
public void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg)
{
// Verifying whether the sender is a registered user or not.
if (participants.Contains(fromFriend))
{
/* Verifying whether the receiver is a registered user and he is online.*/
if (participants.Contains(toFriend) && toFriend.Status=="On")
{
Console.WriteLine($"\n[{fromFriend.Name}] posts: {msg}Last message posted {DateTime.Now}");
System.Threading.Thread.Sleep(1000);
//Target receiver will receive this message.
toFriend.ReceiveMessage(fromFriend, msg);
}
else
{
Console.WriteLine($"\n{fromFriend.Name},at this moment, you cannot send message to {toFriend.Name} because he is either not a registered user or he is currently offline.");
}
}
//Message sender is NOT a registered user.
else
{
Console.WriteLine($"\nAn outsider named {fromFriend.Name} of [{fromFriend.GetType()}] is trying to send a message to {toFriend.Name}.");
}
}
}
/// <summary>
/// AbstractFriend class
/// Making it an abstract class, so that you cannot instantiate it /// directly.
/// </summary>
abstract class AbstractFriend
{
IMediator mediator;
// Using auto property
public string Name { get; set; }
// New property for Demonstration 2
public string Status { get; set; }
// Constructor
public AbstractFriend(IMediator mediator)
{
this.mediator = mediator;
}
public void SendMessage(AbstractFriend toFriend, string msg)
{
mediator.Send(this, toFriend, msg);
}
public void ReceiveMessage(AbstractFriend fromFriend, string msg)
{
Console.WriteLine($"{this.Name} has received a message from {fromFriend.Name} saying: {msg} ");
}
}
/// <summary>
/// Friend class
/// </summary>
class Friend : AbstractFriend
{
// Constructor
public Friend(IMediator mediator)
: base(mediator)
{
}
}
/// <summary>
/// Another class called Stranger
/// </summary>
class Stranger : AbstractFriend
{
// Constructor
public Stranger(IMediator mediator)
: base(mediator)
{
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Mediator Pattern Modified Demonstration.***\n");
IMediator mediator = new ConcreteMediator();
//AbstractFriend afriend = new AbstractFriend(mediator);//error
// 3 persons-Amit, Sohel, Joseph
// Amit and Sohel from Friend class
Friend friend1 = new Friend(mediator);
friend1.Name = "Amit";
friend1.Status = "On";
Friend friend2 = new Friend(mediator);
friend2.Name = "Sohel";
friend2.Status = "On";
// Joseph is from Stranger class
Stranger stranger1 = new Stranger(mediator);
stranger1.Name = "Joseph";
stranger1.Status = "On";
// Registering the participants
mediator.Register(friend1);
mediator.Register(friend2);
mediator.Register(stranger1);
// Displaying the participant's list
mediator.DisplayDetails();
Console.WriteLine("Communication starts among participants...");
friend1.SendMessage(friend2, "Hi Sohel,can we discuss the mediator pattern?");
friend2.SendMessage(friend1, "Hi Amit,Yup, we can discuss now.");
stranger1.SendMessage(friend1, " How are you?");
// Another friend who does not register to the mediator
Friend friend4 = new Friend(mediator);
friend4.Name = "Todd";
// This message cannot reach Joseph, because Todd
// is not the registered user.
friend4.SendMessage(stranger1, "Hello Joseph...");
// This message will NOT reach Todd because he
// is not a registered user.
friend1.SendMessage(friend4, "Hello Todd...");
// An outsider tries to participate
Stranger stranger2 = new Stranger(mediator);
stranger2.Name = "Jack";
//mediator.Register(stranger1);
// This message cannot reach Joseph, because Jack
// is not the registered user.
stranger2.SendMessage(stranger1, "Hello friend...");
Console.WriteLine("Sohel is going to offline now.");
friend2.Status = "Off";
/*
Since Sohel is offline, he will NOT receive
this message.
*/
friend1.SendMessage(friend2, "Hi Sohel, I have a gift for you.");
Console.WriteLine("Sohel is online again.");
friend2.Status = "On";
stranger1.SendMessage(friend2, "Hi Sohel, Amit was looking for you.");
// Wait for user
Console.Read();
}
}
}
输出
这是修改后的输出。
***Mediator Pattern Modified Demonstration.***
Current list of registered participants is as follows:
Amit
Sohel
Joseph
Communication starts among participants...
[Amit] posts: Hi Sohel,can we discuss the mediator pattern?Last message posted 15-05-2020 11:30:50
Sohel has received a message from Amit saying: Hi Sohel,can we discuss the mediator pattern?
[Sohel] posts: Hi Amit,Yup, we can discuss now.Last message posted 15-05-2020 11:30:51
Amit has received a message from Sohel saying: Hi Amit,Yup, we can discuss now.
[Joseph] posts: How are you?Last message posted 15-05-2020 11:30:52
Amit has received a message from Joseph saying: How are you?
An outsider named Todd of [MediatorPatternModifiedDemo.Friend] is trying to send a message to Joseph.
Amit,at this moment, you cannot send message to Todd because he is either not a registered user or he is currently offline.
An outsider named Jack of [MediatorPatternModifiedDemo.Stranger] is trying to send a message to Joseph.
Sohel is going to offline now.
Amit,at this moment, you cannot send message to Sohel because he is either not a registered user or he is currently offline.
Sohel is online again.
[Joseph] posts: Hi Sohel, Amit was looking for you.Last message posted 15-05-2020 11:30:53
Sohel has received a message from Joseph saying: Hi Sohel, Amit was looking for you.
Note
前面输出中的一些行被加粗,以展示修改后的程序的影响(演示 2)。
现在您可以看到,当且仅当一个参与者在线时,他才可以向另一个参与者发送消息。中介负责将消息发送到正确的目的地,在发送消息之前,它确保两个参与者都是注册用户。
21.2 使用中介模式有什么好处?
以下是一些优点。
-
您可以降低系统中对象通信的复杂性。
-
该模式促进了松散耦合。因此,对象可以重用。
-
该模式减少了系统中子类的数量。
-
您用一对多关系替换了多对多关系,因此代码更容易阅读和理解。这样做的一个明显效果是,维护变得更加容易。
-
您可以使用这种模式提供集中控制。
-
简而言之,从代码中去除紧密耦合总是一个好的目标,在这种情况下,中介模式得分很高。
21.3 使用中介模式的缺点是什么?
以下几点应对这些挑战。
-
在某些情况下,实现适当的封装变得棘手,中介对象的架构变得复杂。
-
有时维护一个复杂的中介会成为一个大问题。
21.4 如果您需要添加一个新的规则或逻辑,您可以直接将其添加到中介器中。这是正确的吗?
是的。
我在门面模式 和中介模式之间找到了一些相似之处。这是正确的吗?
是的。Steve Holzner 在他的书中提到了相似性,他将中介模式描述为一个复用的门面模式。在 Mediator 模式中,不是使用单个对象的接口,而是在多个对象之间创建一个多路复用的接口来实现平滑过渡。
在这个模式中,你减少了不同对象之间的相互联系。由于这一缩减,您获得了哪些主要好处?
对象之间更多的互连会创建一个难以改变的整体系统(因为行为分布在许多对象中)。另一个副作用是,您可能需要创建许多子类来将这些更改引入系统。
21.7 在这两个实现中,你都在使用 Thread.Sleep(1000)
。这是什么原因呢?
你可以忽略它。我用这个来模拟现实生活中的场景。我假设参与者在正确阅读消息后发布消息,此活动至少需要 1 秒钟。
二十二、责任链模式
本章涵盖了责任链模式。
GoF 定义
通过给多个对象一个处理请求的机会,避免将请求的发送方耦合到接收方。链接接收对象,并沿着链传递请求,直到有对象处理它。
概念
在这种模式中,您形成了一个对象链,在这个对象链中,您将任务的责任从一个对象传递到另一个对象,直到一个对象接受完成任务的责任。链中的每个对象都可以处理特定类型的请求。如果一个对象不能完全处理请求,它会将请求传递给链中的下一个对象。这个过程可以持续到链的末端。这种请求处理机制为您提供了在链中添加新处理对象(处理程序)的灵活性。图 22-1 显示了这样一个有 N 个处理器的链。
图 22-1
责任链模式
真实世界的例子
大多数软件组织都有一些客户服务代表,他们接受客户的反馈,并将任何问题转发给组织中适当的部门。但是,这两个部门不能同时解决这个问题。看似负责的部门会首先查看问题,如果这些员工认为问题应该转发给另一个部门,他们就会转发。
当病人去医院看病时,你可能会看到类似的场景。如果认为有必要,一个部门的医生可以将病人转到另一个部门(以作进一步诊断)。
你也可以考虑移动公司组织。例如,在印度,沃达丰移动公司有一个客户服务部。如果你有投诉,你首先向客户服务部提出问题。如果他们不能解决你的问题,你可以把它升级到一个节点官员。如果您对节点官员给出的解决方案不满意,您可以进一步将问题升级到上诉官员。
计算机世界的例子
考虑一个可以发送电子邮件和传真的软件应用(例如打印机)。因此,任何客户都可以报告传真问题或电子邮件问题,所以您需要两种不同类型的错误处理程序:EmailErrorHandler
和FaxErrorHandler
。你可以放心地假设EmailErrorHandler
只处理电子邮件错误,它不对传真错误负责。同样,FaxErrorHandler
处理传真错误,不关心电子邮件错误。
您可以像这样形成一个链:每当您的应用发现一个错误,它就抛出一张票并转发错误,希望其中一个处理程序会处理它。让我们假设请求首先到达FaxErrorhandler
。如果这个处理程序同意这是一个传真问题,它就处理它;否则,它将问题转发给EmailErrorHandler
。
注意,这里的链以EmailErrorHandler
结束。但是如果您需要处理另一种类型的问题,比如身份验证问题,由于安全漏洞,您可以创建一个AuthenticationErrorHandler
并将其放在EmailErrorHandler
之后。现在,如果一个EmailErrorHandler
也不能完全解决问题,它会将问题转发给AuthenticationErrorHandler
,这个链就此结束。
Points to Remember
这只是一个例子;您可以按照自己喜欢的任何顺序随意放置这些处理程序。底线是处理链可能会在以下两种情况下结束:
-
处理程序可以完全处理请求。
-
你已经到了链条的末端。
当您在 C# 应用中使用多个 catch 块实现异常处理机制时,您会看到类似的机制。如果 try 块中出现异常,第一个 catch 块会尝试处理它。如果它不能处理这种类型的异常,下一个 catch 块将尝试处理它,并遵循相同的机制,直到该异常被一些处理程序(catch 块)正确处理。如果应用中的最后一个 catch 块也无法处理它,则会在此链之外引发异常。
履行
让我们假设在下面的例子中,你为我刚刚讨论的计算机世界例子编写程序。在这个例子中,我假设我们需要处理来自电子邮件或传真的不同种类的消息。客户还可以将这些消息标记为普通优先级或高优先级。所以,在程序的开始,你会看到下面的代码段。
/// <summary>
/// Message priorities
/// </summary>
public enum MessagePriority
{
Normal,
High
}
/// <summary>
/// Message class
/// </summary>
public class Message
{
public string Text { get; set; }
public MessagePriority Priority;
public Message(string msg, MessagePriority priority)
{
this.Text = msg;
this.Priority = priority;
}
}
这次我选择了一个抽象的Receiver
类,因为我想在它的派生类之间共享一些公共功能。
Points to Note
或者,您可以选择一个接口并使用默认接口方法的概念,这在 C# 8 中是受支持的。因为遗留版本不支持这一点,所以我为这个例子选择了抽象类。
Receiver
类如下所示。
abstract class Receiver
{
protected Receiver nextReceiver;
//To set the next handler in the chain. public void NextReceiver(Receiver nextReceiver)
{
this.nextReceiver = nextReceiver;
}
public abstract void HandleMessage(Message message);
}
FaxErrorHandler
和EmailErrorHandler
类继承自Receiver
,它们在这个程序中充当具体的处理程序。为了演示一个非常简单的用例,我可以在FaxErrorHandler
.
中使用下面的代码段
if (message.Text.Contains("fax"))
{
Console.WriteLine($"FaxErrorHandler processed { message.Priority } priority issue: { message.Text }");
}
else if (nextReceiver != null)
{
nextReceiver.HandleMessage(message);
}
Points to Remember
在前面的代码段中,您可以看到,如果一条消息包含单词 fax ,那么 FaxErrorHandler 会处理它;否则,它会将问题传递给下一个处理程序。同样,在接下来的例子中,如果一条消息包含单词 email ,那么 EmailErrorHandler 将处理这条消息,以此类推。所以,你可能会问,如果一条消息中同时包含了电子邮件和传真,会发生什么?我在接下来的例子中处理了这个问题,但是为了简单起见,您可以忽略使用这段代码的情况。在现实世界的问题中,一个错误会导致另一个错误;因此,当传真代码库中出现错误时,相同的错误会传播到电子邮件代码库(如果它们共享一个公共代码库)。一个通用的补丁可以解决这两个问题。在接下来的例子中,我将向您展示何时应该传递问题,以及如何将问题传递给下一个处理者。因此,首先,您可能会忽略单个支柱的复杂性。
实际上,一个组织可能更喜欢实现一个基于人工智能的机制来首先分析一个问题,然后根据症状,他们可以将问题转发给一个特定的部门,但在核心部分,您可能会看到这种模式。
为了演示一条消息同时包含单词 email 和单词 fax 的情况,我对FaxErrorHandler
,
使用了一个相对复杂的结构,如下所示(相关的注释可以作为您的指南)。
class FaxErrorHandler : Receiver
{
bool messagePassedToNextHandler = false;
public override void HandleMessage(Message message)
{
// Start processing if the error message contains "fax"
if (message.Text.Contains("fax"))
{
Console.WriteLine("FaxErrorHandler processed {0} priority issue: {1}", message.Priority, message.Text);
/*
Do not leave now, if the error message contains 'email' too.
*/
if (nextReceiver != null && message.Text.Contains("email"))
{
Console.WriteLine("I've fixed fax side defect.Now email team needs to work on top of this fix.");
nextReceiver.HandleMessage(message);
// We'll not pass the message repeatedly to next handler
messagePassedToNextHandler = true;
}
}
if (nextReceiver != null && messagePassedToNextHandler != true)
{
nextReceiver.HandleMessage(message);
}
}
}
EmailErrorHandler
与此类似。现在,如果你有一条包含电子邮件和传真的消息,像"Neither the fax nor email is working,"
一样,这个相对复杂的结构可以帮助你得到下面的输出,你可以看到两个团队都在处理缺陷:
FaxErrorHandler processed High priority issue: Neither fax nor email are working.
I've fixed fax side defect. Now email team needs to work on top of this fix.
EmailErrorHandler processed High priority issue: Neither fax nor email are working.
Email side defect is fixed. Now fax team needs to cross verify this fix.
在我的链的末端,有一个UnknownErrorHandler
声明这个问题既不是来自Email
也不是来自Fax
;所以你需要咨询专业的开发者来解决这个问题。
class UnknownErrorHandler : Receiver
{
public override void HandleMessage(Message message)
{
if (!(message.Text.Contains("fax")|| message.Text.Contains("email")))
{
Console.WriteLine("Unknown error occurs.Consult experts immediately.");
}
else if (nextReceiver != null)
{
nextReceiver.HandleMessage(message);
}
}
}
最后,错误处理程序对象的形成非常简单明了,如下所示。
// Different handlers
Receiver emailHandler = new EmailErrorHandler();
Receiver faxHandler = new FaxErrorHandler();
Receiver unknownHandler = new UnknownErrorHandler();
从下面的代码段,你可以很容易地理解如何形成一个处理程序链。
/*
Making the chain :
FaxErrorhandler->EmailErrorHandler->UnknownErrorHandler.
*/
faxHandler.NextReceiver(emailHandler);
emailHandler.NextReceiver(unknownHandler);
类图
图 22-2 为类图。
图 22-2
类图
解决方案资源管理器视图
图 22-3 显示了程序的高层结构。
图 22-3
解决方案资源管理器视图
示范
这是完整的程序。
using System;
namespace ChainOfResponsibilityPattern
{
/// <summary>
/// Message priorities
/// </summary>
public enum MessagePriority
{
Normal,
High
}
/// <summary>
/// Message class
/// </summary>
public class Message
{
public string Text { get; set; }
public MessagePriority Priority;
public Message(string msg, MessagePriority priority)
{
this.Text = msg;
this.Priority = priority;
}
}
/// <summary>
/// Abstract class -Receiver
/// The abstract class is chosen to share
/// the common codes across derived classes.
/// </summary>
abstract class Receiver
{
protected Receiver nextReceiver;
//To set the next handler in the chain.
public void NextReceiver(Receiver nextReceiver)
{
this.nextReceiver = nextReceiver;
}
public abstract void HandleMessage(Message message);
}
/// <summary>
/// FaxErrorHandler class
/// </summary>
class FaxErrorHandler : Receiver
{
bool messagePassedToNextHandler = false;
public override void HandleMessage(Message message)
{
//Start processing if the error message contains "fax"
if (message.Text.Contains("fax"))
{
Console.WriteLine($"FaxErrorHandler processed {message.Priority} priority issue: {message.Text}");
//Do not leave now, if the error message contains email too.
if (nextReceiver != null && message.Text.Contains("email"))
{
Console.WriteLine("I've fixed fax side defect.Now email team needs to work on top of this fix.");
nextReceiver.HandleMessage(message);
//We'll not pass the message repeatedly to next handler.
messagePassedToNextHandler = true;
}
}
if (nextReceiver != null && messagePassedToNextHandler != true)
{
nextReceiver.HandleMessage(message);
}
}
}
/// <summary>
/// EmailErrorHandler class
/// </summary>
class EmailErrorHandler : Receiver
{
bool messagePassedToNextHandler = false;
public override void HandleMessage(Message message)
{
//Start processing if the error message contains "email"
if (message.Text.Contains("email"))
{
Console.WriteLine($"EmailErrorHandler processed {message.Priority} priority issue: {message.Text}");
//Do not leave now, if the error message contains "fax" too.
if (nextReceiver != null && message.Text.Contains("fax"))
{
Console.WriteLine("Email side defect is fixed.Now fax team needs to cross verify this fix.");
//Keeping the following code here.
//It can be useful if you place this handler before fax //error handler
nextReceiver.HandleMessage(message);
//We'll not pass the message repeatedly to the next //handler.
messagePassedToNextHandler = true;
}
}
if (nextReceiver != null && messagePassedToNextHandler != true)
{
nextReceiver.HandleMessage(message);
}
}
}
/// <summary>
/// UnknownErrorHandler class
/// </summary>
class UnknownErrorHandler : Receiver
{
public override void HandleMessage(Message message)
{
if (!(message.Text.Contains("fax") || message.Text.Contains("email")))
{
Console.WriteLine("Unknown error occurs.Consult experts immediately.");
}
else if (nextReceiver != null)
{
nextReceiver.HandleMessage(message);
}
}
}
/// <summary>
/// Client code
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Chain of Responsibility Pattern Demo***\n");
//Different handlers
Receiver emailHandler = new EmailErrorHandler();
Receiver faxHandler = new FaxErrorHandler();
Receiver unknownHandler = new UnknownErrorHandler();
/*
Making the chain :
FaxErrorhandler->EmailErrorHandler->UnknownErrorHandler.
*/
faxHandler.NextReceiver(emailHandler);
emailHandler.NextReceiver(unknownHandler);
Message msg = new Message("The fax is reaching late to the destination.", MessagePriority.Normal);
faxHandler.HandleMessage(msg);
msg = new Message("The emails are not reaching to the destinations.", MessagePriority.High);
faxHandler.HandleMessage(msg);
msg = new Message("In email, CC field is disabled always.", MessagePriority.Normal);
faxHandler.HandleMessage(msg);
msg = new Message("The fax is not reaching to the destination.", MessagePriority.High);
faxHandler.HandleMessage(msg);
msg = new Message("Cannot login into the system.", MessagePriority.High);
faxHandler.HandleMessage(msg);
msg = new Message("Neither fax nor email are working.", MessagePriority.High);
faxHandler.HandleMessage(msg);
Console.ReadKey();
}
}
}
输出
这是输出。
***Chain of Responsibility Pattern Demo***
FaxErrorHandler processed Normal priority issue: The fax is reaching late to the destination.
EmailErrorHandler processed High priority issue: The emails are not reaching to the destinations.
EmailErrorHandler processed Normal priority issue: In email, CC field is disabled always.
FaxErrorHandler processed High priority issue: The fax is not reaching to the destination.
Unknown error occurs.Consult experts immediately.
FaxErrorHandler processed High priority issue: Neither fax nor email are working.
I've fixed fax side defect.Now email team needs to work on top of this fix.
EmailErrorHandler processed High priority issue: Neither fax nor email are working.
Email side defect is fixed.Now fax team needs to cross verify this fix.
问答环节
22.1 在上例中,为什么需要消息优先级?
接得好。实际上,您可以忽略消息优先级,因为为了简单起见,您只是在处理程序中搜索文本电子邮件或传真。我添加这些优先级是为了美化代码。不要为电子邮件和传真使用单独的处理程序,你可以创建一个不同类型的链来处理基于优先级的消息。但是在我们的演示中,我没有形成基于优先级的链,因为我假设从事传真支柱工作的开发人员不太了解电子邮件支柱,反之亦然。
22.2 使用责任链设计模式有什么好处?
一些显著的优点如下。
-
您有多个对象来处理一个请求。(如果一个处理程序不能处理整个请求,它可以将责任转发给链中的下一个处理程序。)
-
链的节点可以动态添加或删除。此外,你可以打乱他们的顺序。例如,在前面的应用中,如果您看到大多数缺陷来自电子邮件,那么您可能会将
EmailErrorHandler
放置为第一个处理程序,以节省应用的平均处理时间。 -
处理程序不需要知道链中的下一个处理程序如何处理请求。它可以专注于它的处理机制。
-
在这个模式中,您将(请求的)发送者与接收者分离。
22.3 使用责任链设计模式有哪些挑战?
以下几点描述了一些挑战。
-
不能保证请求得到处理,因为您可能到达了链的末端,但是没有找到任何显式的接收者来处理请求。
-
对于这种设计,调试变得很棘手。
22.4 如果到达了链的末端,但没有处理程序处理请求,你如何处理这种情况?
一个简单的解决方案是通过 try/catch(或 try/finally 或 try/catch/finally)块。您可以将所有的处理程序放在try
块中,如果没有一个处理请求,您可以使用适当的消息引发一个异常,并在catch
块中捕获该异常以引起您的注意(或者以某种不同的方式处理它)。
GoF 在类似的背景下谈到了 Smalltalk 的自动转发机制(doesNotUnderstand)。如果一个消息找不到合适的处理程序,它就会在doesNotUnderstand
实现中被捕获,该实现可以被覆盖以在对象的后继中转发消息,将其记录在一个文件中,并将其存储在一个队列中供以后处理,或者您可以简单地执行任何其他操作。但是您必须注意,默认情况下,该方法会引发一个需要正确处理的异常。
我可以说一个处理程序要么完全处理这个请求,要么把它传递给下一个处理程序。这是正确的吗?
是的。
在我看来,观察者模式 和责任链模式有相似之处。这是正确的吗?
在观察者模式中,所有注册用户并行获得通知,但是在责任链模式中,责任链中的对象依次被逐个通知,并且这个过程一直持续到一个对象完全处理通知(或者您到达责任链的末端)。比较结果在观察者模式的“问答环节”部分用图表显示(参见第十四章中的问答 14.4)。
二十三、解释器模式
本章涵盖了解释器模式。
GoF 定义
给定一种语言,为它的语法定义一个表示,以及一个使用该表示来解释该语言中的句子的解释器。
概念
这种模式扮演着翻译者的角色,它经常被用来评估一种语言中的句子。所以,你首先需要定义一个语法来表示这种语言。然后解释器处理语法。当语法简单时,这种模式是最好的。
Points to Note
为了更好地理解这种模式,熟悉自动机中的单词(或句子)、语法、语言等等是很有帮助的,这是一个很大的话题。对它的详细讨论超出了本书的范围。现在,你知道在正式语言中,字母表可能包含无限数量的元素,一个单词可以是有限的字母序列(简单地说是字符串),由语法生成的所有字符串的集合称为语言生成的语法(G)。通常,语法由元组(V,T,S,P)表示,其中 V 是一组非终结符,T 是一组终结符,S 是开始符,P 是产生式规则。例如,如果你有一个语法 G = (V,T,S,P)其中
V={S},
T={a,b},
P={S->aSbS,S->bSaS,S->ε },
S={S};
ε表示空字符串。该语法可以生成相同数量的 a 和 b,如 ab、ba、abab、baab 等等。例如,以下步骤显示了获取 abba 的推导过程。
S
aSbS [since S->aSbS]
abS [since S->ε]
abbSaS [since S->bSaS]
abbaS [since S->ε]
abba [sinceS->ε]
同样的方法,可以生成 baab 。下面是推导步骤,作为快速参考。
S
bSaS [since S->bSaS]
baS [sinceS->ε]
baaSbS [since S->aSbS]
baabS [sinceS->ε]
baab [sinceS->ε]
这个模式中的每个类可能代表语言中的一个规则,它应该有一个解释表达式的方法。因此,为了处理更多的规则,您需要创建更多的类,这就是为什么解释器模式很少用于处理非常复杂的语法。
让我们考虑计算器程序中不同的算术表达式。虽然这些表达式是不同的,但它们都是使用一些基本规则构造的,并且这些规则是在语言的语法中定义的(这些算术表达式)。因此,如果您能够解释这些规则的一般组合,而不是将每个不同的规则组合视为单独的情况,这将是一个更好的想法。在这样的场景中可以使用解释器模式,当您看到演示 2 的细节时,就会明白这一点。但在此之前,我们先来看看演示 1 中一个相对简单的例子。
这种模式的典型结构通常用类似于图 23-1 的图表来描述。
图 23-1
典型解释器模式的结构
术语描述如下。
-
抽象表达式通常是一个带有解释器方法的接口。您需要向该方法传递一个上下文对象。
-
终端表达式用于终端表达式。终结表达式是不需要其他表达式来解释的表达式。它们是数据结构中的叶节点(即,它们没有子节点)。
-
非终结符用于非终结符表达式。它也称为交替表达式、重复表达式和顺序表达式。这就像可以包含终结和非终结表达式的组合。当您对此调用
Interpret()
方法时,您会对它的所有子对象调用Interpret()
。在演示 2 中,您将看到它们的运行。 -
上下文保存解释器需要的全局信息。
-
客户端调用
Interpret()
方法。可选地,它可以基于语言的规则构建语法树。
Points to Remember
-
解释器用简单的语法规则处理语言。理想情况下,开发人员不想创建他们自己的语言,这就是他们很少使用这种模式的原因。
-
本章中有两个演示,它们互不相关。第一个相对简单,但是第二个比较复杂,涉及更多的代码。
-
在第一个演示中,您将一个三位数的数字转换成它的对等单词形式。这个程序是从这本书的前一版微调而来的。
-
第二个程序使用解释器模式作为规则验证器,并解释细节。我的书 Java 设计模式 (Apress,2018)用多个例子讨论了同一个概念。
真实世界的例子
现实世界的例子包括翻译外语的翻译。音乐家扮演着音符解释者的角色,也就是“语法”
计算机世界的例子
Java 编译器将 Java 源代码解释成 Java 虚拟机能够理解的字节码。在 C# 中,源代码被转换为由公共语言运行库(CLR)解释的 MSIL 中间代码。在执行时,这个 MSIL 被实时(JIT)编译器转换成本机代码(二进制可执行代码)。
履行
一般来说,你用一个类来表示这些语法规则。让我们定义一个简单的规则,如下所示。
-
E::= E1E2E3
-
E1:=零百(s) |一百(s) |两百(s) |…|九百(s)
-
E2:=零十(s) |一个十(s) | "两个十(s) | …|九十
-
E3:=和零|和一|和二|和三|…|和九
为了简单和更好的可读性,我用四个类来表示这个语法:InputExpression
表示 E(一个抽象类)HundredExpression
表示 E1TensExpression
表示 E2UnitExpression
表示 E 3 。所以,在接下来的节目(演示 1)中,789
被解释为Seven hundred(s) Eight ten(s) and Nine.
在演示 1 中,Context
类非常容易理解。它有一个公共构造函数,接受一个名为input,
的字符串参数,这个参数稍后会以 word 形式解释。该类还包含一个只读属性Input
和一个名为Output
的读写属性,定义如下。
public class Context
{
private string input;
public string Input {
get
{
return input;
}
}
public string Output { get; set; }
// The constructor
public Context(string input)
{
this.input = input;
}
}
抽象类InputExpression
拥有抽象方法Interpret(...)
,它被它的具体子类HundredExpression
、TensExpression
和UnitExpression
覆盖。这个类还包含一个具体的方法GetWord(string str)
,它在所有具体的子类中使用。我将这个方法放在这个抽象类中,这样我就可以简单地避免在具体的子类中重复这些代码。这个类如下。
// The abstract class-will hold the common code.
abstract class InputExpression
{
public abstract void Interpret(Context context);
public string GetWord(string str)
{
switch (str)
{
case "1":
return "One";
case "2":
return "Two";
case "3":
return "Three";
case "4":
return "Four";
case "5":
return "Five";
case "6":
return "Six";
case "7":
return "Seven";
case "8":
return "Eight";
case "9":
return "Nine";
case "0":
return "Zero";
default:
return "*";
}
}
}
在具体的子类中,您可以看到内置的Substring
方法从输入中选择想要的数字。下面一行显示了这一点。
string hundreds = context.Input.Substring(0, 1);
最后,在客户端代码中,在给定的上下文中解释input
之前,我使用了一个名为EvaluateInputWithContext
的独立方法来构建解析树。所以,你会看到下面几行。
// Building the parse tree
List<InputExpression> expTree = new List<InputExpression>();
expTree.Add(new HundredExpression());
expTree.Add(new TensExpression());
expTree.Add(new UnitExpression());
// Interpret the input
foreach (InputExpression inputExp in expTree)
{
inputExp.Interpret(context);
}
// some other code..
剩下的代码很容易理解,所以让我们继续。
类图
图 23-2 为类图。
图 23-2
类图
解决方案资源管理器视图
图 23-3 显示了程序各部分的高层结构。
图 23-3
解决方案资源管理器视图
演示 1
这是完整的演示。
using System;
using System.Collections.Generic;
namespace InterpreterPattern
{
public class Context
{
private string input;
public string Input {
get
{
return input;
}
}
public string Output { get; set; }
// The constructor
public Context(string input)
{
this.input = input;
}
}
// The abstract class. It will hold the common code
abstract class InputExpression
{
public abstract void Interpret(Context context);
public string GetWord(string str)
{
switch (str)
{
case "1":
return "One";
case "2":
return "Two";
case "3":
return "Three";
case "4":
return "Four";
case "5":
return "Five";
case "6":
return "Six";
case "7":
return "Seven";
case "8":
return "Eight";
case "9":
return "Nine";
case "0":
return "Zero";
default:
return "*";
}
}
}
class HundredExpression : InputExpression
{
public override void Interpret(Context context)
{
string hundreds = context.Input.Substring(0,1);
context.Output += GetWord(hundreds) + " hundred(s) ";
}
}
class TensExpression : InputExpression
{
public override void Interpret(Context context)
{
string tens = context.Input.Substring(1,1);
context.Output += GetWord(tens) + " ten(s) ";
}
}
class UnitExpression : InputExpression
{
public override void Interpret(Context context)
{
string units = context.Input.Substring(2, 1);
context.Output += "and "+GetWord(units);
}
}
// Client Class
class Client
{
public static void Main(String[] args)
{
Console.WriteLine("***Interpreter Pattern Demonstation-1.***\n");
Console.WriteLine(" It will validate first three digit of a valid number.");
string inputString="789";
EvaluateInputWithContext(inputString);
inputString = "456";
EvaluateInputWithContext(inputString);
inputString = "123";
EvaluateInputWithContext(inputString);
inputString = "075";
EvaluateInputWithContext(inputString);
inputString = "Ku79";//invalid input
EvaluateInputWithContext(inputString);
Console.ReadLine();
}
public static void EvaluateInputWithContext(string inputString)
{
Context context = new Context(inputString);
//Building the parse tree
List<InputExpression> expTree = new List<InputExpression>();
expTree.Add(new HundredExpression());
expTree.Add(new TensExpression());
expTree.Add(new UnitExpression());
// Interpret the input
foreach (InputExpression inputExp in expTree)
{
inputExp.Interpret(context);
}
if (!context.Output.Contains("*"))
Console.WriteLine($" {context.Input} is interpreted as {context.Output}");
else
{
Console.WriteLine($" {context.Input} is not a valid input.");
}
}
}
}
输出
这是输出。
***Interpreter Pattern Demonstation-1.***
It will validate first three digit of a valid number.
789 is interpreted as Seven hundred(s) Eight ten(s) and Nine
456 is interpreted as Four hundred(s) Five ten(s) and Six
123 is interpreted as One hundred(s) Two ten(s) and Three
075 is interpreted as Zero hundred(s) Seven ten(s) and Five
Ku79 is not a valid input.
另一个实现
让我们看看这种模式的另一种用法。当您考虑实现该模式时,有一些重要的步骤(在本例中遵循这些步骤)。这些如下。
-
第一步定义你想为之构建解释器的语言的规则。
-
第二步定义一个抽象类或者接口来表示一个表达式。它应该包含一个解释表达式的方法。
-
步 2A 识别终结符和非终结符表达式。例如,在接下来的例子中,
IndividualEmployee
类是一个终端表达式类。 -
步 2B 创建非终结符表达式类。他们每个人都在他们的子节点上调用解释方法。例如,在接下来的例子中,
OrExpression
和AndExpression
类是非终结表达式类。
-
-
步骤 3 使用这些类构建抽象语法树。您可以在客户端代码中完成这项工作,或者您可以创建一个单独的类来完成任务。
-
客户现在使用这个树来解释一个句子。
-
步骤 5 将上下文传递给解释器。它通常有需要解释的句子。解释器也可以使用这个上下文执行一些额外的任务。
Points to Note
在接下来的程序中,我使用解释器模式作为规则验证器。
在这里,我用不同的员工的“经验年数”和当前的等级来举例说明。为了简单起见,有四个不同级别的员工:G1、G2、G3 和 G4。所以,你会看到下面几行。
Employee emp1 = new IndividualEmployee(5, "G1");
Employee emp2 = new IndividualEmployee(10, "G2");
Employee emp3 = new IndividualEmployee(15, "G3");
Employee emp4 = new IndividualEmployee(20, "G4");
我想在上下文中验证一个规则,它告诉你要被提升,一个员工应该至少有 10 年的经验,并且他应该来自 G2 级或 G3 级。一旦这些表达式被解释,你会看到布尔值的输出。您可以在Main()
方法中看到下面几行代码。
// Minimum Criteria for promoton is:
// The year of experience is minimum 10 yrs. and
// Employee grade should be either G2 or G3
List<string> allowedGrades = new List<string> { "G2", "G3" };
Context context = new Context(10, allowedGrades);
可以看到,允许的成绩存储在一个列表中,并传递给了Context
类构造函数。因此,Context
类中的以下代码片段对您来说可能有意义。
private int experienceReqdForPromotion;
private List<string> allowedGrades;
public Context(int experience, List<string> allowedGrades)
{
this.experienceReqdForPromotion = experience;
this.allowedGrades = new List<string>();
foreach (string grade in allowedGrades)
{
this.allowedGrades.Add(grade);
}
}
Employee
是与Interpret(...)
方法的接口,如下所示。
interface Employee
{
bool Interpret(Context context);
}
正如我之前告诉你的,在这个例子中,IndividualEmployee
类充当叶节点。这个类如下实现了Employee
接口方法。
public bool Interpret(Context context)
{
if (this.yearOfExperience >= context.GetYearofExperience()
&& context.GetPermissibleGrades().Contains(this.currentGrade))
{
return true;
}
return false;
}
现在让我们来处理这个例子中一些复杂的规则或表达式。在客户端代码中,您可以看到第一个复杂的规则,如下所示。
Console.WriteLine("Is emp1 and any of emp2, emp3, emp4 is eligible for promotion?" + builder.BuildTreeBasedOnRule1(emp1, emp2, emp3, emp4).Interpret(context));
Console.WriteLine("Is emp2 and any of emp1, emp3, emp4 is eligible for promotion?"+ builder.BuildTreeBasedOnRule1(emp2, emp1, emp3, emp4).Interpret(context));
// and so on..
第二个复杂规则的形式如下。
Console.WriteLine("Is emp1 or (emp2 but not emp3) is eligible for promotion?"+ builder.BuildTreeBasedOnRule2(emp1, emp2, emp3).Interpret(context));
Console.WriteLine("Is emp2 or (emp3 but not emp4) is eligible for promotion?"+ builder.BuildTreeBasedOnRule2(emp2, emp3, emp4).Interpret(context));
所以,你可能会问这些规则是如何运作的?答案如下:另一个类EmployeeBuilder
,有评估这些规则的方法。您将很快看到详细的实现,但是现在,让我们看一下形成第一个规则的一步一步的过程,如下所示,带有支持注释。
// Building the tree
//Complex Rule-1: emp1 and (emp2 or (emp3 or emp4))
public Employee BuildTreeBasedOnRule1(Employee emp1, Employee emp2, Employee emp3, Employee emp4)
{
// emp3 or emp4
Employee firstPhase = new OrExpression(emp3, emp4);
// emp2 or (emp3 or emp4)
Employee secondPhase = new OrExpression(emp2, firstPhase);
// emp1 and (emp2 or (emp3 or emp4))
Employee finalPhase = new AndExpression(emp1, secondPhase);
return finalPhase;
}
AndExpression
、OrExpression,
和NotExpression
是实现接口Employee,
的三个具体类,因此它们都有自己的Interpret(...)
方法。例如,AndExpression
实现Interpret(...)
方法如下。
public bool Interpret(Context context)
{
return emp1.Interpret(context) && emp2.Interpret(context);
}
同样,OrExpression
实现Interpret(...)
方法如下。
public bool Interpret(Context context)
{
return emp1.Interpret(context) || emp2.Interpret(context);
}
并且NotExpression
实现了如下相同的方法。
public bool Interpret(Context context)
{
return !emp.Interpret(context);
}
您可以看到每个复合表达式都在调用其所有子表达式的Interpret()
方法。剩下的代码很容易理解,让我们继续。
Note
这种设计模式不会指导您如何构建语法树或如何解析句子。它给你自由,让你决定如何前进。
类图
图 23-4 为类图。
图 23-4
类图
解决方案资源管理器视图
图 23-5 显示了程序各部分的高层结构。
图 23-5
解决方案资源管理器视图
演示 2
下面是完整的实现。
using System;
using System.Collections.Generic;
namespace InterpreterPatternDemo2
{
interface Employee
{
bool Interpret(Context context);
}
/// <summary>
/// IndividualEmployee class
/// </summary>
class IndividualEmployee : Employee
{
private int yearOfExperience;
private string currentGrade;
public IndividualEmployee(int experience, string grade)
{
this.yearOfExperience = experience;
this.currentGrade = grade;
}
public bool Interpret(Context context)
{
if (this.yearOfExperience >= context.GetYearofExperience()
&& context.GetPermissibleGrades().Contains(this.currentGrade))
{
return true;
}
return false;
}
}
/// <summary>
/// OrExpression class
/// </summary>
class OrExpression : Employee
{
private Employee emp1;
private Employee emp2;
public OrExpression(Employee emp1, Employee emp2)
{
this.emp1 = emp1;
this.emp2 = emp2;
}
public bool Interpret(Context context)
{
return emp1.Interpret(context) || emp2.Interpret(context);
}
}
/// <summary>
/// AndExpression class
/// </summary>
class AndExpression : Employee
{
private Employee emp1;
private Employee emp2;
public AndExpression(Employee emp1, Employee emp2)
{
this.emp1 = emp1;
this.emp2 = emp2;
}
public bool Interpret(Context context)
{
return emp1.Interpret(context) && emp2.Interpret(context);
}
}
/// <summary>
/// NotExpression class
/// </summary>
class NotExpression : Employee
{
private Employee emp;
public NotExpression(Employee expr)
{
this.emp = expr;
}
public bool Interpret(Context context)
{
return !emp.Interpret(context);
}
}
/// <summary>
/// Context class
/// </summary>
class Context
{
private int experienceReqdForPromotion;
private List<string> allowedGrades;
public Context(int experience, List<string> allowedGrades)
{
this.experienceReqdForPromotion = experience;
this.allowedGrades = new List<string>();
foreach (string grade in allowedGrades)
{
this.allowedGrades.Add(grade);
}
}
public int GetYearofExperience()
{
return experienceReqdForPromotion;
}
public List<string> GetPermissibleGrades()
{
return allowedGrades;
}
}
/// <summary>
/// EmployeeBuilder class
/// </summary>
class EmployeeBuilder
{
// Building the tree
// Complex Rule-1: emp1 and (emp2 or (emp3 or emp4))
public Employee BuildTreeBasedOnRule1(Employee emp1, Employee emp2, Employee emp3, Employee emp4)
{
// emp3 or emp4
Employee firstPhase = new OrExpression(emp3, emp4);
// emp2 or (emp3 or emp4)
Employee secondPhase = new OrExpression(emp2, firstPhase);
// emp1 and (emp2 or (emp3 or emp4))
Employee finalPhase = new AndExpression(emp1, secondPhase);
return finalPhase;
}
// Complex Rule-2: emp1 or (emp2 and (not emp3 ))
public Employee BuildTreeBasedOnRule2(Employee emp1, Employee emp2, Employee emp3)
{
// Not emp3
Employee firstPhase = new NotExpression(emp3);
// emp2 or (not emp3)
Employee secondPhase = new AndExpression(emp2, firstPhase);
// emp1 and (emp2 or (not emp3 ))
Employee finalPhase = new OrExpression(emp1, secondPhase);
return finalPhase;
}
}
public class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Interpreter Pattern Demonstration-2***\n");
// Minimum Criteria for promoton is:
// The year of experience is minimum 10 yrs. and
// Employee grade should be either G2 or G3
List<string> allowedGrades = new List<string> { "G2", "G3" };
Context context = new Context(10, allowedGrades);
Employee emp1 = new IndividualEmployee(5, "G1");
Employee emp2 = new IndividualEmployee(10, "G2");
Employee emp3 = new IndividualEmployee(15, "G3");
Employee emp4 = new IndividualEmployee(20, "G4");
EmployeeBuilder builder = new EmployeeBuilder();
// Validating the 1st complex rule
Console.WriteLine("----- Validating the first complex rule.-----");
Console.WriteLine("Is emp1 and any of emp2, emp3, emp4 is eligible for promotion?"
+ builder.BuildTreeBasedOnRule1(emp1, emp2, emp3, emp4).Interpret(context));
Console.WriteLine("Is emp2 and any of emp1, emp3, emp4 is eligible for promotion?"
+ builder.BuildTreeBasedOnRule1(emp2, emp1, emp3, emp4).Interpret(context));
Console.WriteLine("Is emp3 and any of emp1, emp2, emp3 is eligible for promotion?"
+ builder.BuildTreeBasedOnRule1(emp3, emp1, emp2, emp4).Interpret(context));
Console.WriteLine("Is emp4 and any of emp1, emp2, emp3 is eligible for promotion?"
+ builder.BuildTreeBasedOnRule1(emp4, emp1, emp2, emp3).Interpret(context));
Console.WriteLine("-----Validating the second complex rule now.-----");
//Validating the 2nd complex rule
Console.WriteLine("Is emp1 or (emp2 but not emp3) is eligible for promotion?"
+ builder.BuildTreeBasedOnRule2(emp1, emp2, emp3).Interpret(context));
Console.WriteLine("Is emp2 or (emp3 but not emp4) is eligible for promotion?"
+ builder.BuildTreeBasedOnRule2(emp2, emp3, emp4).Interpret(context));
Console.ReadKey();
}
}
}
输出
这是输出。
***Interpreter Pattern Demonstration-2***
----- Validating the first complex rule.-----
Is emp1 and any of emp2, emp3, emp4 is eligible for promotion?False
Is emp2 and any of emp1, emp3, emp4 is eligible for promotion?True
Is emp3 and any of emp1, emp2, emp3 is eligible for promotion?True
Is emp4 and any of emp1, emp2, emp3 is eligible for promotion?False
-----Validating the second complex rule now.-----
Is emp1 or (emp2 but not emp3) is eligible for promotion?False
Is emp2 or (emp3 but not emp4) is eligible for promotion?True
问答环节
23.1 什么时候应该使用这种模式?
说实话,日常编程中并不太需要。然而,在一些罕见的情况下,您可能需要使用您自己的编程语言,这可能会派上用场。但在你继续之前,你必须问自己,投资回报率(ROI)是多少?
23.2 使用解释器设计模式有什么好处?
以下是一些优点。
-
你参与了为一种语言定义语法以及如何表达和解释句子的过程。你也可以改变和扩展语法。
-
你有充分的自由去解释这些表达。
23.3 与使用解释器设计模式相关的 挑战 有哪些?
我相信工作量是最大的问题。此外,维护复杂的语法变得棘手,因为您可能需要创建(和维护)单独的类来处理不同的规则。
这是本书第一部分的结尾。我希望您喜欢所有 GoF 模式的所有详细实现。现在您可以转到本书的下一部分,这一部分涵盖了其他一些有趣的模式。
二十四、简单工厂模式
本章介绍简单工厂模式。
定义
简单工厂模式创建一个对象,而不向客户机公开实例化逻辑。
概念
在面向对象编程(OOP)中,工厂就是这样一种可以创建其他对象的对象。可以通过多种方式调用工厂,但最常见的是,它使用一种可以返回具有不同原型的对象的方法。任何帮助创建这些新对象的子程序都被认为是一个工厂。最重要的是,它帮助您从应用的消费者那里抽象出对象创建的过程。
真实世界的例子
在南印度餐馆,当你点你最喜欢的印度炒菜时,服务员可能会问你是否喜欢你的印度炒菜多加点香料,或者是否应该少加点香料。根据你的选择,厨师在主料中加入香料,为你做出合适的菜肴。
计算机世界的例子
简单工厂模式在软件应用中很常见,但是在继续之前,请注意以下几点。
-
在 GoF 的著名著作中,简单工厂模式没有被视为标准设计模式,但是这种方法对于您编写的任何应用来说都是常见的,在这些应用中,您希望将变化很大的代码与没有变化的代码部分分开。假设您在编写的所有应用中都遵循这种方法。
-
简单工厂模式被认为是工厂方法模式(和抽象工厂模式)的最简单形式。因此,您可以假设任何遵循工厂方法模式或抽象工厂模式的应用也遵循简单工厂模式的设计目标的概念。
在下面的实现中,我用一个常见的用例来讨论这个模式。让我们来看一下实现。
履行
这些是以下实现的重要特征。
-
在这个例子中,你正在处理两种不同类型的动物:狗和老虎。具体有两个类:
Dog.cs
和Tiger.cs
。每个类都有一个共同的父类,IAnimal.cs
。您会看到以下代码: -
我将创建对象的代码放在不同的地方(特别是在工厂类中)。使用这种方法,当您创建一只狗或一只老虎时,您不需要在客户端代码中直接使用
new
操作符。因此,在客户端代码中,您会看到下面一行:
// IAnimal.cs
namespace SimpleFactory
{
public interface IAnimal
{
void AboutMe();
}
}
// Dog.cs
using System;
namespace SimpleFactory
{
public class Dog : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The dogs says: Bow-Wow.I prefer barking.");
}
}
}
//Tiger.cs
using System;
namespace SimpleFactory
{
public class Tiger : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The tiger says: Halum.I prefer hunting.");
}
}
}
-
在接下来的示例中,创建对象的过程取决于用户输入。我将可能变化的代码与最不可能变化的代码分开。这种机制可以帮助您消除系统中的紧密耦合。因此,在
Main()
中,您会看到下面的代码和支持性的注释:IAnimal preferredType = null; SimpleFactory simpleFactory = new SimpleFactory(); #region The code region that can vary based on users preference /* * Since this part may vary, we're moving the * part to CreateAnimal() of SimpleFactory class. */ preferredType = simpleFactory.CreateAnimal(); #endregion #region The codes that do not change frequently. preferredType.AboutMe(); #endregion
preferredType = simpleFactory.CreateAnimal();
Note
在某些地方,您可能会看到这种模式的变体,其中对象是通过参数化的构造函数(如preferredType=simpleFactory.CreateAnimal("Tiger")
)创建的。
在接下来的例子中,我根据用户的输入选择动物,不需要参数化的构造函数。在本书的早期版本中,我使用了两种方法:Speak()
和Action()
。但是为了使这个例子简短,我选择了一个叫做AboutMe()
的方法。我把前面的两个方法合并成一个方法。
类图
图 24-1 为类图。
图 24-1
类图
解决方案资源管理器视图
图 24-2 显示了程序的高层结构。
图 24-2
解决方案资源管理器视图
示范
下面是完整的实现。程序的所有部分都被分开并放在名称空间 SimpleFactory 中。因此,对于下面的代码段,您可能会多次看到命名空间声明。
//IAnimal.cs
namespace SimpleFactory
{
public interface IAnimal
{
void AboutMe();
}
}
//Dog.cs
using System;
namespace SimpleFactory
{
public class Dog : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The dog says: Bow-Wow.I prefer barking.");
}
}
}
//Tiger.cs
using System;
namespace SimpleFactory
{
public class Tiger : IAnimal
{
public void AboutMe()
{
Console.WriteLine("The tiger says: Halum.I prefer hunting.");
}
}
}
//SimpleFactory.cs
using System;
namespace SimpleFactory
{
public class SimpleFactory
{
public IAnimal CreateAnimal()
{
IAnimal intendedAnimal = null;
Console.WriteLine("Enter your choice(0 for Dog, 1 for Tiger)");
string b1 = Console.ReadLine();
int input;
if (int.TryParse(b1, out input))
{
Console.WriteLine("You have entered {0}", input);
switch (input)
{
case 0:
intendedAnimal = new Dog();
break;
case 1:
intendedAnimal = new Tiger();
break;
default:
Console.WriteLine("You must enter either 0 or 1");
//We'll throw a runtime exception for any other //choices.
throw new ApplicationException(String.Format
(" Unknown Animal cannot be instantiated."));
}
}
return intendedAnimal;
}
}
}
//Program.cs(Client)
using System;
namespace SimpleFactory
{
/*
* A client is interested to get an animal
* who can tell something about it.
*/
class Client
{
static void Main(string[] args)
{
Console.WriteLine("*** Simple Factory Pattern Demo.***\n");
IAnimal preferredType = null;
SimpleFactory simpleFactory = new SimpleFactory();
#region The code region that can vary based on users preference
/*
* Since this part may vary,we're moving the
* part to CreateAnimal() in SimpleFactory class.
*/
preferredType = simpleFactory.CreateAnimal();
#endregion
#region The codes that do not change frequently.
preferredType.AboutMe();
#endregion
Console.ReadKey();
}
}
}
输出
以下是情况 1,用户输入为 0。
*** Simple Factory Pattern Demo.***
Enter your choice(0 for Dog, 1 for Tiger)
0
You have entered 0
The dog says: Bow-Wow.I prefer barking.
下面是情况 2,用户输入 1。
*** Simple Factory Pattern Demo.***
Enter your choice(0 for Dog, 1 for Tiger)
1
You have entered 1
The tiger says: Halum.I prefer hunting.
下面是情况 3,用户输入 3。
*** Simple Factory Pattern Demo.***
Enter your choice(0 for Dog, 1 for Tiger)
3
You have entered 3
You must enter either 0 or 1
在这种情况下,您会得到以下异常:“未知动物无法实例化”(见图 24-3 )。
图 24-3
由于输入无效,出现异常
问答环节
24.1 在这个例子中,我看到客户通过简单的工厂模式委托 对象的创建 。但是他们可以用 new
操作符直接创建对象。这是正确的吗?
不。这些是之前设计背后的主要原因。
-
面向对象设计的关键原则之一是将代码中最有可能发生变化的部分与其余部分分开。
-
在这种情况下,只有对象的创建过程会发生变化。您可以假设有代码片段来描述关于动物的一些事情,并且该部分代码不需要在客户端代码中变化。所以,在将来,如果在创建过程中需要任何更改,您只需要更改
SimpleFactory
类的CreateAnimal()
方法。客户端代码不会因为这些更改而受到影响。 -
您不希望在客户端主体中放置大量的
if-else
块(或switch
语句)。这使得你的代码笨拙。 -
客户端代码看不到您是如何创建对象的。这种抽象提高了安全性。
24.2 与此模式相关的 挑战 有哪些?
如果要添加新的动物或者删除已有的动物,需要修改CreateAnimal()
方法。这个过程违反了 SOLID 原则的开放/封闭原则(即代码模块应该对扩展开放,但对修改关闭)。
Note
罗伯特·c·马丁提出了坚实的原则。有许多在线资源可用。如果你对快速介绍感兴趣,去 https://en.wikipedia.org/wiki/SOLID
。
24.3 你能让工厂类成为静态的吗?
可以,但是必须记住与静态类相关的限制。例如,您不能继承它们,等等。当您处理一些没有实现类或单独接口的值对象时,这是有意义的。当您使用不可变的类时,它也很有用,并且您的工厂类不需要在每次使用它时都返回一个全新的对象。
简而言之,值对象是其相等性基于值而不是身份的对象。值对象最重要的特征是,没有身份,它是不可变的。
一个简单的现实生活中的例子可以用印度的五卢比纸币和五卢比硬币来给出。它们的货币价值是相同的,但它们是不同的实例。
一般来说,静态工厂类可以提升全局状态,这对于面向对象编程来说并不理想。
二十五、空对象模式
本章介绍了空对象模式。
定义
空对象模式不是 GoF 设计模式。我从维基百科上得到这个定义,它是这样说的。
在面向对象的计算机编程中,空对象是指没有引用值或具有已定义的中立(“空”)行为的对象。空对象设计模式描述了此类对象的用途及其行为(或缺乏行为)。它最初发表在程序设计的模式语言系列丛书中。
概念
该模式可以实现“什么都不做”的关系,或者当应用遇到空对象而不是真实对象时,它可以提供默认行为。使用这种模式,我们的核心目标是通过if
块避免“空对象检查”或“空协作检查”,并通过提供不做任何事情的默认行为来封装对象的缺失,从而制定一个更好的解决方案。该模式的基本结构如图 25-1 所示。
图 25-1
空对象模式的基本结构
本章从一个看似没问题的程序开始,但它有一个严重的潜在 bug。当您使用潜在的解决方案分析 bug 时,您会理解对空对象模式的需求。那么,让我们跳到下一节。
错误的程序
让我们假设您有两种不同类型的交通工具:Bus
和Train,
,并且一个客户端可以传递不同的输入(例如,a
和b
)来创建一个Bus
对象或一个Train
对象。下面的程序演示了这一点。当输入有效时,这个程序可以顺利运行,但是当您提供一个无效的输入时,一个潜在的错误就暴露出来了。这是有问题的程序。
using System;
namespace ProgramWithOnePotentialBug
{
interface IVehicle
{
void Travel();
}
class Bus : IVehicle
{
public static int busCount = 0;
public Bus()
{
busCount++;
}
public void Travel()
{
Console.WriteLine("Let us travel with Bus");
}
}
class Train : IVehicle
{
public static int trainCount = 0;
public Train()
{
trainCount++;
}
public void Travel()
{
Console.WriteLine("Let us travel with Train");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***This program demonstrates the need of null object pattern.***\n");
string input = String.Empty;
int totalObjects = 0;
while (input != "exit")
{
Console.WriteLine("Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.");
input = Console.ReadLine();
IVehicle vehicle = null;
switch (input)
{
case "a":
vehicle = new Bus();
break;
case "b":
vehicle = new Train();
break;
case "exit":
Console.WriteLine("Creating one more bus and closing the application");
vehicle = new Bus();
break;
}
totalObjects = Bus.busCount + Train.trainCount;
vehicle.Travel();
Console.WriteLine($"Total objects created in the system ={totalObjects}");
}
}
}
}
具有有效输入的输出
你可能有一个眼前的问题;当你输入exit
时,你创建了一个不必要的对象。这是真的。我们以后再处理。现在,让我们关注另一个对我们来说更危险的 bug。下面是一些有效输入的输出。
***This program demonstrates the need of null object pattern.***
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
a
Let us travel with Bus
Total objects created in the system =1
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
b
Let us travel with Train
Total objects created in the system =2
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
a
Let us travel with Bus
Total objects created in the system =3
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
不需要输入的分析
让我们假设用户错误地提供了一个不同的字符,比如 e ,如下所示。
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
e
这一次,你得到了一个名为System.NullReferenceException
的运行时异常,如图 25-2 所示。
图 25-2
当用户提供无效输入时,会发生运行时异常
潜在的解决办法
您可能想到的直接补救方法是在调用操作之前进行空检查,如下所示。
if (vehicle != null)
{
vehicle.Travel();
}
分析
先前的解决方案在这种情况下有效。但是请考虑一个企业应用。当您对每个场景进行空检查时,如果您在每个场景中都像这样放置if
条件,那么您的代码就会变脏。同时,你可能会注意到维护困难的副作用。空对象模式的概念在类似的情况下很有用。
Point to Remember
在前面的例子中,当用户键入 exit 时,我可以避免创建不必要的对象,如果我使用如下的空条件操作符,也可以避免空检查:
vehicle?.Travel();
该运算符仅在 C# 6 和更高版本中可用。不过,研究一下空对象模式的实现细节对您还是有好处的。例如,当您使用空对象模式时,您可以为这些空对象提供默认行为(最适合您的应用),而不是什么都不做。
真实世界的例子
当有水供应而没有任何内部泄漏时,洗衣机就能正常工作。但是假设有一次,你忘记在开始洗衣服之前供水,但是你按下了开始洗衣服的按钮。在这种情况下,洗衣机不应损坏自身;所以,它可以发出警报声来引起你的注意,并指示此刻没有供水。
计算机世界的例子
假设在客户端-服务器架构中,服务器基于客户端输入进行计算。服务器需要足够智能,不会启动任何不必要的计算。在处理输入之前,它可能希望进行交叉验证,以确保是否需要开始计算,或者应该忽略无效的输入。在这种情况下,您可能会注意到带有空对象模式的命令模式。
基本上,在企业应用中,使用这种设计模式可以避免大量的 null 检查和 if/else 阻塞。下面的实现给出了这种模式的概述。
履行
让我们修改之前讨论过的有问题的程序。这次您通过一个NullVehicle
对象处理无效输入。因此,如果用户错误地提供了任何无效数据(换句话说,除了本例中的 a 或 b 之外的任何输入),应用什么都不做;也就是说,它可以通过一个NullVehicle
对象忽略那些无效输入,这个对象什么也不做。该类定义如下。
/// <summary>
/// NullVehicle class
/// </summary>
class NullVehicle : IVehicle
{
private static readonly NullVehicle instance = new NullVehicle();
private NullVehicle()
{
nullVehicleCount++;
}
public static int nullVehicleCount;
public static NullVehicle Instance
{
get
{
return instance;
}
}
public void Travel()
{
// Do Nothing
}
}
您可以看到,当我创建一个NullVehicle
对象时,我应用了单体设计模式的概念。因为可能有无限多的无效输入,所以在下面的例子中,我不想重复创建NullVehicle
对象。一旦有了一个NullVehicle
对象,我想重用那个对象。
Note
对于空对象方法,您需要返回任何看起来合理的默认值。在我们的例子中,你不能乘坐一辆不存在的车辆。因此,对于NullVehicle
类来说,Travel()
方法什么也不做是有道理的。
类图
图 25-3 为类图。
图 25-3
类图
解决方案资源管理器视图
图 25-4 显示了程序的高层结构。
图 25-4
解决方案资源管理器视图
示范
下面是完整的实现。
using System;
namespace NullObjectPattern
{
interface IVehicle
{
void Travel();
}
/// <summary>
/// Bus class
/// </summary>
class Bus : IVehicle
{
public static int busCount = 0;
public Bus()
{
busCount++;
}
public void Travel()
{
Console.WriteLine("Let us travel with Bus.");
}
}
/// <summary>
/// Train class
/// </summary>
class Train : IVehicle
{
public static int trainCount = 0;
public Train()
{
trainCount++;
}
public void Travel()
{
Console.WriteLine("Let us travel with Train.");
}
}
/// <summary>
/// NullVehicle class
/// </summary>
class NullVehicle : IVehicle
{
private static readonly NullVehicle instance = new NullVehicle();
private NullVehicle()
{
nullVehicleCount++;
}
public static int nullVehicleCount;
public static NullVehicle Instance
{
get
{
return instance;
}
}
public void Travel()
{
// Do Nothing
}
}
/// <summary>
/// Client code
/// </summary>
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***Null Object Pattern Demonstration.***\n");
string input = String.Empty;
int totalObjects = 0;
while (input != "exit")
{
Console.WriteLine("Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit) ");
input = Console.ReadLine();
IVehicle vehicle = null;
switch (input)
{
case "a":
vehicle = new Bus();
break;
case "b":
vehicle = new Train();
break;
case "exit":
Console.WriteLine("Closing the application.");
vehicle = NullVehicle.Instance;
break;
default:
Console.WriteLine("Please supply the correct input(a/b/exit)");
vehicle = NullVehicle.Instance;
break;
}
totalObjects = Bus.busCount + Train.trainCount + NullVehicle.nullVehicleCount;
// No need to do null check now.
//if (vehicle != null)
vehicle.Travel();
//}
Console.WriteLine("Total objects created in the system ={0}",
totalObjects);
}
Console.ReadKey();
}
}
}
输出
这是输出。
***Null Object Pattern Demonstration.***
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
a
Let us travel with Bus.
Total objects created in the system =2
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
b
Let us travel with Train.
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
c
Please supply the correct input(a/b/exit)
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
d
Please supply the correct input(a/b/exit)
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
b
Let us travel with Train.
Total objects created in the system =4
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
exit
Closing the application.
Total objects created in the system =4
分析
我提请你注意以下几点。
-
无效输入及其影响以粗体显示。
-
由于空车辆对象/无效输入,对象计数没有增加。
-
您没有执行任何空值检查。尽管如此,程序执行不会因为无效的用户输入而中断。
问答环节
25.1 在实现的开始,我看到创建了一个额外的对象。这是故意的吗?
为了节省一些计算机内存/存储,我在构造NullVehicle
类时遵循了支持早期初始化的单例设计模式。您不希望为每个无效输入创建一个NullVehicle
对象,因为您的应用可能会收到大量无效输入。如果您不防范这种情况,大量的NullVehicle
对象可能会驻留在系统中(这是无用的),它们会占用大量的计算机内存,这反过来会导致一些不必要的副作用。(例如,系统可能会变慢,应用响应时间可能会增加,等等。)
25.2 什么时候应该使用这种模式?
这种模式在下列情况下很有用。
-
您不希望遇到
NullReferenceException
(例如,如果您错误地试图调用一个空对象的方法)。 -
您喜欢忽略代码中的大量空检查。
-
你想让你的代码更干净,更容易维护。
Note
在这一章的最后,你会学到这种模式的另一种用法。
25.3 与空对象模式相关的 挑战 有哪些?
您需要注意以下情况。
-
大多数情况下,您可能希望找到并修复失败的根本原因。所以,如果你扔一个
NullReferenceException
,那对你来说会更好。您总是可以在try
/catch
块或try
/catch
/finally
块中处理这些异常,并相应地更新日志信息。 -
当您无意中想要处理一个根本不存在的对象时,空对象模式可以帮助您实现一个默认行为。但是试图提供这样的默认行为可能并不总是合适的。
-
空对象模式的不正确实现会抑制程序执行中可能正常出现的真正错误。
25.4。看起来好像空对象像代理一样工作。这是正确的吗?
不会。一般来说,代理在某个时间点作用于真实对象,它们也可能提供一些行为。但是空对象不应该做这样的事情。
25.5。空对象模式总是与 NullReferenceException
相关联。这是正确的吗?
概念是相同的,但是异常名可以不同或特定于语言。例如,在 Java 中,您可以使用此模式来防范 java.lang.NullPointerException,但在 C# 这样的语言中,您使用它来防范 System.NullReferenceException。
最后,我想提请大家注意另一个有趣的点。空对象模式在另一个上下文中很有用。例如,考虑下面的代码段。
//A case study in another context.
List<IVehicle> vehicleList = new List<IVehicle>();
vehicleList.Add(new Bus());
vehicleList.Add(new Train());
vehicleList.Add(null);
foreach (IVehicle vehicle in vehicleList)
{
vehicle.Travel();
}
当你使用前面的代码段时,你再次得到System.NullReferenceException
。但是如果你用vehicleList.Add(NullVehicle.Instance);
代替vehicleList.Add(null);
,就没有运行时异常。因此,您可以轻松地循环,这是该模式的另一个重要用途。
二十六、MVC 模式
本章涵盖了 MVC 模式。
定义
MVC(模型-视图-控制器)是一种架构模式。这种模式通常用于 web 应用和开发强大的用户界面。Trygve Reenskaug 于 1979 年在一篇题为“Smalltalk-80TM 中的应用编程:如何使用模型-视图-控制器”的论文中首次描述了 MVC,这篇论文是在万维网存在之前写的。所以,那时候还没有 web 应用的概念。但是现代的应用是最初概念的改编。一些开发人员宁愿称之为“MVC 架构”,而不是真正的设计模式
维基百科是这样定义的。
模型-视图-控制器(Model-view-controller,MVC)是一种通常用于开发用户界面的架构模式,它将应用分成三个相互连接的部分。这样做是为了将信息的内部表示与信息呈现给用户并被用户接受的方式分开。MVC 设计模式将这些主要组件解耦,允许高效的代码重用和并行开发。 (
https://en.wikipedia.org/wiki/Model-view-controller
)
我最喜欢的关于 MVC 的描述来自 Connelly Barnes,他说,
理解 MVC 的一个简单方法:模型是数据,视图是屏幕上的窗口,控制器是两者之间的粘合剂。 (
http://wiki.c2.com/?ModelViewController
)
概念
使用这种模式,您可以将用户界面逻辑与业务逻辑分离开来,并以一种可以有效重用的方式分离主要组件。这种方法促进了并行开发。
从定义中可以明显看出,模式由这些主要组件组成:模型、视图和控制器。控制器放置在视图和模型之间,使得它们只能通过控制器相互通信。该模型将数据显示机制与数据操作机制分开。图 26-1 显示了 MVC 模式。
图 26-1
典型的 MVC 架构
需要记住的要点
这些是对该模式中关键组件的简要描述。
-
视图表示最终输出。它还可以接受用户输入。它是表示层,你可以把它想象成一个图形用户界面(GUI)。你可以用各种技术来设计它。例如,在. NET 应用中,您可以使用 HTML、CSS、WPF 等等,而对于 Java 应用,您可以使用 AWT、Swing、JSF、JavaFX 等等。
-
模型管理数据和业务逻辑,它充当应用的实际大脑。它管理数据和业务逻辑。它知道如何存储、管理或操作数据,并处理来自控制器的请求。但是这个组件与视图组件是分离的。一个典型的例子是数据库、文件系统或类似的存储。它可以用 Oracle、SQL Server、DB2、Hadoop、MySQL 等等来设计。
-
控制器是中介。它接受来自视图组件的用户输入,并将请求传递给模型。当它从模型得到响应时,它将数据传递给视图。可以用 C# 设计。NET、ASP.NET、VB.NET、核心 Java、JSP、Servlets、PHP、Ruby、Python 等等。
您可能会注意到不同应用中的不同实现。这里有一些例子。
-
您可以有多个视图。
-
视图可以将运行时值(例如,使用 JavaScript)传递给控制器。
-
您的控制器可以验证用户的输入。
-
您的控制器可以通过多种方式接收输入。例如,它可以通过 URL 从 web 请求中获取输入,或者通过单击表单上的 Submit 按钮传递输入。
-
在某些应用中,您可能会注意到模型可以更新视图组件。
简而言之,您需要使用这个模式来支持您自己的需求。图 26-2 、 26-3 和 26-4 显示了 MVC 架构的已知变体。
变体 1
图 26-2 为变型 1。
图 26-2
典型的 MVC 框架
变体 2
图 26-3 为变型 2。
图 26-3
一个多视图的 MVC 框架
变体 3
图 26-4 为变型 3。
图 26-4
用观察者模式/基于事件的机制实现的 MVC 模式
对 MVC 最好的描述之一来自于 wiki。c2。com ( http://wiki.c2.com/?ModelViewController
),上面写着,“我们需要智能模型、轻薄控制器、哑视图。”
真实世界的例子
考虑我们的模板方法模式的真实例子。但这一次,让我们换个角度来解读。我说在餐厅里,根据顾客的输入,一个厨师调整口味,做出最后一道菜。但是你知道顾客不会直接向厨师下订单。顾客看到菜单卡(视图)后,可能会咨询服务员,然后下订单。服务员将订单交给厨师,厨师从餐厅的厨房(类似于仓库或计算机数据库)收集所需的材料。准备好后,服务员把盘子端到顾客的桌子上。所以,你可以考虑一个服务员作为控制者,厨房里的厨师作为模型,食物准备材料作为数据。
计算机世界的例子
许多 web 编程框架使用 MVC 框架的概念。典型的例子包括 Django、Ruby on Rails、ASP.NET 等等。一个典型的 ASP.NET MVC 项目可以有如下图所示的视图 26-5 。
图 26-5
一个典型的 ASP.NET MVC 项目的解决方案浏览器视图
Points to Note
不同的技术遵循不同的结构,所以你不需要如图 26-5 所示的严格命名约定的文件夹结构。
履行
为了简单和符合我们的理论,我还将即将到来的实现分成三个主要部分:模型、视图和控制器。一旦注意到解决方案资源管理器视图,您就可以确定为完成此任务而创建的独立文件夹。以下是一些要点。
-
IModel, IView,
和IController
是三个接口,分别由具体的类EmployeeModel, ConsoleView,
和EmployeeController,
实现。看到这些名称,您可以假设它们是我们 MVC 架构的模型、视图和控制器层的代表。 -
在这个应用中,要求非常简单。一些员工需要在申请表上注册。最初,该应用有三个不同的注册员工:Amit、Jon 和 Sam。这些员工的 ID 是 E1、E2 和 E3。所以,你看到下面这个构造函数:
public EmployeeModel() { // Adding 3 employees at the beginning. enrolledEmployees = new List<Employee>(); enrolledEmployees.Add(new Employee("Amit", "E1")); enrolledEmployees.Add(new Employee("John", "E2")); enrolledEmployees.Add(new Employee("Sam", "E3")); }
-
在任何时间点,您都应该能够在系统中看到注册的员工。在客户端代码中,您调用控制器对象上的
DisplayEnrolledEmployees()
,如下所示:
controller.DisplayEnrolledEmployees();
然后,控制器将调用传递给视图层,如下所示:
view.ShowEnrolledEmployees(enrolledEmployees);
您会看到视图接口的具体实现者(ConsoleView.cs)对该方法的描述如下:
-
您可以在注册员工列表中添加新员工或删除员工。为此使用了
AddEmployeeToModel(Employee employee)
和RemoveEmployeeFromModel(string employeeIdToRemove)
方法。让我们看看RemoveEmployeeFromModel(...)
的方法签名。要删除一个雇员,您需要提供雇员 ID(它只不过是一个字符串)。但是如果没有找到雇员 ID,应用将忽略这个删除请求。 -
在 Employee 类中添加了一个简单的检查,以确保不会在应用中重复添加具有相同 ID 的雇员。
public void ShowEnrolledEmployees (List<Employee> enrolledEmployees)
{
Console.WriteLine("\n ***This is a console view of currently enrolled employees.*** ");
foreach (Employee emp in enrolledEmployees)
{
Console.WriteLine(emp);
}
Console.WriteLine("---------------------");
}
现在来看一下实现。是的,它很大,但是当你在前面的要点和支持图的帮助下一部分一部分地分析它时,你应该不会在理解代码上遇到任何困难。也可以考虑一下评论,供自己即时参考。
Points to Note
通常,您希望将 MVC 与提供内置支持并执行大量基础工作的技术结合使用。例如,当你使用 ASP.NET(或类似的技术)来实现 MVC 模式时,因为你有很多内置的支持。在这些情况下,你需要学习新的术语。
在本书中,我使用控制台应用来实现设计模式。让我们在即将到来的实现中继续使用同样的方法,因为我们的重点只放在 MVC 架构上。
类图
图 26-6 为类图。
图 26-6
类图
解决方案资源管理器视图
图 26-7 显示了程序的高层结构。
图 26-7
解决方案资源管理器视图
演示 1
这是完整的演示。
模型文件夹中的内容
// Employee.cs
namespace MVCPattern.Model
{
// The key "data" in this application
public class Employee
{
private string empName;
private string empId;
public string GetEmpName()
{
return empName;
}
public string GetEmpId()
{
return empId;
}
public Employee(string empName, string empId)
{
this.empName = empName;
this.empId = empId;
}
public override string ToString()
{
return $"{empName} is enrolled with id : {empId}.";
}
}
}
// Model.cs
using System.Collections.Generic;
namespace MVCPattern.Model
{
public interface IModel
{
List<Employee> GetEnrolledEmployeeDetailsFromModel();
void AddEmployeeToModel(Employee employeee);
void RemoveEmployeeFromModel(string employeeId);
}
}
// EmployeeModel.cs
using System;
using System.Collections.Generic;
namespace MVCPattern.Model
{
public class EmployeeModel : IModel
{
List<Employee> enrolledEmployees;
public EmployeeModel()
{
// Adding 3 employees at the beginning.
enrolledEmployees = new List<Employee>();
enrolledEmployees.Add(new Employee("Amit", "E1"));
enrolledEmployees.Add(new Employee("John", "E2"));
enrolledEmployees.Add(new Employee("Sam", "E3"));
}
public List<Employee> GetEnrolledEmployeeDetailsFromModel()
{
return enrolledEmployees;
}
// Adding an employee to the model(registered employee list)
public void AddEmployeeToModel(Employee employee)
{
Console.WriteLine($"\nTrying to add an employee to the registered list.The employee name is {employee.GetEmpName()} and id is {employee.GetEmpId()}.");
if (!enrolledEmployees.Contains(employee))
{
enrolledEmployees.Add(employee);
Console.WriteLine(employee + " [added recently.]");
}
else
{
Console.WriteLine("This employee is already added in the registered list.So, ignoring the request of addition.");
}
}
// Removing an employee from model(registered employee list)
public void RemoveEmployeeFromModel(string employeeIdToRemove)
{
Console.WriteLine($"\nTrying to remove an employee from the registered list.The employee id is {employeeIdToRemove}.");
Employee emp = FindEmployeeWithId(employeeIdToRemove);
if (emp != null)
{
Console.WriteLine("Removing this employee.");
enrolledEmployees.Remove(emp);
}
else
{
Console.WriteLine($"At present, there is no employee with id {employeeIdToRemove}.Ignoring this request.");
}
}
Employee FindEmployeeWithId(string employeeIdToRemove)
{
Employee removeEmp = null;
foreach (Employee emp in enrolledEmployees)
{
if (emp.GetEmpId().Equals(employeeIdToRemove))
{
Console.WriteLine($" Employee Found.{emp.GetEmpName()} has id: { employeeIdToRemove}.");
removeEmp = emp;
}
}
return removeEmp;
}
}
}
视图文件夹中的内容
// View.cs
using MVCPattern.Model;
using System.Collections.Generic;
namespace MVCPattern.View
{
public interface IView
{
void ShowEnrolledEmployees(List<Employee> enrolledEmployees);
}
}
// ConsoleView.cs
using System;
using System.Collections.Generic;
using MVCPattern.Model;
namespace MVCPattern.View
{
public class ConsoleView : IView
{
public void ShowEnrolledEmployees(List<Employee> enrolledEmployees)
{
Console.WriteLine("\n ***This is a console view of currently enrolled employees.*** ");
foreach (Employee emp in enrolledEmployees)
{
Console.WriteLine(emp);
}
Console.WriteLine("---------------------");
}
}
}
控制器文件夹中的内容
// Controller.cs
using MVCPattern.Model;
namespace MVCPattern.Controller
{
interface IController
{
void DisplayEnrolledEmployees();
void AddEmployee(Employee employee);
void RemoveEmployee(string employeeId);
}
}
// EmployeeController.cs
using System.Collections.Generic;
using MVCPattern.Model;
using MVCPattern.View;
namespace MVCPattern.Controller
{
public class EmployeeController : IController
{
IModel model;
IView view;
public EmployeeController(IModel model, IView view)
{
this.model = model;
this.view = view;
}
public void DisplayEnrolledEmployees()
{
// Get data from Model
List<Employee> enrolledEmployees = model.GetEnrolledEmployeeDetailsFromModel();
// Connect to View
view.ShowEnrolledEmployees(enrolledEmployees);
}
// Sending a request to model to add an employee to the list.
public void AddEmployee(Employee employee)
{
model.AddEmployeeToModel(employee);
}
// Sending a request to model to remove an employee from the list.
public void RemoveEmployee(string employeeId)
{
model.RemoveEmployeeFromModel(employeeId);
}
}
}
客户代码
// Program.cs
using MVCPattern.Controller;
using MVCPattern.Model;
using MVCPattern.View;
using System;
namespace MVCPattern
{
class Client
{
static void Main(string[] args)
{
Console.WriteLine("***MVC architecture Demo***\n");
// Model
IModel model = new EmployeeModel();
// View
IView view = new ConsoleView();
// Controller
IController controller = new EmployeeController(model, view);
controller.DisplayEnrolledEmployees();
// Add an employee
Employee empToAdd = new Employee("Kevin", "E4");
controller.AddEmployee(empToAdd);
// Printing the current details
controller.DisplayEnrolledEmployees();
// Remove an existing employee using the employee id.
controller.RemoveEmployee("E2");
// Printing the current details
controller.DisplayEnrolledEmployees();
/* Cannot remove an employee who does not belong to the list.*/
controller.RemoveEmployee("E5");
// Printing the current details
controller.DisplayEnrolledEmployees();
// Avoiding a duplicate entry
controller.AddEmployee(empToAdd);
// Printing the current details
controller.DisplayEnrolledEmployees();
/* This segment is added to discuss a question in "Q&A Session" and initially commented out. */
// view = new MobileDeviceView();
// controller = new EmployeeController(model, view);
// controller.DisplayEnrolledEmployees();
Console.ReadKey();
}
}
}
输出
这是输出。
***MVC architecture Demo***
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
---------------------
Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
Kevin is enrolled with id : E4\. [added recently.]
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
Trying to remove an employee from the registered list.The employee id is E2.
Employee Found.John has id: E2.
Removing this employee.
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
Trying to remove an employee from the registered list.The employee id is E5.
At present, there is no employee with id E5.Ignoring this request.
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
This employee is already added in the registered list.So, ignoring the request of addition.
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
问答环节
假设你有一名程序员、一名数据库管理员和一名图形设计师。你能预测他们在 MVC 架构中的角色吗?
图形设计师设计视图层,DBA 创建模型,程序员制作智能控制器。
使用 MVC 设计模式的主要优势是什么?
一些重要的优点如下。
-
高内聚和低耦合是 MVC 的好处。您可能已经注意到,在这种模式中,模型和视图之间的紧密耦合很容易消除。因此,应用可以很容易地扩展和重用。
-
该模式支持并行开发。
-
您还可以容纳多个运行时视图。
26.3 与 MVC 模式相关的挑战是什么?
这里有一些挑战。
-
它需要高度熟练的人员。
-
对于微小的应用来说,可能不太适合。
-
开发人员可能需要熟悉多种语言、平台和技术。
-
多工件一致性是一个大问题,因为您将整个项目分成三个主要部分。
26.4 你能在这个实现中提供多个视图吗?
当然可以。让我们在应用中添加一个名为 MobileDeviceView 的新的更短的视图。让我们将这个类添加到视图文件夹中,如下所示。
using System;
using System.Collections.Generic;
using MVCPattern.Model;
namespace MVCPattern.View
{
public class MobileDeviceView:IView
{
public void ShowEnrolledEmployees(List<Employee> enrolledEmployees)
{
Console.WriteLine("\n +++This is a mobile device view of currently enrolled employees.+++ ");
foreach (Employee emp in enrolledEmployees)
{
Console.WriteLine(emp.GetEmpId() + "\t" + emp.GetEmpName());
}
Console.WriteLine("+++++++++++++++++++++");
}
}
}
一旦添加了这个类,修改后的解决方案资源管理器视图应该类似于图 26-8 。
图 26-8
修改的解决方案资源管理器视图
现在,在客户端代码的末尾添加以下代码段(请参考注释以供参考)。
/* This segment is added to discuss a question in "Q&A Session and was
initially commented out.Now I’m uncommenting the following three lines of code."
*/
view = new MobileDeviceView();
controller = new EmployeeController(model, view);
controller.DisplayEnrolledEmployees();
现在,如果您运行应用,您会看到修改后的输出。
修改输出
下面是修改后的输出。输出的最后一部分显示了新变化的效果。更改以粗体显示。
***MVC architecture Demo***
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
---------------------
Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
Kevin is enrolled with id : E4\. [added recently.]
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
Trying to remove an employee from the registered list.The employee id is E2.
Employee Found.John has id: E2.
Removing this employee.
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
Trying to remove an employee from the registered list.The employee id is E5.
At present, there is no employee with id E5.Ignoring this request.
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
This employee is already added in the registered list.So, ignoring the request of addition.
***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
+++This is a mobile device view of currently enrolled employees
.+++
E1 Amit
E3 Sam
E4 Kevin
+++++++++++++++++++++
二十七、异步编程中的模式
你会在异步编程中看到许多有趣的模式,这很艰难,很有挑战性,但也很有趣。它通常被称为异步。整体概念不是一天进化出来的,这需要时间,而在 C# 5.0 中,你得到了async
和await
关键词让它变得更简单。在此之前,程序员用各种技术实现了这个概念。每种技术都有其优点和缺点。本章的目标是向你介绍不同的异步编程模式。
概观
首先,让我们讨论异步编程。简单地说,你在你的应用中取一个代码段,并在一个单独的线程上运行它。关键优势是什么?简单的答案是,您可以释放原始线程,让它继续执行剩余的任务,而在一个单独的线程中,您可以执行不同的任务。这种机制帮助您开发现代应用;例如,当您实现一个高度响应的用户界面时,这些概念非常有用。
Points to Remember
大体上,您会注意到异步编程中的三种不同模式,如下所示:
-
IAsyncResult 模式 : 或者,它被称为异步编程模型(APM)。在这个模式中,在核心处,您可以看到支持异步行为的
IAsyncResult
接口。在同步模型中,如果您有一个名为 XXX()的同步方法,在异步版本中,您会看到对应同步方法的BeginXXX()
和EndXXX()
方法。比如在同步版本中,如果你有Read()
方法支持读操作;在异步编程中,通常有BeginRead()
和EndRead()
方法来异步支持相应的读操作。使用这个概念,从演示 5 到演示 7,您会看到BeginInvoke
和EndInvoke
方法。但是对于即将到来的和新的开发,不推荐使用这种模式。 -
标签:Console,--,void,class,WriteLine,new,设计模式,public From: https://www.cnblogs.com/apachecn/p/18352668