设计模式深度解析与实战指南
1. 访客模式与设计思考
访客模式(VISITOR)允许在不改变层次结构类的情况下为其定义新操作。在访客模式中,通常被访问的层次结构本身行为较少,行为设计的责任主要由访客承担,避免了责任分散的问题。不过,访客模式并非必不可少,很多时候存在更稳健的替代设计。
访客模式的机制包括为访客定义接口,并在层次结构中添加
Accept()
方法,访客会调用这些方法。
Accept()
方法通过双重分派机制将调用返回到访客,从而执行适用于层次结构中特定对象类型的
Visit()
方法。
访客开发者需要了解被访问层次结构设计中的一些细节,尤其要注意被访问对象模型中可能出现的循环。这一难点使得一些开发者选择避开访客模式,转而采用替代方案。是否使用访客模式通常应是团队决策,取决于开发方法和应用的具体情况。
替代访客模式的方案有:
-
直接扩展原层次结构
:若与层次结构开发者沟通良好,或所在团队不强调代码所有权,可直接在原层次结构中添加所需行为。
-
直接遍历结构
:让操作机器或流程结构的类直接遍历该结构。若需了解复合对象子元素的类型,可使用
is
运算符,或构建
IsLeaf()
和
IsComposite()
等布尔函数。
-
创建平行层次结构
:若要添加的行为与现有行为差异较大,可创建平行层次结构。例如,将机器的规划行为放在单独的层次结构中。
2. 从书本中汲取更多价值
学习设计模式后,可通过以下方式进一步提升能力:
-
重新挑战
:重新做之前遇到的挑战题目,只有在认为自己有正确答案或完全卡住时再查看解决方案。通过这些挑战锻炼模式知识,增强在工作中应用模式的信心。
-
实践代码
:从相关网站下载代码,确保能在自己的系统上重复书中示例的结果。实践代码比仅在纸上研究示例更能增强信心。还可自行设置新挑战,如以新方式组合装饰器过滤器,或实现显示熟悉领域数据的数据适配器。
3. 理解经典设计模式
许多设计模式被融入到 .NET FCL 中,理解这些经典示例有助于掌握设计并与其他开发者交流。以下是一些经典设计模式在 C# 和 FCL 中的应用问题:
| 问题 | 解释 |
| — | — |
| GUI 控件如何使用观察者模式? | 当 GUI 控件状态发生变化时,会通知所有注册的观察者,观察者可以根据变化做出相应的响应。 |
| 菜单为何常使用命令模式? | 菜单中的每个选项可以看作一个命令,点击菜单选项相当于执行相应的命令,将请求封装在命令对象中,方便管理和扩展。 |
| 驱动为何是桥接模式的好例子?每个特定驱动是否是适配器模式的实例? | 驱动将抽象的操作(如设备控制)与具体的实现(如不同硬件的驱动程序)分离,符合桥接模式的思想。而适配器模式是将一个类的接口转换成客户希望的另一个接口,特定驱动不一定是适配器模式的实例,具体要看其是否进行了接口转换。 |
| C# 流采用装饰器模式意味着什么? | C# 流可以通过装饰器模式在不改变原有流对象的基础上,动态地添加新的功能,如加密、压缩等。 |
| 代理模式为何是 ASP.NET 设计的基础? | 在 ASP.NET 中,代理模式可以用于实现远程对象的访问、缓存等功能,将客户端和实际服务分离,提高系统的可维护性和性能。 |
| 如果排序是模板方法模式的好例子,算法的哪个步骤未指定? | 排序算法的比较步骤通常未指定,不同的排序需求可以通过实现不同的比较方法来满足,这体现了模板方法模式中让子类填充缺失步骤的思想。 |
4. 将模式融入代码
学习设计模式的主要目的是成为更优秀的开发者。在日常工作的代码库中应用模式有两种方式:
-
新增代码时应用
:在编写新代码时,考虑使用合适的设计模式来解决问题。
-
重构代码时应用
:若部分代码复杂且难以维护,可通过重构代码并应用设计模式来改进。在进行重构项目前,要确保有明确的客户需求,并为重构的代码创建自动化测试套件。
以下是一些可能应用设计模式的场景:
-
状态模式
:若代码处理系统状态或应用用户状态的部分复杂,可应用状态模式进行改进。
-
策略模式
:若代码将策略选择与策略执行结合在一起,可使用策略模式优化。
-
解释器模式
:若客户或分析师提供的流程图转换的代码难以理解,可应用解释器模式,让流程图的每个节点成为解释器层次结构中类的实例,实现从流程图到代码的直接转换。
-
组合模式
:若代码中的复合对象不允许子元素也是复合对象,可使用组合模式增强代码。
-
中介者模式
:若对象模型中出现关系完整性错误,可应用中介者模式集中管理对象关系,防止错误发生。
-
工厂方法模式
:若代码中客户端使用服务信息来决定实例化哪个类,可应用工厂方法模式改进和简化代码。
5. 持续学习的方向
学习设计模式后,持续学习至关重要。建议开发者每周安排一定时间用于职业发展,可在工作时间之外阅读书籍和杂志,或编写与感兴趣主题相关的软件。
在深入学习模式之前,要确保掌握基础知识,可阅读相关书籍。若想进一步学习模式,有很多选择。若想了解应用设计模式的实际示例,可阅读相关书籍;若想丰富模式词汇,可学习并发模式相关内容。
6. 接口与适配器模式
6.1 接口相关知识
抽象类和接口有相似之处,但也存在一些区别:
| 比较项 | 抽象类 | 接口 |
| — | — | — |
| 继承与实现 | 类最多只能继承一个抽象类 | 类可以实现任意数量的接口 |
| 方法实现 | 可以有非抽象方法 | 所有方法都是抽象的 |
| 变量使用 | 可以声明和使用变量 | 不能使用变量 |
| 访问修饰符 | 方法可以有多种访问修饰符 | 成员隐式具有公共访问权限,且声明时不能包含任何修饰符 |
| 构造函数 | 可以定义构造函数 | 不能定义构造函数 |
以下是一些接口相关的代码示例:
using System.Data;
namespace DataLayer
{
public interface IBorrower
{
object BorrowReader(IDataReader reader);
}
}
using System;
using System.Data;
using DataLayer;
public class ShowBorrowing2 : IBorrower
{
public static void Main()
{
string sel = "SELECT * FROM ROCKET";
DataServices2.LendReader2(
sel, new ShowBorrowing2());
}
public object BorrowReader(IDataReader reader)
{
while (reader.Read())
{
Console.WriteLine(reader["Name"]);
}
return null;
}
}
6.2 适配器模式
适配器模式可将一个类的接口转换成客户希望的另一个接口。例如,
OozinozRocket
类将
PhysicalRocket
类适配成满足
IRocketSim
接口的需求。
public class OozinozRocket : PhysicalRocket, IRocketSim
{
private double _time;
public OozinozRocket(
double burnArea, double burnRate,
double fuelMass, double totalMass)
: base (burnArea, burnRate, fuelMass, totalMass)
{
}
public double GetMass()
{
return GetMass(_time);
}
public double Thrust()
{
return GetThrust(_time);
}
public void SetSimTime (double time)
{
_time = time;
}
}
对象适配器设计(如
OozinozSkyrocket
类)可能比类适配器方法更脆弱,原因如下:
-
接口不明确
:
OozinozSkyrocket
类未明确指定其提供的接口,
Skyrocket
类的变化可能在运行时产生问题,但在编译时无法检测到。
-
依赖不稳定
:
OozinozSkyrocket
类依赖于
_time
变量,但无法保证该变量始终被声明为受保护的,也无法确定其在
Skyrocket
类中的含义。
7. 外观模式与组合模式
7.1 外观模式
外观模式为开发者隔离工具包的复杂性,具有以下优点:
-
易于理解和使用
:相比集成开发环境(IDE),外观模式通常更简单易懂。
-
创建成本低
:创建外观模式比创建 IDE 更容易。
-
无代码负担
:外观模式不生成代码,避免了拥有不需要或不理解的代码。
IDE 的优点包括:
-
功能丰富
:IDE 及其向导可以提供简单直接的使用路径,同时允许使用其他功能和进行定制。
-
适应性强
:在猜测或确定子系统的“简洁”使用方式方面,IDE 通常比外观模式限制更少。
-
环境支持
:IDE 可以探索开发环境,如查找和测试数据库连接,并帮助生成在该环境中运行的代码。
7.2 组合模式
设计组合类来维护组件对象集合,可使组合对象包含叶子对象或其他组合对象,这种设计更灵活。例如,可将用户的系统权限定义为特定权限或其他权限组的集合,也可将工作流程定义为流程步骤和其他流程的集合。
在计算机器数量时,
Machine
类的
GetMachineCount()
方法返回 1,
MachineComposite
类的
GetMachineCount()
方法则递归计算其所有组件的机器数量。
public override int GetMachineCount()
{
return 1;
}
public override int GetMachineCount()
{
int count = 0;
foreach (MachineComponent mc in _components)
{
count += mc.GetMachineCount();
}
return count;
}
组合模式中机器复合行为的定义如下表所示:
| 方法 | 类 | 定义 |
| — | — | — |
|
GetMachineCount()
|
MachineComposite
| 返回组件中每个组件计数的总和 |
|
GetMachineCount()
|
Machine
| 返回 1 |
|
IsCompletelyUp()
|
MachineComposite
| 若所有组件都“完全正常”,则返回
true
|
|
IsCompletelyUp()
|
Machine
| 若该机器正常,则返回
true
|
|
StopAll()
|
MachineComposite
| 通知所有组件“停止所有操作” |
|
StopAll()
|
Machine
| 停止该机器 |
|
GetOwners()
|
MachineComposite
| 创建一个集合,添加所有组件的所有者,然后返回该集合 |
|
GetOwners()
|
Machine
| 返回该机器的所有者 |
|
GetMaterial()
|
MachineComposite
| 返回所有组件上的材料集合 |
|
GetMaterial()
|
Machine
| 返回该机器上的材料 |
8. 桥接模式与责任引入
8.1 桥接模式
若要使用通用接口控制各种机器,可应用适配器模式,为每个控制器创建适配器类,将标准接口调用转换为现有控制器支持的调用。
例如,机器的关机操作可通过以下代码实现:
public void Shutdown()
{
StopProcess();
ConveyOut();
StopMachine();
}
桥接模式将抽象操作(如机器管理)与具体实现(如驱动程序)分离,通过抽象驱动对象实现两者的解耦。
8.2 责任引入
在设计类时,需考虑类的合理性。一个好的类应具备以下特点:
-
数据结构清晰
:包含数据成员(常量和字段)、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、析构函数和静态构造函数)和嵌套类型。
-
属性关联合理
:类的属性之间应有合理的关联。
-
目的明确
:类应有明确的、一致的目的。
-
命名恰当
:类名应能反映其含义,包括属性和行为。
-
行为支持完整
:类必须支持其定义的所有行为,以及超类和实现接口中的所有方法。
-
继承关系合理
:类与超类之间应有合理的继承关系。
-
方法命名清晰
:类中每个方法的名称应能清晰说明该方法的作用。
调用操作的效果可能取决于接收对象的状态和类。例如,
MachineManager2
类的
stopMachine()
方法的效果取决于该对象所使用的驱动程序。当设计中存在多态时,调用操作的效果可能部分或完全取决于接收对象的类。
9. 单例模式与观察者模式
9.1 单例模式
为防止其他开发者实例化类,可创建一个具有私有访问权限的构造函数。若不创建构造函数,C# 会提供一个默认构造函数;若创建了其他非私有构造函数,其他开发者仍可实例化该类。
单例模式懒初始化的原因有:
-
信息不足
:在静态初始化时,可能没有足够的信息来实例化单例。例如,工厂单例可能需要等待实际工厂的机器建立通信通道。
-
资源管理
:对于需要资源(如数据库连接)的单例,若应用程序在特定会话中可能不需要该单例,可选择懒初始化。
在多线程应用中,访问单例数据时需小心。例如,为避免多个线程同时调用
recordWipMove()
方法时产生混淆,可使用锁机制。
public void RecordWipMove()
{
lock (_classLock)
{
_wipMoves++;
}
}
判断一个类是否为单例,需考虑其用途和实现方式。例如,
OurBiggestRocket
和
TopSalesAssociate
类的命名不恰当,应使用类属性来表示“最大”等属性;
Math
和
System.Console
类是实用工具类,不是单例;
TextWriter
类不是单例;若公司只有一台打印机,
PrintSpooler
类可能是单例;在某些场景下,
PrinterManager
类可作为单例来查找打印机地址。
9.2 观察者模式
观察者模式允许对象注册以接收其他对象状态变化的通知。例如,一个有趣的对象的
Wiggle()
方法可调用委托,该委托会调用一个好奇对象的
React()
方法。
在某些应用中,可引入新的事件来实现状态变化的传播。例如,在一个弹道应用中,通过引入新的
Change
事件,使滑块的移动引发函数的更新和事件的调用,从而实现事件在应用中的正确流动。
graph LR
A[TrackBar] -->|Scroll| B[ShowBallistics]
B -->|Update Functions| C[TpeakFunction]
C -->|Invoke Change Event| D[AlertPlotPanel]
C -->|Invoke Change Event| E[ValueTextBox]
10. 中介者模式与代理模式
10.1 中介者模式
中介者模式用于处理组件之间的交互,将组件的交互逻辑集中在中介者类中。例如,在一个移动桶的应用中,
MoveATubMediator
类处理组件之间的事件,而
MoveATub
类负责组件的构建和布局。
internal void SelectChanged(object sender, EventArgs e)
{
//...
_gui.AssignButton().Enabled =
_gui.MachineList().SelectedItems.Count > 0 &&
_gui.TubList().SelectedItems.Count > 0;
}
中介者模式的优点是将组件交互逻辑分离,使代码更易于维护和扩展;缺点是可能导致中介者类出现“特征嫉妒”问题,即中介者类对 GUI 类的依赖过强。
10.2 代理模式
代理模式为其他对象提供一种代理以控制对这个对象的访问。例如,数据读取器代理可用于限制对特定记录中特定字段的访问,将请求转移到其他数据源,编辑数据库中的数据,或监控、记录数据访问等。
using System;
using System.Data;
using DataLayer;
public class LimitingReader : DataReaderProxy
{
public LimitingReader(IDataReader subject) :
base (subject)
{
}
public override object this [string name]
{
get
{
if (String.Compare(name, "apogee", true) == 0)
{
return 0;
}
else
{
return base [name];
}
}
}
}
使用代理模式时需注意,要了解每个被代理方法的行为,避免出现漏洞。例如,上述
LimitingReader
类虽然限制了对
apogee
字段的访问,但用户仍可通过数字索引获取该信息。
11. 职责链模式与享元模式
11.1 职责链模式
职责链模式将请求沿着对象链传递,直到有对象处理该请求。在查找机器的负责工程师时使用职责链模式可能存在以下缺点:
-
父对象信息不明
:未明确机器如何知道其父对象,实际中可能难以确保父对象不为空。
-
工程师信息不足
:系统对于当前在工厂且可用的工程师信息掌握不足,不清楚这种职责分配的实时性要求。
-
可能陷入无限循环
:查找父对象的过程可能会进入无限循环。
在设计中,可让每个可视化项对象实现
Responsible
属性,当请求该属性时,对象可直接响应或转发给父对象。
// MachineComponent对象的Responsible属性实现
public Engineer getResponsible()
{
if (responsible != null)
{
return responsible;
}
if (parent != null)
{
return parent.getResponsible();
}
return null;
}
// Tool对象的Responsible属性实现
public Engineer Responsible
{
get
{
return _toolCart.Responsible;
}
}
// ToolCart对象的Responsible属性实现
public Engineer Responsible
{
get
{
return _responsible;
}
}
职责链模式也可应用于非复合对象,例如按标准轮换的值班工程师链,若主值班工程师未在规定时间内响应生产支持呼叫,通知系统将呼叫链中的下一位工程师;或者在用户输入事件日期等信息时,由一系列解析器依次尝试解码用户输入的文本。
11.2 享元模式
享元模式用于共享对象的不可变部分,以节省资源。字符串的不可变性是享元模式的一个例子。
-
支持不可变性的观点
:在实际应用中,字符串经常在多个客户端之间共享。如果字符串是可变的,客户端可能会无意中相互影响,导致出现许多缺陷。例如,一个返回客户姓名的方法通常会保留对该姓名的引用。如果客户端将字符串转换为大写用于哈希表,若字符串是可变的,客户对象的姓名也会改变。在 C# 中,可以生成字符串的大写版本,但这必须是一个新对象,而不是原始字符串的修改版本。字符串的不可变性使其在多个客户端之间共享时更加安全。
-
反对不可变性的观点
:字符串的不可变性虽然能保护我们免受某些错误的影响,但代价很高。首先,开发者无法更改字符串,无论这种需求是否合理。其次,为语言添加特殊规则会使语言更难学习和使用。C# 比同样强大的 Smalltalk 语言难学得多。最后,没有一种计算机语言能完全避免错误。如果能快速学习语言,就有更多时间学习如何设置和使用测试框架。
对于物质类,可以将其不可变的方面(如名称、符号和原子量)提取到一个单独的类中,实现享元模式。
// 原Substance类的不可变部分提取到Chemical类
public class Substance2
{
private double _grams;
private Chemical _chemical;
public double AtomicWeight
{
get
{
return _chemical.AtomicWeight;
}
}
public double Grams
{
get
{
return _grams;
}
}
public double Moles
{
get
{
return _grams / AtomicWeight;
}
}
}
public class Chemical
{
private string _symbol;
private string _name;
private double _atomicWeight;
public string Symbol
{
get
{
return _symbol;
}
}
public string Name
{
get
{
return _name;
}
}
public double AtomicWeight
{
get
{
return _atomicWeight;
}
}
public double GetMoles(double grams)
{
return grams / _atomicWeight;
}
}
为了防止开发者自行实例化
Chemical
类,可以将
Chemical
和
ChemicalFactory
类放在同一个程序集中,并为
Chemical
类的构造函数设置内部访问权限。也可以使用嵌套类来确保只有
ChemicalFactory2
类可以实例化新的享元对象。
using System;
using System.Collections;
namespace Chemicals
{
public class ChemicalFactory2
{
private static Hashtable _chemicals =
new Hashtable();
private class ChemicalImpl : IChemical
{
private String _name;
private String _symbol;
private double _atomicWeight;
internal ChemicalImpl (
String name,
String symbol,
double atomicWeight)
{
_name = name;
_symbol = symbol;
_atomicWeight = atomicWeight;
}
public string Name
{
get { return _name; }
}
public string Symbol
{
get { return _symbol; }
}
public double AtomicWeight
{
get { return _atomicWeight; }
}
}
static ChemicalFactory2 ()
{
_chemicals["carbon"] =
new ChemicalImpl("Carbon", "C", 12);
_chemicals["sulfur"] =
new ChemicalImpl("Sulfur", "S", 32);
_chemicals["saltpeter"] =
new ChemicalImpl("Saltpeter", "KN03", 101);
//...
}
public static IChemical GetChemical(String name)
{
return (IChemical) _chemicals[name.ToLower()];
}
}
}
12. 构建相关模式
12.1 构造函数规则
构造函数有一些特殊规则:
-
默认构造函数
:如果没有为类提供构造函数,C# 会提供一个默认构造函数。
-
调用方式
:必须使用
new
关键字来调用构造函数。
-
命名规则
:构造函数的名称必须与类名相同。
-
返回类型
:构造函数的返回类型是类的实例,而普通方法的返回类型可以是任意类型。
-
关键字限制
:构造函数不能重写超类的构造函数,因此
new
、
virtual
、
override
、
abstract
和
sealed
关键字不适用于构造函数声明。
-
调用其他构造函数
:构造函数可以使用
:this()
和
:base()
调用其他构造函数。
例如,以下代码会编译失败:
using System;
public class Fuse
{
private string _name;
public Fuse(string name) { this._name = name; }
}
public class QuickFuse : Fuse
{
}
编译器会报错,因为
QuickFuse
类的默认构造函数会尝试调用
Fuse
类的无参构造函数,但
Fuse
类中没有无参构造函数。
12.2 建造者模式
建造者模式用于将对象的构建过程与其表示分离。在解析预订信息时,可以通过修改正则表达式使解析器更灵活。例如,让
Regex
对象接受逗号后的多个空格或任何类型的空白字符。
// 接受逗号后的多个空格
new Regex(“, *”)
// 接受任何类型的空白字符
new Regex(@",\s*")
也可以考虑让旅行社以 XML 格式发送预订信息,使用
XmlTextReader
类读取信息,或者使用
SoapFormatter
对象接受的格式。
UnforgivingBuilder
类的
Build()
方法在遇到无效属性时会抛出异常,否则返回一个有效的预订对象。
public override Reservation Build()
{
if (_date == DateTime.MinValue)
{
throw new BuilderException("Valid date not found");
}
if (_city == null)
{
throw new BuilderException("Valid city not found");
}
if (_headcount < MINHEAD)
{
throw new BuilderException(
"Minimum headcount is " + MINHEAD);
}
if (_dollarsPerHead * _headcount < MINTOTAL)
{
throw new BuilderException(
"Minimum total cost is " + MINTOTAL);
}
return new Reservation(
_date,
_headcount,
_city,
_dollarsPerHead,
_hasSite);
}
对于缺失某些属性值的预订信息,可以根据不同情况进行处理:
- 如果没有指定人数和每人费用,将人数设置为最小值,每人费用设置为总费用最小值除以人数。
- 如果没有指定人数但有每人费用,将人数设置为至少是最小值且能产生足够的费用。
- 如果有指定人数但没有每人费用,将每人费用设置为能产生总费用最小值的金额。
public override Reservation Build()
{
bool noHeadcount = (_headcount == 0);
bool noDollarsPerHead = (_dollarsPerHead == 0M);
//
if (noHeadcount && noDollarsPerHead)
{
_headcount = MINHEAD;
_dollarsPerHead = MINTOTAL / _headcount;
}
else if (noHeadcount)
{
_headcount =
(int) Math.Ceiling(
(double)(MINTOTAL / _dollarsPerHead));
_headcount = Math.Max(_headcount, MINHEAD);
}
else if (noDollarsPerHead)
{
_dollarsPerHead = MINTOTAL / _headcount;
}
//
Check();
return new Reservation(
_date,
_headcount,
_city,
_dollarsPerHead,
_hasSite);
}
protected void Check()
{
if (_date == DateTime.MinValue)
{
throw new BuilderException("Valid date not found");
}
if (_city == null)
{
throw new BuilderException("Valid city not found");
}
if (_headcount < MINHEAD)
{
throw new BuilderException(
"Minimum headcount is " + MINHEAD);
}
if (_dollarsPerHead * _headcount < MINTOTAL)
{
throw new BuilderException(
"Minimum total cost is " + MINTOTAL);
}
}
13. 工厂方法模式与抽象工厂模式
13.1 工厂方法模式
工厂方法模式将对象的创建延迟到子类。例如,可以创建一个
Set
类,使其支持
foreach
循环。
using System;
using System.Collections;
namespace Utilities
{
public class Set
{
private Hashtable h = new Hashtable();
public IEnumerator GetEnumerator()
{
return h.Keys.GetEnumerator();
}
public void Add(Object o)
{
h[o] = null;
}
}
}
using System;
using Utilities;
public class ShowSet
{
public static void Main()
{
Set set = new Set();
set.Add("Shooter");
set.Add("Orbit");
set.Add("Shooter");
set.Add("Biggie");
foreach (string s in set)
{
Console.WriteLine(s);
}
}
}
常见的创建新对象的方法有
ToString()
和
Clone()
。例如,
DateTime.Now.ToString()
创建一个新的
String
对象,
Clone()
方法通常返回接收对象的浅拷贝。
在信用检查场景中,两个信用检查类实现
ICreditCheck
接口,工厂类提供一个返回
ICreditCheck
对象的方法。客户端调用
CreateCreditCheck()
方法时,不知道接收到的对象的确切类。
// 信用检查工厂类
public static ICreditCheck CreateCreditCheck()
{
if (IsAgencyUp())
{
return new CreditCheckOnline();
}
else
{
return new CreditCheckOffline();
}
}
在机器规划场景中,
Machine
类的
GetPlanner()
方法利用抽象的
CreatePlanner()
方法,实现了模板方法模式。
public MachinePlanner GetPlanner()
{
if (_planner == null)
{
_planner = CreatePlanner();
}
return _planner;
}
13.2 抽象工厂模式
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。例如,可以创建一个
BetaUI
类,继承自
UI
类,重写一些创建按钮的方法。
public class BetaUI : UI
{
public BetaUI ()
{
Font f = Font;
_font = new Font(f, f.Style ^ FontStyle.Italic);
}
public override Button CreateButtonOk()
{
Button b = base.CreateButtonOk();
b.Image = GetImage("cherry-large.gif");
return b;
}
public override Button CreateButtonCancel()
{
Button b = base.CreateButtonCancel();
b.Image = GetImage("cherry-large-down.gif");
return b;
}
}
为了使设计更具弹性,可以在接口中指定预期的创建方法和标准 GUI 属性。在信用检查场景中,
Credit.Canada
包提供了一系列具体类,实现了
Credit
包中的接口和抽象类。
using System;
using Credit;
namespace Credit.Canada
{
public class CheckFactoryCanada : CreditCheckFactory
{
public override IBillingCheck CreateBillingCheck()
{
return new BillingCheckCanada();
}
public override ICreditCheck CreateCreditCheck()
{
if (IsAgencyUp())
{
return new CreditCheckCanadaOnline();
}
else
{
return new CreditCheckOffline();
}
}
public override IShippingCheck CreateShippingCheck()
{
return new ShippingCheckCanada();
}
}
}
14. 原型模式与备忘录模式
14.1 原型模式
原型模式用于创建对象的副本。
MemberwiseClone()
方法创建一个新对象,该对象具有与原始对象相同的类和属性类型,并且所有字段值也相同。但对于引用类型的字段,只是复制了引用,而不是对象本身,因此
MemberwiseClone()
方法创建的是浅拷贝。
using System;
using System.Windows.Forms;
namespace UserInterface
{
public class OzGroupBox : GroupBox
{
public OzGroupBox Copy()
{
return (OzGroupBox) this.MemberwiseClone();
}
public OzGroupBox Copy2()
{
OzGroupBox gb = new OzGroupBox();
gb.BackColor = BackColor;
gb.Dock = Dock;
gb.Font = Font;
gb.ForeColor = ForeColor;
return gb;
}
}
}
在克隆
MachineSimulator
对象时,如果仅使用
MemberwiseClone()
方法,可能会导致多个克隆对象共享同一个
Location
对象,从而出现问题。
14.2 备忘录模式
备忘录模式用于保存和恢复对象的状态。在工厂模型中,可以使用栈来保存和恢复对象的状态。
public void Pop()
{
if (_mementos.Count > 1)
{
_mementos.Pop(); // pop the current state
if (RebuildEvent != null) RebuildEvent();
}
}
当需要将备忘录保存到持久存储中时,可能是因为需要在系统崩溃后恢复对象状态、用户退出系统后继续工作,或者在另一台计算机上重建对象。
internal void Restore(object sender, System.EventArgs e)
{
OpenFileDialog d = new OpenFileDialog();
if (d.ShowDialog() == DialogResult.OK)
{
using (FileStream fs =
File.Open(d.FileName, FileMode.Open))
{
IList list = (IList)
(new SoapFormatter().Deserialize(fs));
_factoryModel.Push(list);
}
}
}
将对象保存为 SOAP 格式的字符串可能会违反封装原则,因为这会暴露对象的数据,任何人都可以使用文本编辑器更改对象的状态。为了确保数据完整性,可以限制对数据的访问或对数据进行加密。
15. 操作与扩展相关模式
15.1 操作相关模式
职责链模式将操作分布在对象链上,每个方法直接实现操作的服务或将调用转发给链中的下一个对象。
C# 方法修饰符包括
new
、
public
、
protected
、
internal
、
private
、
static
、
virtual
、
sealed
、
override
、
abstract
和
extern
,但它们的组合使用受到一些规则的限制。
委托相关的知识点如下:
| 项目 | 描述 |
| — | — |
| 委托声明 | 定义一个派生自
System.Delegate
的类 |
| 委托实例 | 封装一个或多个方法 |
|
+=
运算符右侧的表达式 | 创建一个委托实例 |
|
Click
| 是一个类型为
System.EventHandler
的实例变量 |
|
System.EventHandler
委托 | 指定方法的参数和返回类型,但没有方法名 |
|
LoadImage()
方法 | 必须具有与
System.EventHandler
相同的参数和返回类型 |
在模板方法模式中,子类可以重写父类算法中的某些步骤。例如,在排序场景中,
Array.Sort()
方法可以使用不同的比较器实现不同的排序策略。
using System;
using System.Collections;
using Fireworks;
public class ShowComparator
{
public static void Main()
{
Rocket r1 = new Rocket(,
"Mach-it", 1.1, 22.95m, 1000, 70);
Rocket r2 = new Rocket(
"Pocket", 0.3, 4.95m, 150, 20);
Rocket r3 = new Rocket(
"Sock-it", 0.8, 11.95m, 320, 25);
Rocket r4 = new Rocket(
"Sprocket", 1.5, 22.95m, 270, 40);
Rocket[] rockets = new Rocket[] { r1, r2, r3, r4 };
Array.Sort(rockets, new ApogeeCompare());
foreach (Rocket r in rockets)
{
Console.WriteLine(r);
}
}
private class ApogeeCompare : IComparer
{
public int Compare(Object o1, Object o2)
{
Rocket r1 = (Rocket)o1;
Rocket r2 = (Rocket)o2;
return r1.Apogee.CompareTo(r2.Apogee);
}
}
}
15.2 扩展相关模式
在面向对象编程中,虽然数学上圆是椭圆的特殊情况,但在 OO 编程中,椭圆具有一些圆不具备的行为,因此如果这些行为对程序很重要,将圆作为椭圆的子类可能违反里氏替换原则(LSP)。
在代码中,
tub.Location.IsUp()
表达式可能会导致编程错误,因为
tub
对象的
Location
属性可能为
null
或为
Robot
对象。为了避免这些问题,相关代码应放在
Tub
类中。
以下是一些使用设计模式扩展行为的示例:
| 示例 | 应用的模式 |
| — | — |
| 烟花模拟设计师建立一个接口,定义对象参与模拟所需的行为 | 适配器模式 |
| 工具包允许在运行时组合可执行对象 | 解释器模式 |
| 超类有一个方法,需要子类填充缺失的步骤 | 模板方法模式 |
| 对象通过接受封装在对象中的方法并在适当的时候调用该方法来扩展其行为 | 命令模式 |
| 代码生成器插入行为,使在另一台机器上执行的对象看起来像是本地对象 | 代理模式 |
| 设计允许注册回调,当对象发生变化时触发 | 观察者模式 |
| 设计允许定义依赖于明确定义接口的抽象操作,并允许添加满足该接口的新驱动程序 | 桥接模式 |
16. 装饰器模式、迭代器模式与访客模式
16.1 装饰器模式
装饰器模式用于动态地向对象添加功能。例如,可以创建一个
RandomCaseFilter
类,将输入的字符随机转换为大写或小写。
using System;
namespace Filters
{
public class RandomCaseFilter : OozinozFilter
{
protected Random ran = new Random();
public RandomCaseFilter(ISimpleWriter writer) :
base (writer)
{
}
public override void Write(char c)
{
_writer.Write(ran.NextDouble() > .5
? Char.ToLower(c)
: Char.ToUpper(c));
}
}
}
using System;
using Filters;
public class ShowRandom
{
public static void Main()
{
ISimpleWriter w = new RandomCaseFilter(
new ConsoleWriter());
w.Write(
"buy two packs now and get a " +
"zippie pocket rocket -- free!");
w.WriteLine();
w.Close();
}
}
ConsoleWriter
类实现
ISimpleWriter
接口,用于将字符和字符串输出到控制台。
using System;
namespace Filters
{
public class ConsoleWriter : ISimpleWriter
{
public void Write(char c)
{
Console.Write(c);
}
public void Write(string s)
{
Console.Write(s);
}
public void WriteLine()
{
Console.WriteLine();
}
public void Close()
{
}
}
}
16.2 迭代器模式
迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。在多线程环境下,迭代器的使用需要注意线程安全问题。例如,
DisplayUpMachines()
方法和
NewMachineComesUp()
方法在不同线程中操作列表,可能会导致输出结果不符合预期。
迭代器是工厂方法模式的一个经典示例,客户端知道何时创建迭代器,但接收类知道要实例化哪个类。
public override bool MoveNext()
{
if (!_visited.Contains(_head))
{
_visited.Add(_head);
if (ReturnInterior)
{
_current = _head;
return true;
}
}
return SubiteratorNext();
}
16.3 访客模式
访客模式中,
Accept()
方法调用
MachineVisitor
对象的
Visit()
方法。
Machine
类的
Accept()
方法会查找具有
visit(:Machine)
签名的
Visit()
方法,而
MachineComposite
类的
Accept()
方法会查找具有
visit(:MachineComposite)
签名的方法。
using System;
using Machines;
using Utilities;
public class RakeVisitor : IMachineVisitor
{
protected Set _leaves;
public Set GetLeaves(MachineComponent mc)
{
_leaves = new Set();
mc.Accept(this);
return _leaves;
}
public void Visit(Machine m)
{
_leaves.Add(m);
}
public void Visit(MachineComposite mc)
{
foreach (MachineComponent child in mc.Children)
{
child.Accept(this);
}
}
}
为了处理访问节点的集合,可以在所有
Accept()
和
Visit()
方法中添加一个
Set
参数。
访客模式的替代方案包括:
-
直接扩展原层次结构
:若与层次结构开发者沟通良好,或所在团队不强调代码所有权,可直接在原层次结构中添加所需行为。
-
直接遍历结构
:让操作机器或流程结构的类直接遍历该结构。若需了解复合对象子元素的类型,可使用
is
运算符,或构建
IsLeaf()
和
IsComposite()
等布尔函数。
-
创建平行层次结构
:若要添加的行为与现有行为差异较大,可创建平行层次结构。例如,将机器的规划行为放在单独的层次结构中。
通过学习和应用这些设计模式,可以提高代码的可维护性、可扩展性和灵活性,从而成为更优秀的开发者。在实际开发中,应根据具体需求选择合适的设计模式,并不断学习和实践,以提升自己的编程能力。
超级会员免费看
5337

被折叠的 条评论
为什么被折叠?



