设计模式常见问题解答 -第一部分(培训)






4.46/5 (177投票s)
设计模式常见问题解答 -
已更新单例模式的解释。
设计模式 FAQ
- 什么是设计模式?
- 你能解释一下工厂模式吗?
- 你能解释一下抽象工厂模式吗?
- 你能解释一下建造者模式吗?
- 你能解释一下原型模式吗?
- 你能解释一下原型模式中的浅拷贝和深拷贝吗?
- 你能解释一下单例模式吗?
- 你能解释一下命令模式吗?
- 带项目的设计模式
引言
这里有一个关于设计模式的简短问答形式的 FAQ。在本节中,我们将介绍工厂、抽象工厂、建造者、原型、浅拷贝和深拷贝原型、单例和命令模式。
您可以在以下链接中阅读我其他的同系列设计模式 FAQ 部分
- 第二部分 设计模式 FAQ -- 解释器模式、迭代器模式、中介者模式、备忘录模式和观察者模式
- 第三部分 设计模式 FAQ -- 状态模式、策略模式、访问者模式、适配器模式和享元模式
- 第四部分 设计模式 FAQ -- 桥梁模式、组合模式、装饰器模式、外观模式、责任链 (COR)、代理模式和模板模式
什么是设计模式?
设计模式是在给定上下文中,针对反复出现的问题的已记录的、经过考验和验证的解决方案。所以,基本上,你有一个问题上下文和针对它的建议解决方案。设计模式从软件开发的早期阶段就以某种形式存在。比如说,如果你想实现一个排序算法,首先想到的是冒泡排序。所以问题是排序,解决方案是冒泡排序。设计模式也是如此。
设计模式的三种主要类别是什么?
模式有三种基本分类 - 创建型、结构型和行为型模式。
Creational Patterns
- 抽象工厂:创建一个或多个类的实例
- 建造者:将对象构建与其表示分离
- 工厂方法:创建派生类的一个实例
- 原型:一个完全初始化的实例,可被复制或克隆
- 单例:一个类,其中只能存在一个实例
注意:记住创建型模式的最佳方法是记住 ABFPS(Abraham Became First President of States)。
Structural Patterns
- 适配器:匹配不同类的接口
- 桥梁:将对象的抽象与其实现分离
- 组合:由简单对象和组合对象组成的树形结构
- 装饰器:动态地为对象添加职责
- 外观:代表整个子系统的单个类
- 享元:用于高效共享的细粒度实例
- 代理:代表另一个对象的对象
- 中介者:定义类之间简化的通信
- 备忘录:捕获并恢复对象的内部状态
- 解释器:在程序中包含语言元素的方法
- 迭代器:顺序访问集合的元素
- 责任链:一种在对象链之间传递请求的方法
- 命令:将命令请求封装为对象
- 状态:当对象的状态改变时,改变其行为
- 策略:将算法封装在类中
- 观察者:一种向多个类通知更改的方法
- 模板方法:将算法的确切步骤推迟到子类
- 访问者:在不更改类的情况下定义新操作
你能解释一下工厂模式吗?
工厂模式是创建型模式的一种。从“工厂”这个名字本身就可以看出,它是用来制造和创造某物的。在软件架构领域,工厂模式旨在集中创建对象。下面是一个客户端的代码片段,它有不同类型的发票。这些发票是根据客户端指定的发票类型创建的。下面的代码有两个问题:
-
首先,我们的客户端散布着大量的 '
new
' 关键字。换句话说,客户端加载了很多对象创建活动,这可能会使客户端逻辑非常复杂。 -
第二个问题是客户端需要了解所有类型的发票。所以,如果我们添加一个名为 '
InvoiceWithFooter
' 的新发票类类型,我们需要在客户端引用新类,并且也需要重新编译客户端。
以这些问题为基础,我们现在来看看工厂模式如何帮助我们解决它们。下图“工厂模式”显示了两个具体类“ClsInvoiceWithHeader
”和“ClsInvoiceWithOutHeader
”。
第一个问题是这些类直接与客户端接触,导致客户端代码中散布着大量的 'new
' 关键字。通过引入一个新类 'ClsFactoryInvoice
' 来处理所有对象的创建,这个问题得到了解决。
第二个问题是客户端代码同时了解这两个具体类,即 'ClsInvoiceWithHeader
' 和 'ClsInvoiceWithOutHeader
'。这会导致在添加新的发票类型时需要重新编译客户端代码。例如,如果我们添加 'ClsInvoiceWithFooter
',则需要相应地更改和重新编译客户端代码。为了解决这个问题,我们引入了一个通用接口 'IInvoice
'。 'ClsInvoiceWithHeader
' 和 'ClsInvoiceWithOutHeader
' 这两个具体类都继承并实现了 'IInvoice
' 接口。
客户端只引用 'IInvoice
' 接口,从而消除了客户端与具体类( 'ClsInvoiceWithHeader
' 和 'ClsInvoiceWithOutHeader
')之间的所有连接。因此,如果我们现在添加新的具体发票类,就不需要更改客户端的任何内容。
一句话概括,对象的创建由 'ClsFactoryInvoice
' 负责,而客户端与具体类的解耦则由 'IInvoice
' 接口负责。
以下是如何在 C# 中实际实现工厂模式的代码片段。为了避免重新编译客户端,我们引入了发票接口 'IInvoice
'。 'ClsInvoiceWithOutHeaders
' 和 'ClsInvoiceWithHeader
' 这两个具体类都继承并实现了 'IInvoice
' 接口。
我们还引入了一个额外的类 'ClsFactoryInvoice
',其中有一个函数 'getInvoice()
',它根据 'intInvoiceType
' 值生成这两种发票的对象。简而言之,我们将对象创建的逻辑集中在 'ClsFactoryInvoice
' 中。客户端调用 'getInvoice
' 函数来生成发票类。需要注意的一个重要之处是,客户端只引用 'IInvoice
' 类型,并且工厂类 'ClsFactoryInvoice
' 也返回相同类型的引用。这有助于客户端完全脱离具体类,因此当我们添加新类和发票类型时,就不需要重新编译客户端了。
你能解释一下抽象工厂模式吗?
抽象工厂是对基本工厂模式的扩展。抽象工厂帮助我们将相似的工厂模式类统一到一个统一的接口中。所以,基本上,所有通用的工厂模式现在都继承自一个通用的抽象工厂类,它将它们统一到一个公共类中。与工厂模式相关的所有其他内容与上一问题中讨论的相同。
工厂类帮助我们集中创建类和类型。抽象工厂帮助我们在相关的工厂模式之间带来一致性,从而为客户端提供更简化的接口。
现在我们知道了基本概念,让我们尝试理解抽象工厂模式实际实现的细节。正如前面所说的,我们有工厂模式类(factory1
和 factory2
),它们通过共同的 abstract
工厂(AbstractFactory
接口)通过继承连接起来。Factory
类位于具体类之上,而具体类又派生自通用接口。例如,在图“抽象工厂的实现”中,'product1
' 和 'product2
' 这两个具体类都继承自一个接口,即 'common
'。想要使用具体类的客户端只会与抽象工厂以及具体类继承自的通用接口进行交互。
现在让我们看看如何在实际代码中实现抽象工厂。我们有一个场景,其中我们通过各自集中的工厂类 'ClsFactoryButton
' 和 'ClsFactoryText
' 来处理 UI 创建活动。这两个类都继承自通用接口 'InterfaceRender
'。 'ClsFactoryButton
' 和 'ClsFactoryText
' 这两个工厂都继承自通用工厂 'ClsAbstractFactory
'。图“AbstractFactory
的示例”显示了这些类的排列方式以及相应的客户端代码。关于客户端代码的一个重要注意事项是,它不与具体类交互。对于对象创建,它使用抽象工厂 (ClsAbstractFactory
),对于调用具体类的实现,它通过接口 'InterfaceRender
' 调用方法。因此,'ClsAbstractFactory
' 类为 'ClsFactoryButton
' 和 'ClsFactoryText
' 这两个工厂提供了通用接口。
我们只需快速浏览一下抽象工厂的示例代码。下面的代码片段“抽象工厂和工厂代码片段”显示了工厂模式类如何继承自抽象工厂。
图“具体类的通用接口”显示了具体类如何继承自通用接口 'InterFaceRender
',该接口强制所有具体类实现 'render
' 方法。
最后是使用接口 'InterfaceRender
' 和抽象工厂 'ClsAbstractFactory
' 来调用和创建对象的客户端代码。关于代码的一个重要之处是,它与具体类完全隔离。因此,具体类的任何更改,例如添加和删除具体类,都不需要更改客户端。
你能解释一下建造者模式吗?
建造者属于创建型模式类别。建造者模式帮助我们将一个复杂对象的构建与其表示分离,以便相同的构建过程可以创建不同的表示。当对象的构建非常复杂时,建造者模式很有用。主要目标是将对象的构建与其表示分离。如果我们能够分离构建和表示,我们就可以从相同的构建中获得多种表示。
为了理解我们所说的构建和表示是什么意思,让我们以下面的“茶的准备”序列为例。
从图“茶的准备”中可以看出,通过相同的准备步骤,我们可以得到三种茶的表示(即无糖茶、加糖/奶茶和无奶茶)。
现在,让我们以软件世界中的一个实际示例来看看建造者如何分离复杂的创建及其表示。假设我们有一个应用程序,需要以“PDF”或“EXCEL”格式显示相同的报告。图“请求报告”显示了实现此目标的步骤系列。根据报告类型,创建一个新报告,设置报告类型,设置报告的页眉和页脚,最后,我们会得到报告以供显示。
现在让我们从不同的角度看待问题,如图“不同视图”所示。在“请求报告”中定义的相同流程现在按表示和通用构建进行分析。两种报告类型的构建过程是相同的,但它们会产生不同的表示。
我们将以相同的报告问题为例,并尝试使用建造者模式来解决它。在实现建造者模式时,主要有三个部分:
Builder
:Builder
负责定义各个部分的构建过程。Builder 包含那些用于初始化和配置产品的各个过程。Director
:Director
从 builder 获取这些单独的过程,并定义构建产品的顺序。Product
:Product
是由 builder 和 director 协调产生的最终对象。
首先,让我们看看 builder 类的层次结构。我们有一个名为 'ReportBuilder
' 的 abstract
类,自定义 builder 如 'ReportPDF
' builder 和 'ReportEXCEL
' builder 将从它构建。
图“实际代码中的 Builder 类”显示了这些类的方法。要生成报告,我们首先需要创建一个新报告,设置报告类型(EXCEL 或 PDF),设置报告页眉,设置报告页脚,最后获取报告。我们定义了两个自定义 builder,一个用于“PDF”(ReportPDF
),另一个用于“EXCEL”(ReportExcel
)。这两个自定义 builder 会根据报告类型定义自己的过程。
现在让我们了解 director 的工作方式。类 'clsDirector
' 接受 builder,并按顺序调用各个方法进程。所以 director 就像一个驱动程序,它获取所有单独的过程并将它们按顺序调用以生成最终产品,在本例中是报告。图“Director 在起作用”显示了 'MakeReport
' 方法如何调用各个过程来生成 PDF 或 EXCEL 报告产品。
建造者中的第三个组件是产品,在本例中就是报告类。
现在让我们从顶层来看建造者项目。图“客户端、Builder、Director 和 Product”显示了它们如何协同工作以实现建造者模式。客户端创建 director 类的对象,并将适当的 builder 传递给它以初始化产品。根据 builder,产品被初始化/创建,并最终发送给客户端。
输出类似于此。我们可以看到两种报告类型显示了它们根据 builder 设置的页眉。
你能解释一下原型模式吗?
原型模式属于创建型模式部分。它提供了一种从现有对象实例创建新对象的方法。一句话来说,就是我们克隆现有对象及其数据。通过克隆,对克隆对象的任何更改都不会影响原始对象的值。如果你认为只需设置对象就可以得到克隆,那么你就错了。通过将一个对象设置为另一个对象,我们按引用 (BYREF
) 设置对象。所以改变新对象也会改变原始对象。为了更清楚地理解 BYREF
的基本原理,请看下面的图“BYREF
”。以下是下面代码的执行顺序:
- 第一步,我们创建了第一个对象,即
class1
的obj1
。 - 第二步,我们创建了第二个对象,即
class1
的obj2
。 - 第三步,我们设置旧对象的值,即
obj1
的值为“old value”。 - 第四步,我们将
obj1
设置为obj2
。 - 第五步,我们改变
obj2
的值。 - 现在,我们显示两个值,发现两个对象都具有新值。
以上示例的结论是,对象被设置为其他对象时是按引用 (BYREF
) 设置的。所以改变新对象的值也会改变旧对象的值。
有很多情况我们希望新复制的对象更改不会影响旧对象。答案就是原型模式。
让我们看看如何在 C# 中实现这一点。在下图“原型模式在起作用”中,我们有一个需要克隆的客户类 'ClsCustomer
'。在 C# 中,这可以通过使用 'MemberWiseClone
' 方法来实现。在 JAVA 中,我们使用 'Clone
' 方法来实现。在同一代码中,我们也显示了客户端代码。我们创建了客户类的两个对象 'obj1
' 和 'obj2
'。对 'obj2
' 的任何更改都不会影响 'obj1
',因为它是一个完整的克隆副本。
你能解释一下原型模式中的浅拷贝和深拷贝吗?
原型模式有两种克隆类型。一种是浅拷贝,你刚才在第一个问题中已经读过了。在浅拷贝中,只有该对象被克隆,该对象中包含的任何对象都不会被克隆。例如,考虑图“深拷贝在起作用”,我们有一个 customer
类,并且有一个聚合在 customer
类中的 address
类。'MemberWiseClone
' 只会克隆 customer
类 'ClsCustomer
',但不会克隆 'ClsAddress
' 类。所以我们也为 address
类添加了 'MemberWiseClone
' 函数。现在当我们调用 'getClone
' 函数时,我们调用父级的克隆函数以及子级的克隆函数,这会导致整个对象被克隆。当父对象及其包含的对象被克隆时,称为深拷贝;当仅克隆父对象时,称为浅拷贝。
你能解释一下单例模式吗?
Punch :- Create a single instance of object and provides access to
this single instance via a central point.
在项目中,有时我们希望只创建一个实例对象并供客户端共享。例如,假设我们有以下两个类,currency
和 country
。
这些类加载主数据,这些数据将在项目中反复引用。我们希望共享类的单个实例,以通过不重复访问数据库来获得性能优势。
public class Currency
{
List<string> oCurrencies = new List<string>();
public Currency()
{
oCurrencies.Add("INR");
oCurrencies.Add("USD");
oCurrencies.Add("NEP");
oCurrencies.Add("GBP");
}
public IEnumerable<string> getCurrencies()
{
return (IEnumerable<string>)oCurrencies;
}
}
public class Country
{
List<string> oCountries = new List<string>();
public Country()
{
oCountries.Add("India");
oCountries.Add("Nepal");
oCountries.Add("USA");
oCountries.Add("UK");
}
public IEnumerable<string> getCounties()
{
return (IEnumerable<string>) oCountries;
}
}
实现单例模式是一个四步过程。
步骤 1:创建一个带有私有构造函数的密封类
private
构造函数很重要,这样客户端就不能直接创建类的对象。如果您还记得要点,这个模式的主要目的是创建一个可以全局共享的单个对象实例,因此我们不希望将创建实例的控制权直接交给客户端。
public sealed class GlobalSingleton
{
private GlobalSingleton() { }
………..
……….
}
步骤 2:创建我们要创建单个实例的类的聚合对象(在本例中是 currency 和 country)。
public sealed class GlobalSingleton
{
// object which needs to be shared globally
public Currency Currencies = new Currency();
public Country Countries = new Country();
步骤 3:创建类本身的一个静态只读对象,并通过静态属性公开它,如下所示。
public sealed class GlobalSingleton
{
….
…
// use static variable to create a single instance of the object
static readonly GlobalSingleton INSTANCE = new GlobalSingleton();
public static GlobalSingleton Instance
{
get
{
return INSTANCE;
}
}
}
步骤 4:现在,您可以使用下面的客户端代码来使用单例对象。
GlobalSingleton oSingle = GlobalSingleton.Instance;
Country o = osingl1.Country;
以下是上面我们分块讨论的单例模式的完整代码。
public sealed class GlobalSingleton
{
// object which needs to be shared globally
public Currency Currencies = new Currency();
public Country Countries = new Country();
// use static variable to create a single instance of the object
static readonly GlobalSingleton INSTANCE = new GlobalSingleton();
/// This is a private constructor, meaning no outsides have access.
private GlobalSingleton()
{ }
public static GlobalSingleton Instance
{
get
{
return INSTANCE;
}
}
}
你能解释一下命令模式吗?
命令模式允许请求作为对象存在。好的,让我们来理解它的含义。考虑图“菜单和命令”,我们根据点击的菜单有不同的操作。因此,根据点击的菜单,我们传递了一个 string
,其中包含操作文本。根据操作 string
,我们将执行操作。代码的不好之处在于它有很多 'IF
' 条件,这使得编码更加晦涩难懂。
命令模式将上述操作转移到对象中。这些对象在执行时实际上会执行命令。
如前所述,每个命令都是一个对象。我们首先为每个操作准备单独的类,即 exit、open、file 和 print。所有上述操作都封装在类中,例如,exit 操作封装在 'clsExecuteExit
' 中,open 操作封装在 'clsExecuteOpen
' 中,print 操作封装在 'clsExecutePrint
' 中,依此类推。所有这些类都继承自通用接口 'IExecute
'。
使用所有操作类,我们现在可以创建调用者。调用者的主要工作是将操作与拥有该操作的类进行映射。
因此,我们将所有操作都添加到一个集合中,即 arraylist
。我们公开了一个名为 'getCommand
' 的方法,它接受一个 string
并返回 abstract
对象 'IExecute
'。客户端代码现在干净整洁。所有的 'IF
' 条件现在都移到了 'clsInvoker
' 类中。
如果您对设计模式完全陌生,或者您真的不想阅读整篇文章,请观看我们免费的 设计模式培训和面试问答 视频。
如需进一步阅读,请观看以下面试准备视频和分步视频系列。
- C# 设计模式分步教程
- C# 面试问答
- ASP.NET MVC 面试题及答案
- Angular 面试题及答案
- 逐步学习Azure。
- SQL Server 分步教程
- C# is vs As 关键字
- C# throw vs throw ex
- C# 并发 vs 并行
- C# 抽象类与接口
- C# 字符串是不可变的
带项目的设计模式
学习设计模式的最佳方式是通过一个项目。因此,本教程逐个模式地教你,但如果你想通过项目方法学习设计模式,那么 请点击此链接。
历史
- 2008 年 8 月 2 日:初始版本
- 2021 年 6 月 10 日:已更新单例模式的解释