一、单例模式
这一章涵盖了单例模式。
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
懒惰类的 Visual Studio 截图
在这个例子中,我使用了下面的版本。
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 模式是一个不错的选择。***