23种设计模式详解
文章目录
- 23种设计模式详解
- 1 设计模式概述
- 2 设计模式详解
- 2.1 简单工厂模式(Simple Factory Pattern)
- 2.2 抽象工厂模式(Abstract Factory Pattern)
- 2.3 单例模式(Singleton Pattern)
- 2.4 建造者模式(Builder Pattern)
- 2.5 原型模式(Prototype Pattern)
- 2.6 适配器模式(Adapter Pattern)
- 2.7 桥接模式(Bridge Pattern)
- 2.8 组合模式(Composite Pattern)
- 2.9 装饰器模式(Decorator Pattern)
- 2.10 外观模式(Facade Pattern)
- 2.11 享元模式(Flyweight Pattern)
- 2.12 代理模式(Proxy Pattern)
- 2.13 责任链模式(Chain of Responsibility Pattern)
- 2.14 命令模式(Command Pattern)
- 2.15 解释器模式(Interpreter Pattern)
- 2.16 迭代器模式(Iterator Pattern)
- 2.17 中介者模式(Mediator Pattern)
- 2.18 备忘录模式(Memento Pattern)
- 2.19 观察者模式(Observer Pattern)
- 2.20 状态模式(State Pattern)
- 2.21 策略模式(Strategy Pattern)
- 2.22 模板方法模式(Template Method Pattern)
- 2.23 访问者模式(Visitor Pattern)
设计模式(Design Patterns)是软件工程中的一种解决方案,它提供了一套经过验证的代码设计和架构方案,用于解决软件设计中反复出现的问题。设计模式不特定于某种编程语言,而是普遍适用于各种面向对象的编程语言。设计模式主要分为三大类:创建型模式、结构型模式和行为型模式。
1 设计模式概述
1.1 创建型模式(Creational Patterns)
创建型模式关注对象的创建过程,旨在使对象的创建与使用分离,避免系统与具体类之间的紧密耦合。
- 工厂方法模式(Factory Method):定义一个创建对象的接口,但由子类决定实例化哪个类。
- 抽象工厂模式(Abstract Factory):提供一个创建一系列相关或依赖对象的接口,而无需指定它们的具体类。
- 单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。
- 建造者模式(Builder):将复杂对象的创建与其表示分离,使得同样的构建过程可以创建不同的表示。
- 原型模式(Prototype):使用对象的原型实例创建新对象,即通过克隆来创建对象。
2.2 结构型模式(Structural Patterns)
结构型模式关注类和对象的组合,旨在通过组合接口和实现来实现新的功能。
- 适配器模式(Adapter):将一个类的接口转换成客户希望的另一个接口,使原本不兼容的类可以一起工作。
- 桥接模式(Bridge):将抽象部分与实现部分分离,使它们可以独立变化。
- 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构,使客户可以一致地处理单个对象和组合对象。
- 装饰者模式(Decorator):动态地给一个对象添加额外的职责,扩展对象的功能。
- 外观模式(Facade):为子系统中的一组接口提供一个一致的界面,使子系统更容易使用。
- 享元模式(Flyweight):通过共享尽可能多的细节以支持大量细粒度对象的高效共享。
- 代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
3.3 行为型模式(Behavioral Patterns)
行为型模式关注对象之间的通信和职责分配,旨在提高对象之间的通信效率和灵活性。
- 责任链模式(Chain of Responsibility):为请求创建一个接收者对象的链,沿着这条链传递请求,直到有一个对象处理它为止。
- 命令模式(Command):将请求封装成对象,以便使用不同的请求、队列或日志参数化其他对象。
- 解释器模式(Interpreter):为语言创建解释器,定义一种文法表示,并建立一个解释器来处理该文法。
- 迭代器模式(Iterator):提供一种方法顺序访问聚合对象中的各个元素,而不暴露其内部表示。
- 中介者模式(Mediator):用一个中介对象来封装一系列对象交互,使对象之间不需要显式地相互引用,从而使它们可以松散耦合。
- 备忘录模式(Memento):在不破坏封装的前提下,捕获对象的内部状态,并在以后恢复该状态。
- 观察者模式(Observer):定义对象间的一对多依赖关系,当一个对象改变状态时,所有依赖对象都会被通知并自动更新。
- 状态模式(State):允许对象在内部状态改变时改变其行为,看起来对象好像修改了它的类。
- 策略模式(Strategy):定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。
- 模板方法模式(Template Method):定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。
- 访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
2 设计模式详解
2.1 简单工厂模式(Simple Factory Pattern)
2.1.1 定义
简单工厂模式,也称为静态工厂方法模式,并不属于GoF(Gang of Four)设计模式之一。它提供了一个创建对象的简单接口,通过传递不同的参数来决定创建哪种类的实例。
2.1.2 结构
简单工厂模式包含以下角色:
- 工厂类(Factory): 提供一个静态方法,根据传入的参数来创建不同类型的对象。
- 产品(Product): 定义创建对象的接口或抽象类。
- 具体产品(ConcreteProduct): 实现产品接口的具体类。
UML 类图
+-------------------+
| Factory |
+-------------------+
| + CreateProduct() |
+-------------------+
|
v
+-------------------+
| Product |
+-------------------+
/ \
/ \
+--------+ +---------+
|ProductA| |ProductB |
+--------+ +---------+
2.1.3 示例代码
// 产品接口
public interface IProduct
{
void Display();
}
// 具体产品A
public class ProductA : IProduct
{
public void Display()
{
Console.WriteLine("Product A");
}
}
// 具体产品B
public class ProductB : IProduct
{
public void Display()
{
Console.WriteLine("Product B");
}
}
// 简单工厂
public class SimpleFactory
{
public static IProduct CreateProduct(string type)
{
if (type == "A")
{
return new ProductA();
}
else if (type == "B")
{
return new ProductB();
}
else
{
throw new ArgumentException("Invalid type");
}
}
}
// 客户端代码
class Program
{
static void Main(string[] args)
{
IProduct product = SimpleFactory.CreateProduct("A");
product.Display();
}
}
2.1.4 特点
- 优点:
- 简单易懂,适合创建少量对象的情况。
- 统一了对象的创建过程,便于管理和修改。
- 缺点:
- 不符合开放-关闭原则:每增加一种新类型的产品,都需要修改工厂类。
- 工厂类过于集中,扩展性较差,随着产品种类的增加,工厂类会变得臃肿。
- 只适用于创建单一产品族的情况。
2.1.5 适用场景
- 当一个系统中需要创建的对象较少,并且不需要对创建逻辑进行高度的扩展或修改时。
- 当客户端只需指定创建对象的类型,而不关心具体的创建过程时。
2.1.6 工厂方法模式(Factory Method Pattern)
2.1.6.1 定义
工厂方法模式定义了一个用于创建对象的接口,但由子类决定实例化哪一个类。通过这种方式,工厂方法将对象的创建推迟到子类。
2.1.6.2 结构
工厂方法模式包含以下角色:
- 抽象产品(Product): 定义工厂方法所创建对象的接口或抽象类。
- 具体产品(ConcreteProduct): 实现
Product
接口或继承Product
抽象类的具体类。 - 抽象创建者(Creator): 声明工厂方法,该方法返回一个
Product
类型的对象。也可以包含一些默认实现。 - 具体创建者(ConcreteCreator): 实现工厂方法,返回具体产品的实例。
UML 类图
+---------------------+
| Creator |
+---------------------+
| + FactoryMethod() |
+---------------------+
|
|
v
+---------------------+
| ConcreteCreator |
+---------------------+
| + FactoryMethod() |
+---------------------+
|
|
v
+---------------------+
| ConcreteProduct |
+---------------------+
| Implements Product |
+---------------------+
2.1.6.3 示例代码
假设我们要创建不同类型的日志记录器(如控制台日志记录器和文件日志记录器),可以使用工厂方法模式。
// 产品接口
public interface ILogger
{
void Log(string message);
}
// 具体产品1:控制台日志记录器
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine("Console Logger: " + message);
}
}
// 具体产品2:文件日志记录器
public class FileLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine("File Logger: " + message);
}
}
// 抽象创建者
public abstract class LoggerFactory
{
public abstract ILogger CreateLogger();
public void WriteLog(string message)
{
var logger = CreateLogger();
logger.Log(message);
}
}
// 具体创建者1
public class ConsoleLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger()
{
return new ConsoleLogger();
}
}
// 具体创建者2
public class FileLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger()
{
return new FileLogger();
}
}
// 客户端代码
class Program
{
static void Main(string[] args)
{
LoggerFactory loggerFactory = new ConsoleLoggerFactory();
loggerFactory.WriteLog("This is a console log.");
loggerFactory = new FileLoggerFactory();
loggerFactory.WriteLog("This is a file log.");
}
}
2.1.6.4 特点
- 优点:
- 遵循单一职责原则:将对象的创建和使用分离。
- 遵循开放-关闭原则:通过子类来扩展新的产品类型,而无需修改现有代码。
- 缺点:
- 随着产品种类的增加,会引入多个具体创建者,导致代码量增加。
- 每新增一种产品,需要新增一个具体的创建者类,增加了系统的复杂性。
2.1.6.5 适用场景
- 当系统需要将创建对象的过程与其使用过程分开时。
- 当一个类不应决定它所创建的对象的具体类型时,可以使用工厂方法模式。
2.2 抽象工厂模式(Abstract Factory Pattern)
2.2.1 定义
抽象工厂模式提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们的具体类。它适用于需要创建多个产品族的情况。
2.2.2 结构
抽象工厂模式包含以下角色:
- 抽象工厂(AbstractFactory): 声明创建一系列相关对象的接口。
- 具体工厂(ConcreteFactory): 实现抽象工厂中的创建方法,生成具体产品。
- 抽象产品(AbstractProduct): 为每种产品声明接口。
- 具体产品(ConcreteProduct): 定义由具体工厂创建的产品对象,必须实现抽象产品接口。
UML 类图
+-------------------------+
| AbstractFactory |
+-------------------------+
| + CreateProductA() |
| + CreateProductB() |
+-------------------------+
/|\
|
+-------------------------+
| ConcreteFactory1 |
+-------------------------+
| + CreateProductA() |
| + CreateProductB() |
+-------------------------+
|
+-------------------------+
| ConcreteFactory2 |
+-------------------------+
| + CreateProductA() |
| + CreateProductB() |
+-------------------------+
/|\
|
+----------------------------+ +----------------------------+
| AbstractProductA | | AbstractProductB |
+----------------------------+ +----------------------------+
| + OperationA() | | + OperationB() |
+----------------------------+ +----------------------------+
/|\ /|\
| |
+----------------------------+ +----------------------------+
| ProductA1 | | ProductB1 |
+----------------------------+ +----------------------------+
| + OperationA() | | + OperationB() |
+----------------------------+ +----------------------------+
|
+----------------------------+
| ProductA2 |
+----------------------------+
| + OperationA() |
+----------------------------+
2.2.3 示例代码
假设我们要创建两种风格的家具:现代风格和维多利亚风格,每种风格包括椅子和沙发。我们可以使用抽象工厂模式来实现。
// 抽象产品A:椅子
public interface IChair
{
void SitOn();
}
// 抽象产品B:沙发
public interface ISofa
{
void LieOn();
}
// 具体产品A1:现代椅子
public class ModernChair : IChair
{
public void SitOn()
{
Console.WriteLine("Sitting on a modern chair.");
}
}
// 具体产品B1:现代沙发
public class ModernSofa : ISofa
{
public void LieOn()
{
Console.WriteLine("Lying on a modern sofa.");
}
}
// 具体产品A2:维多利亚椅子
public class VictorianChair : IChair
{
public void SitOn()
{
Console.WriteLine("Sitting on a Victorian chair.");
}
}
// 具体产品B2:维多利亚沙发
public class VictorianSofa : ISofa
{
public void LieOn()
{
Console.WriteLine("Lying on a Victorian sofa.");
}
}
// 抽象工厂
public interface IFurnitureFactory
{
IChair CreateChair();
ISofa CreateSofa();
}
// 具体工厂1:现代家具工厂
public class ModernFurnitureFactory : IFurnitureFactory
{
public IChair CreateChair()
{
return new ModernChair();
}
public ISofa CreateSofa()
{
return new ModernSofa();
}
}
// 具体工厂2:维多利亚家具工厂
public class VictorianFurnitureFactory : IFurnitureFactory
{
public IChair CreateChair()
{
return new VictorianChair();
}
public ISofa CreateSofa()
{
return new VictorianSofa();
}
}
// 客户端代码
class Program
{
static void Main(string[] args)
{
IFurnitureFactory modernFactory = new ModernFurnitureFactory();
IChair modernChair = modernFactory.CreateChair();
ISofa modernSofa = modernFactory.CreateSofa();
modernChair.SitOn();
modernSofa.LieOn();
IFurnitureFactory victorianFactory = new VictorianFurnitureFactory();
IChair victorianChair = victorianFactory.CreateChair();
ISofa victorianSofa = victorianFactory.CreateSofa();
victorianChair.SitOn();
victorianSofa.LieOn();
}
}
2.2.4 特点
- 优点:
- 遵循开放-关闭原则:可以通过添加新的具体工厂来扩展产品家族,而不修改现有代码。
- 保证产品族的一致性:所有由同一具体工厂创建的产品都是相互兼容的。
- 缺点:
- 复杂度较高:随着产品族和具体产品的增加,类的数量会显著增加。
- 扩展困难:增加新的产品族时,必须修改抽象工厂及其子类,这违反了开放-关闭原则。
2.2.5 适用场景
- 当系统要创建多个产品族的对象时。
- 当你需要确保一个产品族中的所有对象是兼容的时。
- 当产品族的创建逻辑较为复杂,需要封装起来时。
2.2.6 比较总结
- 工厂方法模式:
- 适用于创建单一产品的场景。
- 通过子类化具体创建者来扩展产品类型。
- 工厂方法模式更关注的是"如何创建"一个对象。
- 抽象工厂模式:
- 适用于创建多个相关产品的场景。
- 通过增加新的具体工厂来扩展产品族。
- 抽象工厂模式更关注的是"创建什么"(一系列相关的对象)。
在实际应用中,工厂方法模式较适合较简单的对象创建场景,而抽象工厂模式则适合更复杂的、多产品族的对象创建需求。
2.3 单例模式(Singleton Pattern)
2.3.1 定义
单例模式确保一个类只有一个实例,并提供一个访问该实例的全局访问点。它可以防止类被多次实例化,并且在某些情况下可以节省内存、确保一致性或控制资源的访问。
2.3.2 结构
单例模式的主要角色包括:
- 单例类(Singleton): 包含一个私有的静态变量来保存单例实例,并提供一个公共的静态方法来返回这个实例。
UML 类图
+----------------------+
| Singleton |
+----------------------+
| - instance: Singleton|
+----------------------+
| + getInstance(): |
| Singleton |
+----------------------+
2.3.3 示例代码
单例模式的实现有多种方式,以下是最常见的几种。
懒汉式(Lazy Initialization)
懒汉式实现中,实例在第一次调用 getInstance()
方法时才被创建。这种方式可以延迟实例的创建,节省资源,但在多线程环境下需要进行同步以保证线程安全。
public class Singleton
{
private static Singleton _instance;
// 构造函数设置为私有,防止通过new创建实例
private Singleton() { }
public static Singleton GetInstance()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
线程安全的懒汉式
为了保证线程安全,可以在 getInstance
方法上添加 lock
关键字,但这样可能会降低性能。
public class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton GetInstance()
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
return _instance;
}
}
双重检查锁定(Double-Check Locking)
这种方法在检查实例是否已经存在时只加一次锁,提高了性能。这是线程安全且高效的实现方式。
public class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton GetInstance()
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
饿汉式(Eager Initialization)
饿汉式在类加载时就创建实例,因此不存在线程安全问题,但如果实例比较大且未使用时,会浪费资源。
public class Singleton
{
private static readonly Singleton _instance = new Singleton();
// 构造函数设置为私有,防止通过new创建实例
private Singleton() { }
public static Singleton GetInstance()
{
return _instance;
}
}
静态内部类(Static Inner Class)
使用静态内部类的方式可以实现延迟加载和线程安全。静态内部类的实例只会在第一次被引用时初始化,因此可以实现懒加载效果。
public class Singleton
{
private Singleton() { }
private static class SingletonHolder
{
internal static readonly Singleton _instance = new Singleton();
}
public static Singleton GetInstance()
{
return SingletonHolder._instance;
}
}
枚举(Enum)
使用枚举来实现单例是最简单和安全的方式,因为枚举实例化是线程安全的,并且只会被实例化一次。这种方式不仅实现了单例,而且还能防止反序列化和反射攻击。
public enum Singleton
{
Instance;
public void SomeMethod()
{
Console.WriteLine("Singleton method called.");
}
}
2.3.4 特点
-
优点:
-
控制实例数量: 确保系统中只有一个实例存在,减少内存开销。
-
全局访问点: 提供了一个全局访问点,便于共享实例。
-
避免资源冲突: 多个线程或进程访问同一资源时,单例模式可以有效地避免冲突。
-
-
缺点:
-
不易扩展: 由于单例类不能被继承,或者不应该被继承,导致其难以扩展。
-
隐藏依赖: 单例模式通过全局访问点共享状态,可能导致隐藏依赖,使得代码难以理解和测试。
-
多线程问题: 在多线程环境下实现单例模式需要小心处理,否则可能导致线程安全问题。
-
2.3.5 适用场景
- 需要全局唯一实例的场景: 如线程池、数据库连接池、配置文件管理器等。
- 需要控制资源的场景: 如打印机管理类,控制对同一资源的并发访问。
- 需要共享状态的场景: 如应用程序的日志类,保证所有日志信息都记录到同一对象中。
单例模式在实际开发中非常常见,但在使用时要注意其潜在的缺陷,特别是在多线程和高并发的环境下,需要选择合适的实现方式以确保线程安全。
2.4 建造者模式(Builder Pattern)
2.4.1 定义
建造者模式将一个复杂对象的构造过程分离出来,使得相同的构造过程可以创建不同的表示。它使用多个简单的对象一步一步构建复杂对象,通过不同的建造者实现不同的构建方式。
2.4.2 结构
建造者模式主要包含以下角色:
- 产品(Product): 最终要创建的复杂对象。
- 抽象建造者(Builder): 定义构造产品的抽象步骤接口。
- 具体建造者(ConcreteBuilder): 实现
Builder
接口,构建和装配各个部分。 - 指挥者(Director): 负责管理
Builder
接口,按步骤构建产品。
UML 类图
+-------------------+ +-------------------+
| Director | | Builder |
+-------------------+ +-------------------+
| - Construct() |<------------->| + BuildPart() |
+-------------------+ +-------------------+
/|\ |
| |
| |
| +-------------------+
| | ConcreteBuilder |
| +-------------------+
| | + BuildPart() |
| | + GetResult() |
| +-------------------+
|
|
+------------------+
| Product |
+------------------+
| + AddPart() |
+------------------+
2.4.3 示例代码
下面是一个用建造者模式构建复杂对象的示例。在这个例子中,我们通过建造者模式来创建一种复杂的 House
对象,包含多个部分如地基、墙壁和屋顶。
产品类
// 产品:House
public class House
{
private List<string> parts = new List<string>();
public void AddPart(string part)
{
parts.Add(part);
}
public void ShowParts()
{
Console.WriteLine("House parts:");
foreach (var part in parts)
{
Console.WriteLine(part);
}
}
}
抽象建造者
// 抽象建造者
public abstract class HouseBuilder
{
protected House house = new House();
public abstract void BuildFoundation();
public abstract void BuildWalls();
public abstract void BuildRoof();
public House GetResult()
{
return house;
}
}
具体建造者
// 具体建造者1:建造木质房屋
public class WoodenHouseBuilder : HouseBuilder
{
public override void BuildFoundation()
{
house.AddPart("Wooden Foundation");
}
public override void BuildWalls()
{
house.AddPart("Wooden Walls");
}
public override void BuildRoof()
{
house.AddPart("Wooden Roof");
}
}
// 具体建造者2:建造石质房屋
public class StoneHouseBuilder : HouseBuilder
{
public override void BuildFoundation()
{
house.AddPart("Stone Foundation");
}
public override void BuildWalls()
{
house.AddPart("Stone Walls");
}
public override void BuildRoof()
{
house.AddPart("Stone Roof");
}
}
指挥者
// 指挥者
public class ConstructionDirector
{
private HouseBuilder _houseBuilder;
public void SetBuilder(HouseBuilder builder)
{
_houseBuilder = builder;
}
public void ConstructHouse()
{
_houseBuilder.BuildFoundation();
_houseBuilder.BuildWalls();
_houseBuilder.BuildRoof();
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
var director = new ConstructionDirector();
// 建造木质房屋
var woodenHouseBuilder = new WoodenHouseBuilder();
director.SetBuilder(woodenHouseBuilder);
director.ConstructHouse();
House woodenHouse = woodenHouseBuilder.GetResult();
woodenHouse.ShowParts();
Console.WriteLine();
// 建造石质房屋
var stoneHouseBuilder = new StoneHouseBuilder();
director.SetBuilder(stoneHouseBuilder);
director.ConstructHouse();
House stoneHouse = stoneHouseBuilder.GetResult();
stoneHouse.ShowParts();
}
}
2.4.4 特点
-
优点:
-
解耦建造过程和产品表示: 客户端不需要知道构造细节,只需要通过指挥者控制构建过程。
-
代码清晰: 将复杂对象的创建过程一步一步实现,使得代码易于维护和理解。
-
更好的控制: 允许逐步创建产品,使得每个部分的构建步骤可以独立变化。
-
-
缺点:
-
产品类型过多: 如果有很多不同的产品类型,可能会导致建造者类的数量过多,增加系统复杂性。
-
难以支持变化: 如果产品的构建步骤发生变化,所有具体建造者都需要修改,难以适应变化。
-
2.4.5 适用场景
- 构建复杂对象: 当一个对象包含多个部分且构建过程复杂时,使用建造者模式。
- 相同的构建过程,创建不同的表示: 当构建过程相同,但不同的具体建造者可以构建不同表示的对象时。
- 分步骤创建对象: 当需要通过多个步骤来创建一个对象,并且这些步骤可能会有所变化时。
2.4.6 与工厂模式对比
- 与工厂模式的区别: 工厂模式关注的是创建单个产品对象,而建造者模式关注的是通过多个步骤构建复杂对象。
- 与抽象工厂模式的区别: 抽象工厂模式提供了一系列相关对象的创建,而建造者模式则更关注构建过程的控制。
建造者模式非常适合在创建复杂对象时使用,尤其是在构建步骤明确且需要控制构建过程的情况下。
2.5 原型模式(Prototype Pattern)
2.5.1 定义
原型模式允许一个对象通过复制自身来创建新的对象。这种模式提供了一种创建对象的快捷方式,尤其适用于创建代价较高的对象。
2.5.2 结构
原型模式的结构包含以下角色:
- 原型(Prototype): 定义一个可以克隆自身的接口。
- 具体原型(ConcretePrototype): 实现
Prototype
接口并能够克隆自身。 - 客户端(Client): 通过请求
Prototype
对象复制自身来创建新对象。
UML 类图
+----------------------+ +-------------------------+
| Prototype |<---------------| ConcretePrototype |
+----------------------+ +-------------------------+
| + Clone(): Prototype | | + Clone(): Prototype |
+----------------------+ +-------------------------+
/|\ |
| |
| |
+---------------------+ +---------------------------------+
| Client | | AnotherConcretePrototype |
+---------------------+ +---------------------------------+
| + Operation() | | + Clone(): Prototype |
+---------------------+ +---------------------------------+
2.5.3 示例代码
原型模式的核心在于实现对象的深拷贝或浅拷贝。以下是一个实现原型模式的简单示例,其中我们克隆一个对象来创建新的对象。
原型接口
// 原型接口
public abstract class Prototype
{
public abstract Prototype Clone();
}
具体原型类
// 具体原型类
public class ConcretePrototype : Prototype
{
public string Name { get; set; }
public ConcretePrototype(string name)
{
Name = name;
}
// 实现克隆方法
public override Prototype Clone()
{
return (Prototype)this.MemberwiseClone(); // 浅拷贝
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建一个原型对象
ConcretePrototype prototype1 = new ConcretePrototype("Prototype 1");
// 克隆原型对象
ConcretePrototype clonedPrototype = (ConcretePrototype)prototype1.Clone();
// 显示克隆对象的属性
Console.WriteLine("Original Prototype Name: " + prototype1.Name);
Console.WriteLine("Cloned Prototype Name: " + clonedPrototype.Name);
// 修改克隆对象的属性
clonedPrototype.Name = "Cloned Prototype 1";
Console.WriteLine("\nAfter modification:");
Console.WriteLine("Original Prototype Name: " + prototype1.Name);
Console.WriteLine("Cloned Prototype Name: " + clonedPrototype.Name);
}
}
在这个例子中,ConcretePrototype
是具体的原型类,实现了 Clone
方法。Clone
方法使用 MemberwiseClone
来执行浅拷贝,这意味着对象的成员变量会被复制,但对象的引用类型成员变量依然指向同一个内存地址。
浅拷贝与深拷贝:
- 浅拷贝(Shallow Copy): 复制对象时,只复制对象的基本数据类型成员变量,对于引用类型成员变量,只复制引用,不复制引用指向的对象。
- 深拷贝(Deep Copy): 复制对象时,不仅复制对象的基本数据类型成员变量,也复制引用类型成员变量所引用的对象,生成全新的副本。
深拷贝的实现
要实现深拷贝,可以在 Clone
方法中手动复制引用类型的成员变量,或者通过序列化和反序列化来实现。
// 实现深拷贝的具体原型类
[Serializable]
public class DeepConcretePrototype : Prototype
{
public string Name { get; set; }
public List<string> Features { get; set; }
public DeepConcretePrototype(string name, List<string> features)
{
Name = name;
Features = features;
}
public override Prototype Clone()
{
// 深拷贝:通过序列化和反序列化
using (MemoryStream stream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, this);
stream.Seek(0, SeekOrigin.Begin);
return (Prototype)formatter.Deserialize(stream);
}
}
}
2.5.4 特点
-
优点:
-
减少对象创建的成本: 尤其是当对象的创建代价很高时,通过克隆一个现有的对象可以减少资源消耗。
-
避免复杂对象的重复创建: 当创建复杂对象需要设置大量参数或步骤时,原型模式可以避免重复工作。
-
易于扩展: 可以通过继承现有原型类来扩展新的克隆行为,而无需修改现有代码。
-
-
缺点:
-
深拷贝实现复杂: 如果对象包含复杂的引用关系,深拷贝的实现可能会很复杂。
-
容易引起混淆: 如果在系统中有多个原型对象,可能会导致代码的可读性降低,增加理解难度。
-
2.5.5 适用场景
- 对象创建代价高: 当一个对象的创建成本高昂(如初始化配置、数据库连接等)时,使用原型模式可以减少创建成本。
- 对象结构复杂: 对于具有复杂结构的对象,使用原型模式可以避免重复的创建工作。
- 需要多个类似实例: 当需要生成一系列相似对象时,使用原型模式可以通过克隆现有对象来快速创建新对象。
2.5.6 与其他模式对比
- 与工厂模式的区别: 工厂模式是通过实例化来创建对象,而原型模式是通过克隆已有对象来创建新对象。
- 与建造者模式的区别: 建造者模式关注的是一步步构建复杂对象,而原型模式是通过复制现有对象来创建新对象。
原型模式非常适合在对象创建复杂或成本较高的场景下使用,它通过克隆来简化对象的创建过程并提高性能。
2.6 适配器模式(Adapter Pattern)
2.6.1 定义
适配器模式将一个类的接口转换成客户希望的另一个接口,适配器使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
2.6.2 结构
适配器模式主要包含以下角色:
- 目标接口(Target): 定义客户所期待的接口。
- 适配者(Adaptee): 需要适配的类,接口不兼容的已有组件。
- 适配器(Adapter): 实现
Target
接口并包装一个Adaptee
对象,从而将Adaptee
的接口转换为Target
的接口。 - 客户端(Client): 通过
Target
接口与适配器交互。
UML 类图
+-------------------+ +-------------------+
| Client | | Target |
+-------------------+ +-------------------+
| - Request() |<-------| + Request() |
+-------------------+ +-------------------+
^
|
+-----------------------+
| Adapter |
+-----------------------+
| + Request() |
| - adaptee: Adaptee |
+-----------------------+
|
v
+-----------------------+
| Adaptee |
+-----------------------+
| + SpecificRequest() |
+-----------------------+
2.6.3 示例代码
以下是一个实现适配器模式的简单示例。在这个例子中,Adaptee
类有一个接口 SpecificRequest
,它与 Target
接口 Request
不兼容。通过适配器模式,我们可以创建一个适配器类,将 Adaptee
的接口转换为 Target
的接口,使得客户端可以通过 Target
接口与 Adaptee
交互。
目标接口
// 目标接口
public interface ITarget
{
void Request();
}
适配者类
// 需要适配的类
public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("Called SpecificRequest()");
}
}
适配器类
// 适配器类
public class Adapter : ITarget
{
private readonly Adaptee _adaptee;
public Adapter(Adaptee adaptee)
{
_adaptee = adaptee;
}
public void Request()
{
// 调用适配者的接口,将其转换为目标接口
_adaptee.SpecificRequest();
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 客户端需要通过 ITarget 接口调用方法
ITarget target = new Adapter(new Adaptee());
// 客户端通过目标接口与适配器交互
target.Request();
}
}
2.6.4 特点
-
优点:
-
提高了类的复用性: 通过适配器模式,原本无法复用的类现在可以在新的环境中使用。
-
提高了类的灵活性: 通过使用适配器,可以轻松引入新接口,改变现有类的行为,而不需要修改现有代码。
-
符合开闭原则: 新增适配器类不会影响现有代码的功能,符合开闭原则。
-
-
缺点:
-
复杂性增加: 引入适配器模式可能会增加系统的复杂性,特别是在适配多个接口时,需要创建多个适配器类。
-
性能开销: 如果适配器做了大量的转换工作,可能会带来一定的性能开销。
-
2.6.5 适用场景
- 现有类与新接口不兼容: 当你想使用一个已经存在的类,但它的接口不符合你的要求时,可以使用适配器模式。
- 引入遗留系统: 在引入遗留系统的类库时,使用适配器模式可以避免修改现有的代码。
- 替代接口重构: 当接口发生变化时,可以通过适配器模式逐步过渡到新的接口,而不需要一次性修改所有依赖代码。
2.6.6 与其他模式的关系
- 与装饰器模式的区别: 装饰器模式关注的是增强对象的功能,而适配器模式关注的是将一个接口转换为另一个接口。
- 与桥接模式的区别: 桥接模式是为了将抽象部分与实现部分分离,从而可以独立地变化,而适配器模式是为了让现有的接口适配新的接口。
适配器模式非常适合在项目中需要集成现有系统或类库,而它们的接口又与当前需求不兼容时使用。通过适配器模式,可以在不修改现有代码的前提下实现接口的兼容和扩展。
2.7 桥接模式(Bridge Pattern)
2.7 .1 定义
桥接模式允许在运行时将抽象类与其实现类解耦。它通过引入一个接口来使得抽象类与具体的实现类分开,从而支持两者的独立变化。这样可以避免复杂的多层继承结构。
2.7 .2 结构
桥接模式的结构包含以下角色:
- 抽象类(Abstraction): 定义抽象接口,并保存一个指向实现类的引用。
- 扩展抽象类(Refined Abstraction): 继承抽象类,并实现其接口。
- 实现类接口(Implementor): 定义实现类的接口。
- 具体实现类(Concrete Implementor): 实现实现类接口的具体类。
UML 类图
+-------------------+ +-------------------+
| Abstraction | | Implementor |
+-------------------+ +-------------------+
| - implementor | | + OperationImpl() |
| + Operation() | +-------------------+
+-------------------+ ^
| |
| |
+-------------------+ +---------------------+
| RefinedAbstraction| | ConcreteImplementor |
+-------------------+ +---------------------+
| + Operation() | | + OperationImpl() |
+-------------------+ +---------------------+
2.7 .3 示例代码
以下是一个实现桥接模式的简单示例。假设我们要创建一个图形绘制程序,其中有不同类型的图形和不同的绘制方式。
实现类接口
// 实现者接口
public interface IColor
{
void ApplyColor();
}
具体实现类
// 具体实现者1:红色
public class RedColor : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying red color.");
}
}
// 具体实现者2:绿色
public class GreenColor : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying green color.");
}
}
抽象类
// 抽象类:形状
public abstract class Shape
{
protected IColor color;
protected Shape(IColor color)
{
this.color = color;
}
public abstract void Draw();
}
扩展抽象类
// 细化抽象类1:圆形
public class Circle : Shape
{
public Circle(IColor color) : base(color)
{
}
public override void Draw()
{
Console.Write("Circle is being drawn. ");
color.ApplyColor();
}
}
// 细化抽象类2:矩形
public class Rectangle : Shape
{
public Rectangle(IColor color) : base(color)
{
}
public override void Draw()
{
Console.Write("Rectangle is being drawn. ");
color.ApplyColor();
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建红色和绿色的实现者
IColor red = new RedColor();
IColor green = new GreenColor();
// 创建带有不同颜色的形状
Shape redCircle = new Circle(red);
Shape greenRectangle = new Rectangle(green);
// 画形状
redCircle.Draw(); // 输出: Circle is being drawn. Applying red color.
greenRectangle.Draw(); // 输出: Rectangle is being drawn. Applying green color.
}
}
在这个例子中:
IColor
是实现者接口,定义了ApplyColor()
方法。RedColor
和GreenColor
是具体实现者,实现了IColor
接口。Shape
是抽象类,它维护了一个IColor
类型的引用,并定义了Draw()
方法。Circle
和Rectangle
是Shape
的细化抽象类,分别实现了Draw()
方法。
2.7 .4 特点
-
优点:
-
解耦抽象与实现: 通过桥接模式,抽象与实现可以独立变化,降低了系统的耦合度。
-
灵活性: 可以很方便地扩展新的抽象和实现类,而不需要修改已有的代码。
-
符合开闭原则: 新的实现可以在不修改原有代码的情况下添加。
-
-
缺点:
-
增加了系统的复杂性: 由于引入了多个类和接口,可能会使得系统结构变得复杂。
-
维护难度增加: 当扩展的类和接口数量增多时,维护这些类和接口的难度可能会增加。
-
2.7 .5 适用场景
- 多变的实现类: 当你有多个实现类和多个抽象类,并且你希望在运行时动态组合这些实现时,可以使用桥接模式。
- 避免类爆炸: 当抽象类和实现类的数量不断增加,导致类的数量爆炸时,可以考虑使用桥接模式来减少复杂性。
2.7 .6 与其他模式的关系
- 适配器模式: 适配器模式用于解决接口不兼容的问题,而桥接模式用于将抽象与实现解耦。
- 组合模式: 桥接模式与组合模式都使用了组合的方式来构建对象,但桥接模式侧重于解耦,而组合模式更关注于树形结构的创建。
桥接模式通过将抽象与实现分离,使得系统的扩展更加灵活,适用于需要同时支持多种变化的场景。
2.8 组合模式(Composite Pattern)
2.8.1 定义
组合模式通过定义树形结构来组成对象,使得客户端对单个对象和对象集合的处理方式一致。这个模式让你可以使用相同的接口来操作单一对象和对象集合,简化了代码和操作逻辑。
2.8.2 结构
组合模式的结构包含以下角色:
- 组件(Component): 定义树形结构中所有对象的共同接口,包括叶子节点和复合节点(容器节点)。
- 叶子节点(Leaf): 叶子节点是树的基本元素,它实现了
Component
接口,但没有子节点。 - 复合节点(Composite): 复合节点可以包含子节点,包括叶子节点和其他复合节点,实现了
Component
接口并能够管理其子节点。
UML 类图
+---------------------+
| Component |
+---------------------+
| + Operation() |
+---------------------+
| + Add(Component) |
| + Remove(Component) |
| + GetChild(int) |
+---------------------+
^
|
+---------------------+ +---------------------+
| Leaf | | Composite |
+---------------------+ +---------------------+
| + Operation() | | + Operation() |
+---------------------+ +---------------------+
| + Add(Component) |
| + Remove(Component) |
| + GetChild(int) |
+---------------------+
2.8.3 示例代码
以下是一个实现组合模式的简单示例。在这个示例中,我们有一个文件系统,其中 File
是叶子节点,Directory
是复合节点(目录),它可以包含多个文件或目录。
组件接口
// 组件接口
public abstract class FileSystemComponent
{
public abstract void Display(int depth);
// 组合节点的方法
public virtual void Add(FileSystemComponent component) { }
public virtual void Remove(FileSystemComponent component) { }
public virtual FileSystemComponent GetChild(int index) { return null; }
}
叶子节点类
// 叶子节点类:文件
public class File : FileSystemComponent
{
private string _name;
public File(string name)
{
_name = name;
}
public override void Display(int depth)
{
Console.WriteLine(new string('-', depth) + _name);
}
}
复合节点类
// 复合节点类:目录
public class Directory : FileSystemComponent
{
private List<FileSystemComponent> _children = new List<FileSystemComponent>();
private string _name;
public Directory(string name)
{
_name = name;
}
public override void Add(FileSystemComponent component)
{
_children.Add(component);
}
public override void Remove(FileSystemComponent component)
{
_children.Remove(component);
}
public override FileSystemComponent GetChild(int index)
{
return _children[index];
}
public override void Display(int depth)
{
Console.WriteLine(new string('-', depth) + _name);
foreach (var child in _children)
{
child.Display(depth + 2);
}
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建文件和目录
FileSystemComponent file1 = new File("File 1");
FileSystemComponent file2 = new File("File 2");
FileSystemComponent file3 = new File("File 3");
Directory directory1 = new Directory("Directory 1");
Directory directory2 = new Directory("Directory 2");
// 构建目录树
directory1.Add(file1);
directory1.Add(file2);
directory2.Add(file3);
directory2.Add(directory1);
// 显示目录树
directory2.Display(1);
}
}
在这个示例中:
FileSystemComponent
是组件接口,定义了叶子节点和复合节点的共同接口。File
是叶子节点,表示文件,没有子节点。Directory
是复合节点,表示目录,可以包含多个子节点(文件或其他目录)。
2.8.4 特点
-
优点:
-
简化客户端代码: 客户端代码可以统一处理叶子节点和复合节点,减少了代码复杂度。
-
增加灵活性: 通过将叶子节点和复合节点统一成一个接口,可以灵活地构建和管理复杂的树形结构。
-
符合开闭原则: 可以通过添加新的叶子节点或复合节点来扩展功能,而无需修改现有代码。
-
-
缺点:
-
设计复杂: 组合模式可能会增加系统的复杂性,因为你需要设计和管理树形结构。
-
性能问题: 如果树形结构非常庞大,操作树形结构可能会影响性能。
-
2.8.5 适用场景
- 部分-整体层次结构: 当你需要表示部分-整体的层次结构时,例如文件系统、组织结构等。
- 统一处理树形结构: 当你需要统一处理树形结构中的对象,无论它们是叶子节点还是复合节点。
- 动态构建树形结构: 当你需要动态构建和操作复杂的树形结构时,例如图形界面中的组件树。
2.8.6 与其他模式对比
- 与桥接模式的区别: 组合模式用于表示部分-整体层次结构,而桥接模式用于将抽象与实现分离。
- 与装饰器模式的区别: 组合模式主要用于处理树形结构,而装饰器模式用于动态地增加对象的功能。
组合模式非常适合用来构建复杂的树形结构,通过将对象和对象集合统一成一个接口,它能够简化对复杂结构的操作,并提高系统的灵活性和可扩展性。
2.9 装饰器模式(Decorator Pattern)
2.9.1 定义
装饰器模式通过创建一个装饰器类,它实现了与被装饰对象相同的接口,从而可以在运行时动态地添加新功能。这种模式提供了一种灵活的替代方案来扩展对象的功能,避免了使用子类来扩展功能的需要。
2.9.2 结构
装饰器模式的结构包含以下角色:
- 组件(Component): 定义一个接口,描述可以动态添加的功能。
- 具体组件(Concrete Component): 实现
Component
接口的具体对象,表示要装饰的对象。 - 装饰器(Decorator): 维护一个对
Component
对象的引用,定义与Component
接口相同的接口,并可以在运行时扩展功能。 - 具体装饰器(Concrete Decorator): 扩展
Decorator
,实现具体的附加功能。
UML 类图
+-------------------+
| Component |
+-------------------+
| + Operation() |
+-------------------+
^
|
+-------------------+ +---------------------------+
| ConcreteComponent | <------- | Decorator |
+-------------------+ +---------------------------+
| + Operation() | | - component: Component |
+-------------------+ | + Operation() |
+---------------------------+
^
|
+------------------------+
| ConcreteDecorator |
+------------------------+
| + Operation() |
| + AdditionalBehavior() |
+------------------------+
2.9.3 示例代码
以下是一个实现装饰器模式的简单示例。在这个示例中,我们有一个饮料的系统,可以为饮料添加不同的配料(装饰)。
组件接口
// 组件接口
public abstract class Beverage
{
public abstract string GetDescription();
public abstract double Cost();
}
具体组件类
// 具体组件:咖啡
public class Coffee : Beverage
{
public override string GetDescription()
{
return "Coffee";
}
public override double Cost()
{
return 2.00; // 基本咖啡的价格
}
}
装饰器类
// 装饰器类
public abstract class CondimentDecorator : Beverage
{
protected Beverage _beverage;
public CondimentDecorator(Beverage beverage)
{
_beverage = beverage;
}
}
具体装饰器类
// 具体装饰器:牛奶
public class MilkDecorator : CondimentDecorator
{
public MilkDecorator(Beverage beverage) : base(beverage)
{
}
public override string GetDescription()
{
return _beverage.GetDescription() + ", Milk";
}
public override double Cost()
{
return _beverage.Cost() + 0.50; // 添加牛奶的价格
}
}
// 具体装饰器:巧克力
public class ChocolateDecorator : CondimentDecorator
{
public ChocolateDecorator(Beverage beverage) : base(beverage)
{
}
public override string GetDescription()
{
return _beverage.GetDescription() + ", Chocolate";
}
public override double Cost()
{
return _beverage.Cost() + 0.70; // 添加巧克力的价格
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建基本的咖啡
Beverage beverage = new Coffee();
// 添加配料
beverage = new MilkDecorator(beverage);
beverage = new ChocolateDecorator(beverage);
// 显示描述和总费用
Console.WriteLine($"{beverage.GetDescription()} costs {beverage.Cost():C}");
}
}
在这个示例中:
Beverage
是组件接口,定义了饮料的基本功能。Coffee
是具体组件,表示基础饮料(咖啡)。CondimentDecorator
是装饰器类,它包含了对Beverage
对象的引用,并实现了Beverage
接口。MilkDecorator
和ChocolateDecorator
是具体装饰器,分别添加牛奶和巧克力的功能。
2.9.4 特点
-
优点:
-
灵活性: 可以动态添加和扩展对象的功能,而不需要创建大量的子类。
-
符合开闭原则: 可以通过添加新的装饰器类来扩展功能,而无需修改已有的类。
-
避免类的膨胀: 通过组合多个装饰器,可以避免创建过多的子类,从而减少类的数量。
-
-
缺点:
-
复杂性: 装饰器模式可能会增加系统的复杂性,尤其是当装饰器层次过多时。
-
调试困难: 由于装饰器的嵌套,调试代码时可能会变得复杂。
-
2.9.5 适用场景
- 需要动态扩展功能: 当你需要在运行时动态地给对象添加功能时,装饰器模式是一个理想的选择。
- 避免类的膨胀: 当类的扩展导致创建大量子类时,装饰器模式可以帮助你减少类的数量。
- 需要组合多个功能: 当你需要组合多个功能或行为时,装饰器模式能够轻松实现。
2.9.6 与其他模式对比
- 与组合模式的区别: 装饰器模式用于在不修改对象结构的情况下动态添加功能,而组合模式用于表示部分-整体的层次结构。
- 与代理模式的区别: 代理模式用于控制对对象的访问,而装饰器模式用于在不改变对象的接口的情况下动态添加功能。
装饰器模式提供了一种灵活的方式来扩展对象的功能,使得开发者可以在不影响已有代码的情况下,轻松地添加新的行为或属性。
2.10 外观模式(Facade Pattern)
2.10.1 定义
外观模式通过为复杂的子系统提供一个简化的接口,使得客户端可以通过这个接口访问子系统的功能,而不需要直接与子系统的多个组件进行交互。外观模式可以隐藏子系统的复杂性,并且使得子系统与客户端的耦合度降低。
2.10.2 结构
外观模式的结构包含以下角色:
- 外观(Facade): 提供一个简化的接口,封装子系统的复杂性,供客户端调用。
- 子系统(Subsystem Classes): 子系统中的多个类,它们实现了子系统的实际功能。子系统可以有多个类,客户端可以直接访问它们,但通过外观类访问会更简单。
UML 类图
+-------------------+
| Client |
+-------------------+
^
|
+-------------------+ +-------------------+ +-------------------+
| SubsystemA | ----> | Facade | <---- | SubsystemC |
+-------------------+ +-------------------+ +-------------------+
^ ^
| |
+-------------------+ +-------------------+
| SubsystemB | | SubsystemD |
+-------------------+ +-------------------+
2.10.3 示例代码
以下是一个实现外观模式的简单示例。在这个示例中,我们构建了一个家庭影院系统,客户端通过外观类 HomeTheaterFacade
来控制各个子系统,如电视、音响、灯光等。
子系统类
// 子系统类:电视
public class Television
{
public void On()
{
Console.WriteLine("Television is on.");
}
public void Off()
{
Console.WriteLine("Television is off.");
}
}
// 子系统类:音响
public class SoundSystem
{
public void On()
{
Console.WriteLine("Sound system is on.");
}
public void Off()
{
Console.WriteLine("Sound system is off.");
}
public void SetVolume(int volume)
{
Console.WriteLine($"Sound system volume set to {volume}.");
}
}
// 子系统类:灯光
public class Lights
{
public void Dim(int level)
{
Console.WriteLine($"Lights dimmed to {level}%.");
}
public void On()
{
Console.WriteLine("Lights are on.");
}
}
外观类
// 外观类:家庭影院
public class HomeTheaterFacade
{
private readonly Television _television;
private readonly SoundSystem _soundSystem;
private readonly Lights _lights;
public HomeTheaterFacade(Television television, SoundSystem soundSystem, Lights lights)
{
_television = television;
_soundSystem = soundSystem;
_lights = lights;
}
public void WatchMovie()
{
Console.WriteLine("Get ready to watch a movie...");
_lights.Dim(30);
_television.On();
_soundSystem.On();
_soundSystem.SetVolume(5);
}
public void EndMovie()
{
Console.WriteLine("Shutting movie theater down...");
_television.Off();
_soundSystem.Off();
_lights.On();
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建子系统对象
Television television = new Television();
SoundSystem soundSystem = new SoundSystem();
Lights lights = new Lights();
// 创建外观对象
HomeTheaterFacade homeTheater = new HomeTheaterFacade(television, soundSystem, lights);
// 使用外观模式控制子系统
homeTheater.WatchMovie();
Console.WriteLine("\nMovie is running...\n");
homeTheater.EndMovie();
}
}
在这个例子中:
Television
、SoundSystem
、Lights
是子系统类,提供了各自的功能。HomeTheaterFacade
是外观类,它将各个子系统的操作封装成了WatchMovie()
和EndMovie()
两个简单的方法,客户端只需调用这些方法即可控制整个家庭影院系统。- 客户端
Program
通过HomeTheaterFacade
类来控制整个家庭影院的各个设备,而不需要直接与每个设备交互。
2.10.4 特点
-
优点:
-
简化接口: 提供一个简化的接口来访问复杂的子系统,减少客户端与子系统之间的耦合。
-
隐藏子系统的复杂性: 客户端不需要了解子系统的内部结构,只需要与外观类交互即可。
-
减少依赖: 客户端与子系统之间的依赖关系减少,如果子系统发生变化,只需修改外观类,而不需要修改客户端代码。
-
-
缺点:
-
不符合开闭原则: 如果要扩展外观类的功能,可能需要修改外观类的代码,从而违反开闭原则。
-
潜在性能问题: 外观模式可能会因为封装了大量子系统的调用,而引入一定的性能开销。
-
2.10.5 适用场景
- 简化复杂系统的使用: 当系统内部结构复杂,客户端希望能够通过简单的接口使用系统时,使用外观模式非常合适。
- 解耦客户端与子系统: 当你希望减少客户端与多个子系统之间的依赖关系时,可以使用外观模式。
- 构建库或框架: 当你构建一个复杂库或框架,并希望提供一个简洁的接口供外部使用时,外观模式可以帮助你设计这个接口。
2.10.6 与其他模式的关系
- 与适配器模式的区别: 适配器模式是为了将一个接口转换为客户端期望的接口,而外观模式是为了提供一个简化的接口来使用复杂系统。适配器模式专注于接口兼容性,而外观模式专注于简化系统的使用。
- 与桥接模式的区别: 桥接模式用于将抽象与实现分离,使得它们可以独立变化,而外观模式关注于简化接口。
- 与单例模式的结合: 在某些情况下,外观类可以设计成单例,以确保客户端使用的是同一个外观对象。
外观模式通过提供一个统一和简化的接口来隐藏系统的复杂性,使得客户端能够更轻松地使用系统,同时保持系统内部结构的灵活性和可扩展性。
2.11 享元模式(Flyweight Pattern)
2.11.1 定义
享元模式的核心思想是将对象的状态分为内部状态(可以共享的部分)和外部状态(不能共享的部分)。通过共享相同的内部状态,减少内存的重复占用,从而实现系统的资源优化。
2.11.2 结构
享元模式的结构包含以下角色:
- 享元(Flyweight): 定义享元对象的接口,通过外部状态完成享元对象的操作。
- 具体享元(Concrete Flyweight): 实现享元接口,并存储可以共享的内部状态。
- 非共享具体享元(Unshared Concrete Flyweight): 不可共享的享元类,通常是享元对象的组合。
- 享元工厂(Flyweight Factory): 创建并管理享元对象,确保合理地共享对象。
- 客户端(Client): 维护对所有享元对象的引用,并且需要将享元对象的外部状态传递给享元对象。
UML 类图
+-------------------+
| IFlyweight |
+-------------------+
| + Operation() |
+-------------------+
^
|
+-----------------------+
| ConcreteFlyweight |
+-----------------------+
| - property1 | // 共享的内部状态
| - property2 | // 共享的内部状态
| + Operation() |
+-----------------------+
+-------------------+
| FlyweightFactory |
+-------------------+
| - flyweights |
| + Operation() |
+-------------------+
+---------------------------+
| UnsharedConcreteFlyweight |
+---------------------------+
| - property1 | // 不共享的外部状态
| - property2 | // 不共享的外部状态
| - flyweight | // 组合享元
| + Operation() |
+---------------------------+
2.11.3 示例代码
2.11.3.1 文字处理器
以下是一个实现享元模式的简单示例。在这个示例中,我们模拟了一个文字处理器,其中的字符对象可以被共享,以减少内存的占用。
享元接口
// 享元接口
public interface ICharacter
{
void Display(int fontSize);
}
具体享元类
// 具体享元类:字符
public class Character : ICharacter
{
private readonly char _symbol; // 内部状态(共享部分)
public Character(char symbol)
{
_symbol = symbol;
}
public void Display(int fontSize)
{
Console.WriteLine($"Character: {_symbol}, Font size: {fontSize}");
}
}
享元工厂类
// 享元工厂类
public class CharacterFactory
{
private readonly Dictionary<char, ICharacter> _characters = new Dictionary<char, ICharacter>();
public ICharacter GetCharacter(char symbol)
{
if (!_characters.ContainsKey(symbol))
{
_characters[symbol] = new Character(symbol); // 创建新的享元对象
}
return _characters[symbol]; // 返回共享的享元对象
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
CharacterFactory factory = new CharacterFactory();
// 获取并显示字符对象
ICharacter a = factory.GetCharacter('A');
a.Display(12);
ICharacter b = factory.GetCharacter('B');
b.Display(14);
ICharacter a2 = factory.GetCharacter('A');
a2.Display(18);
// 检查两个 'A' 字符是否为同一个实例
Console.WriteLine($"Is 'A' and 'A2' the same instance? {ReferenceEquals(a, a2)}");
}
}
在这个例子中:
ICharacter
是享元接口,定义了Display()
方法,用于显示字符及其大小。Character
是具体享元类,存储了共享的字符符号_symbol
,并在Display()
方法中使用外部状态(字体大小)。CharacterFactory
是享元工厂类,负责创建和管理Character
对象,并确保相同的字符符号只创建一个实例。- 客户端代码通过工厂获取字符对象,并在不同的字体大小下显示它们,同时检查同一字符是否被共享。
2.11.3.2 棋子与棋盘的实现示例
享元接口
// 享元接口:棋子
public interface IChessPiece
{
void Display(int x, int y);
}
具体享元类
// 具体享元类:具体的棋子,如"黑车"或"白马"
public class ChessPiece : IChessPiece
{
private readonly string _color; // 内部状态(共享部分)
private readonly string _type; // 内部状态(共享部分)
public ChessPiece(string color, string type)
{
_color = color;
_type = type;
}
public void Display(int x, int y)
{
Console.WriteLine($"Chess Piece: {_color} {_type}, Position: ({x}, {y})");
}
}
享元工厂类
// 享元工厂类:负责管理棋子对象
public class ChessPieceFactory
{
private readonly Dictionary<string, IChessPiece> _pieces = new Dictionary<string, IChessPiece>();
public IChessPiece GetChessPiece(string color, string type)
{
string key = color + "_" + type;
if (!_pieces.ContainsKey(key))
{
_pieces[key] = new ChessPiece(color, type);
}
return _pieces[key];
}
}
非共享具体享元类
// 非共享具体享元类:棋盘上的棋子
public class ChessBoardPosition
{
private readonly int _x; // 外部状态
private readonly int _y; // 外部状态
private readonly IChessPiece _chessPiece; // 共享享元
public ChessBoardPosition(int x, int y, IChessPiece chessPiece)
{
_x = x;
_y = y;
_chessPiece = chessPiece;
}
public void Display()
{
_chessPiece.Display(_x, _y);
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
ChessPieceFactory factory = new ChessPieceFactory();
// 获取棋子,并在棋盘上设置位置
ChessBoardPosition position1 = new ChessBoardPosition(1, 1, factory.GetChessPiece("Black", "Rook"));
ChessBoardPosition position2 = new ChessBoardPosition(1, 2, factory.GetChessPiece("Black", "Knight"));
ChessBoardPosition position3 = new ChessBoardPosition(1, 3, factory.GetChessPiece("Black", "Bishop"));
ChessBoardPosition position4 = new ChessBoardPosition(1, 1, factory.GetChessPiece("White", "Pawn"));
// 显示棋盘上的棋子
position1.Display();
position2.Display();
position3.Display();
position4.Display();
}
}
在这个例子中:
- 享元接口
IChessPiece
: 定义了显示棋子的方法Display()
,要求提供棋子的位置信息。 - 具体享元类
ChessPiece
: 实现了IChessPiece
接口,包含了棋子的颜色和类型,这些是共享的内部状态。 - 享元工厂类
ChessPieceFactory
: 负责创建和管理享元对象(棋子),确保同种颜色和类型的棋子只创建一个实例。 - 非共享具体享元类
ChessBoardPosition
: 代表棋盘上的每一个棋子位置,包含棋子在棋盘上的位置坐标_x
和_y
,这些是非共享的外部状态。每个位置持有一个共享的棋子对象。
2.11.4 特点
-
优点:
-
减少内存占用: 通过共享对象,显著减少了系统中的内存消耗,特别适用于大量相似对象的场景。
-
提高性能: 减少了创建对象的开销,特别是在对象创建成本高昂的情况下。
-
-
缺点:
-
增加复杂性: 引入共享机制后,代码的复杂性增加,需要管理外部状态和内部状态。
-
非线程安全: 享元对象在多线程环境下可能会引发线程安全问题,需要谨慎处理。
-
2.11.5 适用场景
- 需要大量细粒度对象: 当系统中需要创建大量相似对象时,可以考虑使用享元模式来减少内存消耗。
- 外部状态可分离: 当对象的状态可以分为内部状态和外部状态,且内部状态可以共享时,享元模式非常适合。
2.11.6 与其他模式的关系
- 与单例模式的区别: 单例模式确保一个类只有一个实例,而享元模式允许多个实例共享内部状态。
- 与工厂方法模式的区别: 工厂方法模式负责创建对象,而享元模式则关注共享对象的管理。
享元模式通过共享对象的内部状态,有效地减少了内存的占用,是优化系统性能的一种有效手段,特别是在需要大量相似对象的情况下。
2.12 代理模式(Proxy Pattern)
2.12.1 定义
代理模式通过创建一个代理对象来控制对另一个对象的访问。代理对象具有与原对象相同的接口,客户端可以通过代理对象访问实际的服务对象。代理对象可以在不影响客户端的情况下,对请求进行预处理或后处理。
2.12.2 结构
代理模式的结构包含以下角色:
- Subject(抽象主题): 定义了代理类和真实类的公共接口。
- RealSubject(真实主题): 实现了抽象主题的接口,是代理所代表的真实对象。
- Proxy(代理): 实现了抽象主题接口,并持有一个真实主题对象的引用。代理对象可以在调用真实对象的操作前后执行一些操作。
UML 类图
+-----------------+
| Subject | <--------- 抽象主题
+-----------------+ |
| + Request() | |
+-----------------+ |
^ |
| |
+-----------------+ +-----------------+
| Proxy | | RealSubject |
+-----------------+ +-----------------+
| - realSubject: | ------- | + Request() |
| RealSubject | +-----------------+
| + Request() |
+-----------------+
2.12.3 示例代码
假设我们有一个需要访问的图像文件对象。图像文件可能很大,所以我们希望在图像真正需要显示时才加载它。在这种情况下,我们可以使用代理模式。
抽象主题接口
// 抽象主题
public interface IImage
{
void Display();
}
真实主题类
// 真实主题类:真实的图像文件
public class RealImage : IImage
{
private readonly string _filename;
public RealImage(string filename)
{
_filename = filename;
LoadImageFromDisk(); // 模拟加载图像文件
}
private void LoadImageFromDisk()
{
Console.WriteLine($"Loading image from disk: {_filename}");
}
public void Display()
{
Console.WriteLine($"Displaying image: {_filename}");
}
}
代理类
// 代理类:代理图像
public class ProxyImage : IImage
{
private RealImage _realImage;
private readonly string _filename;
public ProxyImage(string filename)
{
_filename = filename;
}
public void Display()
{
if (_realImage == null)
{
_realImage = new RealImage(_filename); // 延迟加载
}
_realImage.Display();
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
IImage image = new ProxyImage("test_image.jpg");
// 图像第一次显示时,代理对象会加载真实图像
image.Display();
// 图像再次显示时,不需要再次加载
image.Display();
}
}
在这个例子中:
- 抽象主题
IImage
: 定义了Display()
方法,所有图像对象都必须实现这个方法。 - 真实主题类
RealImage
: 实现了IImage
接口,并在构造函数中模拟加载图像文件。Display()
方法显示图像。 - 代理类
ProxyImage
: 同样实现了IImage
接口,持有一个RealImage
对象的引用。只有在需要时,才会加载真实的图像文件,执行延迟加载。
运行结果
运行上述代码,你会看到以下输出:
Loading image from disk: test_image.jpg
Displaying image: test_image.jpg
Displaying image: test_image.jpg
在这个例子中,图像在第一次调用 Display()
时才会加载,之后的调用直接显示图像,无需再次加载。这就是代理模式的延迟加载(Lazy Loading)应用。
2.12.4 特点
-
优点:
-
控制对象访问: 可以在不影响客户端的情况下控制对目标对象的访问。
-
延迟加载: 通过代理可以实现对象的延迟初始化,节省资源。
-
权限控制: 可以通过代理实现对敏感对象的访问控制。
-
-
缺点:
-
增加复杂性: 增加了类的数量和系统的复杂性。
-
可能引入性能开销: 代理模式可能会引入额外的处理逻辑,导致性能开销。
-
2.12.5 适用场景
- 需要延迟加载的场景: 当对象的创建成本较高且不经常使用时。
- 远程对象访问: 通过网络访问远程对象时,可以使用代理来隐藏复杂性。
- 控制访问权限: 需要对对象的访问进行权限控制时。
- 智能引用代理: 在访问对象时附加一些额外操作,如引用计数、日志记录等。
2.12.6 与其他模式的关系
- 装饰器模式: 代理和装饰器都通过代理控制对象,但装饰器用于添加行为,而代理用于控制访问。
- 适配器模式: 适配器模式改变接口以适配另一系统,而代理模式不改变接口,只是控制访问。
代理模式在设计复杂系统时提供了非常灵活的对象管理方式,通过合理使用代理,可以有效地提升系统的性能和安全性。
2.13 责任链模式(Chain of Responsibility Pattern)
2.13.1 定义
责任链模式的核心思想是将请求沿着处理者链传递,直到其中一个处理者处理这个请求。这种模式的一个重要特性是:请求的发送者并不知道哪个对象会最终处理请求,系统中的处理者对象也无需知道其他处理者的结构,处理者之间的解耦提高了系统的灵活性。
2.13.2 结构
责任链模式包含以下角色:
- 抽象处理者(Handler): 定义一个处理请求的接口,并且实现链中下一个处理者的引用。
- 具体处理者(ConcreteHandler): 具体实现抽象处理者,处理请求或者将请求传递给链中的下一个处理者。
- 客户端(Client): 负责将请求发送到链中的第一个处理者,通常不关心请求的最终处理者是谁。
UML 类图
+-----------------------------------+
| Handler | <----- 抽象处理者
+-----------------------------------+
| - next: Handler | <----- 链中下一个处理者的引用
| + SetNext(handler: Handler) |
| + HandleRequest(request: Request) |
+-----------------------------------+
^
|
+-----------------------------------+ +-----------------------------------+
| ConcreteHandler1 | | ConcreteHandler2 |
+-----------------------------------+ +-----------------------------------+
| + HandleRequest(request: Request) | | + HandleRequest(request: Request) |
+-----------------------------------+ +-----------------------------------+
2.13.3 示例代码
假设我们有一个支持多级审批的系统,每个级别的审批员处理不同级别的请求。请求可以从一个审批员传递到下一个审批员,直到某个审批员处理了请求或者请求被拒绝。
抽象处理者
// 抽象处理者
public abstract class Approver
{
protected Approver _nextApprover;
public void SetNext(Approver nextApprover)
{
_nextApprover = nextApprover;
}
public abstract void ProcessRequest(PurchaseRequest request);
}
具体处理者
// 具体处理者1:经理
public class Manager : Approver
{
public override void ProcessRequest(PurchaseRequest request)
{
if (request.Amount < 10000)
{
Console.WriteLine($"{this.GetType().Name} approved request# {request.Number}");
}
else if (_nextApprover != null)
{
_nextApprover.ProcessRequest(request);
}
}
}
// 具体处理者2:总监
public class Director : Approver
{
public override void ProcessRequest(PurchaseRequest request)
{
if (request.Amount < 25000)
{
Console.WriteLine($"{this.GetType().Name} approved request# {request.Number}");
}
else if (_nextApprover != null)
{
_nextApprover.ProcessRequest(request);
}
}
}
// 具体处理者3:副总裁
public class VicePresident : Approver
{
public override void ProcessRequest(PurchaseRequest request)
{
if (request.Amount < 50000)
{
Console.WriteLine($"{this.GetType().Name} approved request# {request.Number}");
}
else if (_nextApprover != null)
{
_nextApprover.ProcessRequest(request);
}
}
}
// 具体处理者4:总裁
public class President : Approver
{
public override void ProcessRequest(PurchaseRequest request)
{
if (request.Amount < 100000)
{
Console.WriteLine($"{this.GetType().Name} approved request# {request.Number}");
}
else
{
Console.WriteLine($"Request# {request.Number} requires an executive meeting!");
}
}
}
请求类
// 请求类:采购请求
public class PurchaseRequest
{
public int Number { get; }
public double Amount { get; }
public string Purpose { get; }
public PurchaseRequest(int number, double amount, string purpose)
{
Number = number;
Amount = amount;
Purpose = purpose;
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建处理者对象
Approver manager = new Manager();
Approver director = new Director();
Approver vp = new VicePresident();
Approver president = new President();
// 设置责任链
manager.SetNext(director);
director.SetNext(vp);
vp.SetNext(president);
// 创建多个采购请求
PurchaseRequest request1 = new PurchaseRequest(1, 5000, "Buy supplies");
PurchaseRequest request2 = new PurchaseRequest(2, 20000, "Buy projectors");
PurchaseRequest request3 = new PurchaseRequest(3, 35000, "Buy laptops");
PurchaseRequest request4 = new PurchaseRequest(4, 90000, "Buy servers");
// 处理请求
manager.ProcessRequest(request1);
manager.ProcessRequest(request2);
manager.ProcessRequest(request3);
manager.ProcessRequest(request4);
}
}
运行结果
Manager approved request# 1
Director approved request# 2
VicePresident approved request# 3
President approved request# 4
在这个例子中,Manager
、Director
、VicePresident
和 President
这四个处理者构成了一个责任链。每个处理者都检查请求的金额,并决定是否可以处理请求,如果不能处理则将请求传递给下一个处理者。
2.13.5 特点
-
优点:
-
降低耦合度: 客户端不需要知道哪个处理者会最终处理请求,这使得系统更灵活、可扩展。
-
增加灵活性: 可以动态地添加或移除处理者,甚至可以动态调整处理者链的顺序。
-
职责分离: 每个处理者只关注自己能够处理的那部分职责,符合单一职责原则。
-
-
缺点:
-
不保证请求被处理: 如果链上的处理者都不能处理请求,那么请求可能会被丢弃。
-
可能影响性能: 如果责任链过长,或者某些处理者链路中的节点过多,可能导致请求处理的延迟。
-
2.13.6 适用场景
- 多个对象可以处理同一请求: 比如事件处理系统、请求处理链。
- 请求的处理需要动态指定: 比如审批流程中的不同审批级别。
- 需要在不明确接收者的情况下发送请求: 客户端不需要知道具体是哪个对象处理了请求。
2.13.7 总结
责任链模式通过将处理者对象串联成一条链,使请求能够沿着链传递,直到被某个处理者处理。这种模式的灵活性使其适用于多种场景,尤其是那些需要动态指定请求处理者或处理者职责可能发生变化的场合。
2.14 命令模式(Command Pattern)
2.14.1 定义
命令模式的核心思想是将“请求”封装为对象,并将请求的执行和请求的具体操作细节解耦。这样可以在不同的时间点、不同的环境下执行请求,还可以通过命令对象的统一接口来记录日志、撤销操作等。
2.14.2 结构
命令模式包含以下角色:
- 命令接口(Command): 声明执行命令的接口。
- 具体命令类(ConcreteCommand): 实现命令接口,定义请求的具体操作。
- 接收者(Receiver): 知道如何执行与请求相关的操作。
- 调用者(Invoker): 负责调用命令对象执行请求。它并不直接操作接收者。
- 客户端(Client): 创建具体的命令对象,并设置它们的接收者。
UML 类图
+-------------------+
| Command | <----- 命令接口
+-------------------+
| + Execute() |
+-------------------+
^
|
+-----------------------+ +----------------------+
| ConcreteCommand1 | | ConcreteCommand2 |
+-----------------------+ +----------------------+
| - receiver: Receiver | | - receiver: Receiver |
| + Execute() | | + Execute() |
+-----------------------+ +----------------------+
+-------------------+
| Receiver | <----- 接收者
+-------------------+
| + Action() |
+-------------------+
+-----------------------------+
| Invoker | <----- 调用者
+-----------------------------+
| - command: Command |
| + SetCommand(cmd: Command) |
| + ExecuteCommand() |
+-----------------------------+
2.14.3 示例代码
假设我们要实现一个简单的家电控制系统,可以用命令模式来设计将家电的操作(如开灯、关灯等)封装为命令对象。
命令接口
// 命令接口
public interface ICommand
{
void Execute();
void Undo();
}
接收者
// 接收者:灯
public class Light
{
public void TurnOn()
{
Console.WriteLine("The light is on");
}
public void TurnOff()
{
Console.WriteLine("The light is off");
}
}
具体命令类
// 具体命令类:打开灯
public class LightOnCommand : ICommand
{
private readonly Light _light;
public LightOnCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOn();
}
public void Undo()
{
_light.TurnOff(); // 撤销打开灯,实际上是关闭灯
}
}
// 具体命令类:关闭灯
public class LightOffCommand : ICommand
{
private readonly Light _light;
public LightOffCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOff();
}
public void Undo()
{
_light.TurnOn(); // 撤销关闭灯,实际上是打开灯
}
}
调用者
public class RemoteControl
{
private ICommand _command;
private readonly Stack<ICommand> _history = new Stack<ICommand>();
private readonly Stack<ICommand> _redoStack = new Stack<ICommand>();
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
_history.Push(_command); // 保存执行的命令
_redoStack.Clear(); // 清空恢复栈,因为有新操作了
}
public void PressUndo()
{
if (_history.Count > 0)
{
ICommand lastCommand = _history.Pop();
lastCommand.Undo();
_redoStack.Push(lastCommand); // 保存到恢复栈
}
}
public void PressRedo()
{
if (_redoStack.Count > 0)
{
ICommand lastUndoneCommand = _redoStack.Pop();
lastUndoneCommand.Execute();
_history.Push(lastUndoneCommand); // 重新保存到历史栈
}
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建接收者
Light livingRoomLight = new Light();
// 创建命令对象
ICommand lightOn = new LightOnCommand(livingRoomLight);
ICommand lightOff = new LightOffCommand(livingRoomLight);
// 创建调用者并设置命令
RemoteControl remote = new RemoteControl();
// 打开灯
remote.SetCommand(lightOn);
remote.PressButton();
// 关闭灯
remote.SetCommand(lightOff);
remote.PressButton();
// 撤销关闭灯操作(即再次打开灯)
remote.PressUndo();
// 撤销打开灯操作(即再次关闭灯)
remote.PressUndo();
// 恢复关闭灯操作(即再次打开灯)
remote.PressRedo();
}
}
运行结果
The light is on
The light is off
在这个例子中,LightOnCommand
和 LightOffCommand
这两个具体命令类封装了开灯和关灯的操作。RemoteControl
是调用者,通过调用 SetCommand
方法将具体命令对象传递给它,并通过 PressButton
方法执行命令。
2.14.4 特点
-
优点:
-
解耦请求发送者和接收者: 发送者只需要知道命令接口,而不需要了解具体实现。
-
支持撤销操作: 可以实现命令的撤销和恢复功能。
-
支持宏命令: 可以将多个命令组合成一个宏命令,顺序执行。
-
支持请求排队和日志记录: 通过保存命令对象,可以将请求保存到队列中或日志中,便于后续操作。
-
-
缺点:
-
类数量增加: 每个具体命令都需要定义一个类,可能导致类的数量增加。
-
系统复杂性增加: 封装请求为对象虽然增加了灵活性,但也增加了系统的复杂性。
-
2.14.5 适用场景
- 需要参数化对象的操作: 客户端可以在运行时选择并设置要执行的命令。
- 需要支持撤销和恢复操作: 系统需要支持撤销功能时,可以使用命令模式保存操作的历史记录。
- 需要在不同时间点执行操作: 可以将命令放入队列中,延迟执行或按顺序执行。
- 需要支持宏命令: 需要将多个操作组合成一个命令时,可以使用命令模式。
2.14.6 总结
命令模式通过将操作封装为独立的命令对象,实现了请求发送者与接收者的解耦。它为系统增加了灵活性,尤其是在支持撤销、恢复、宏命令和请求排队等功能时非常有用。然而,命令模式的使用也可能导致类的数量增加,系统的复杂性增加,因此在设计时需要权衡使用。
2.15 解释器模式(Interpreter Pattern)
2.15.1 定义
解释器模式通过定义一种语言的语法规则,使用这些规则解析和执行语言中的语句。这种模式通常用于那些具有简单语法和小型命令集的领域特定语言(DSL)中。
2.15.2 结构
解释器模式包含以下角色:
- 抽象表达式(AbstractExpression): 声明解释操作,接口或抽象类。
- 终结符表达式(TerminalExpression): 实现与语法规则相关的操作,处理语法中的终结符。
- 非终结符表达式(NonTerminalExpression): 表示语法规则中的非终结符,通常包含一个或多个表达式。
- 上下文(Context): 包含解释器需要的全局信息。
- 客户端(Client): 构建或解释语法规则,并使用解释器来解释语句。
UML 类图
+---------------------------------------+
| AbstractExpression | <----- 抽象表达式
+---------------------------------------+
| + Interpret(context: Context): void |
+---------------------------------------+
^
|
+---------------------------------------+ +---------------------------------------+
| TerminalExpression | | NonTerminalExpression |
+---------------------------------------+ +---------------------------------------+
| + Interpret(context: Context): void | | + Interpret(context: Context): void |
+---------------------------------------+ +---------------------------------------+
+-------------------+
| Context | <----- 上下文
+-------------------+
| + GetInfo(): ... |
+-------------------+
2.15.3 示例代码
假设我们要实现一个简单的数学表达式解释器,可以解析和计算简单的加法和减法运算表达式。
抽象表达式
// 抽象表达式
public interface IExpression
{
int Interpret();
}
终结符表达式
// 终结符表达式:数字
public class NumberExpression : IExpression
{
private readonly int _number;
public NumberExpression(int number)
{
_number = number;
}
public int Interpret()
{
return _number;
}
}
非终结符表达式
// 非终结符表达式:加法
public class AddExpression : IExpression
{
private readonly IExpression _leftExpression;
private readonly IExpression _rightExpression;
public AddExpression(IExpression leftExpression, IExpression rightExpression)
{
_leftExpression = leftExpression;
_rightExpression = rightExpression;
}
public int Interpret()
{
return _leftExpression.Interpret() + _rightExpression.Interpret();
}
}
// 非终结符表达式:减法
public class SubtractExpression : IExpression
{
private readonly IExpression _leftExpression;
private readonly IExpression _rightExpression;
public SubtractExpression(IExpression leftExpression, IExpression rightExpression)
{
_leftExpression = leftExpression;
_rightExpression = rightExpression;
}
public int Interpret()
{
return _leftExpression.Interpret() - _rightExpression.Interpret();
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 构造表达式:10 + 5 - 2
IExpression expression = new SubtractExpression(
new AddExpression(new NumberExpression(10), new NumberExpression(5)),
new NumberExpression(2)
);
// 解释并计算结果
int result = expression.Interpret();
Console.WriteLine($"Result: {result}");
}
}
运行结果
Result: 13
在这个例子中,表达式 10 + 5 - 2
被构造为一个解释器树,并通过调用 Interpret
方法递归地计算出结果。NumberExpression
是终结符表达式,用于表示具体的数字值,AddExpression
和 SubtractExpression
是非终结符表达式,用于表示加法和减法操作。
2.15.4 特点
-
优点:
-
灵活性高: 解释器模式使得设计自定义语言变得更加容易,通过组合不同的表达式类可以实现复杂的语法解析。
-
可扩展性好: 新的语法规则可以通过添加新的表达式类来实现,而不需要修改现有的系统。
-
-
缺点:
-
性能问题: 解释器模式适用于语法规则相对简单的场景。对于复杂的语法解析,由于要递归解析表达式树,可能会导致性能问题。
-
类的数量增加: 每个语法规则都需要一个类来实现,可能导致类的数量急剧增加,增加了系统的复杂性。
-
2.15.5 适用场景
- 简单的语言解释器: 如脚本语言解释器、配置文件解析器、规则引擎等。
- 编译器设计: 在编译器中使用解释器模式来解析和解释源代码。
- 复杂的数据解析: 需要解释和执行复杂的命令或数据时,使用解释器模式来解析并执行。
2.15.6 总结
解释器模式通过定义语言的语法规则,并使用这些规则解析和执行语句。它适用于简单的语法规则和小型语言解析任务,但不适用于复杂的语法解析和大规模系统。解释器模式的灵活性和扩展性使其在某些领域特定语言的实现中非常有用。
2.16 迭代器模式(Iterator Pattern)
2.16.1 定义
迭代器模式的核心思想是提供一种统一的接口来遍历聚合对象中的元素,而不需要了解聚合对象的内部结构。通过这种方式,集合和遍历算法之间解耦,遍历的方式可以更容易地改变或扩展。
2.16.2 结构
迭代器模式包含以下角色:
- 迭代器接口(Iterator): 定义访问和遍历元素的接口,通常包括
Next
、HasNext
、Current
等方法。 - 具体迭代器(ConcreteIterator): 实现迭代器接口,负责具体元素的遍历。
- 聚合接口(Aggregate): 定义创建迭代器对象的接口。
- 具体聚合类(ConcreteAggregate): 实现聚合接口,返回具体的迭代器实例。
UML 类图
+-------------------------------+ +-------------------+
| Aggregate | | Iterator |
+-------------------------------+ +-------------------+
| + CreateIterator(): Iterator | | + HasNext(): bool |
+-------------------------------+ | + Next(): T |
^ | + Current(): T |
| +-------------------+
+-------------------------------+
|ConcreteAggregate | +-------------------+
+-------------------------------+ | ConcreteIterator |
| + CreateIterator(): Iterator | | + HasNext(): bool |
+-------------------------------+ | + Next(): T |
| + Current(): T |
+-------------------+
2.16.3 示例代码
假设我们要实现一个自定义的 List
集合,并为它提供一个迭代器来遍历其中的元素。
迭代器接口
// 迭代器接口
public interface IIterator<T>
{
bool HasNext();
T Next();
T Current { get; }
}
具体迭代器
// 具体迭代器
public class ListIterator<T> : IIterator<T>
{
private readonly List<T> _list;
private int _position = 0;
public ListIterator(List<T> list)
{
_list = list;
}
public bool HasNext()
{
return _position < _list.Count;
}
public T Next()
{
return _list[_position++];
}
public T Current => _list[_position];
}
聚合接口
// 聚合接口
public interface IAggregate<T>
{
IIterator<T> CreateIterator();
}
具体聚合类
// 具体聚合类
public class CustomList<T> : IAggregate<T>
{
private readonly List<T> _items = new List<T>();
public void Add(T item)
{
_items.Add(item);
}
public IIterator<T> CreateIterator()
{
return new ListIterator<T>(_items);
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建聚合对象并添加元素
CustomList<string> list = new CustomList<string>();
list.Add("Item 1");
list.Add("Item 2");
list.Add("Item 3");
// 创建迭代器并遍历元素
IIterator<string> iterator = list.CreateIterator();
while (iterator.HasNext())
{
string item = iterator.Next();
Console.WriteLine(item);
}
}
}
运行结果
Item 1
Item 2
Item 3
在这个例子中,我们创建了一个自定义的 CustomList
类,并为其提供了 ListIterator
作为具体的迭代器。ListIterator
实现了遍历列表元素的逻辑。客户端代码通过迭代器接口来遍历 CustomList
中的元素,而无需了解 CustomList
的内部结构。
2.16.4 特点
-
优点:
-
简化聚合类: 迭代器模式将遍历的职责从聚合类中分离出来,简化了聚合类的实现。
-
一致的接口: 迭代器模式提供了一致的接口用于遍历不同类型的聚合对象,无需关心其内部实现。
-
灵活性高: 可以自由更改迭代算法而不影响聚合类。
-
-
缺点:
- 开销增加: 对于非常简单的聚合类,引入迭代器模式可能会导致额外的开销和复杂性。
2.16.5 适用场景
- 需要遍历聚合对象: 当需要遍历一个聚合对象中的元素时,可以使用迭代器模式。
- 不同的遍历方式: 需要多种遍历方式或需要隐藏遍历的实现细节时,迭代器模式是一个合适的选择。
- 统一的遍历接口: 当需要为不同类型的聚合对象提供统一的遍历接口时,迭代器模式非常适用。
2.16.6 总结
迭代器模式提供了一种遍历聚合对象的标准方法,通过解耦遍历逻辑和聚合对象的实现,增强了系统的灵活性和可扩展性。该模式特别适合需要在不暴露对象内部结构的情况下对对象进行遍历的场景。
2.17 中介者模式(Mediator Pattern)
2.17.1 定义
中介者模式将系统中多个对象之间复杂的交互和依赖关系抽象为一个中介者对象,各个对象不直接引用彼此,而是通过中介者进行通信。这样做可以减少对象之间的直接依赖,从而使系统更加易于维护和扩展。
2.17.2 结构
中介者模式包含以下角色:
- 中介者接口(Mediator): 定义一个接口用于与各同事对象通信。
- 具体中介者(ConcreteMediator): 实现中介者接口,协调各同事对象之间的交互。
- 同事类(Colleague): 每个同事类都知道中介者对象,并且在需要与其他同事通信时,都会通过中介者。
UML 类图
+---------------------------------------------------+
| Mediator |
+---------------------------------------------------+
| + Notify(sender: Colleague, event: string): void |
+---------------------------------------------------+
^
|
+---------------------------------------------------+
|ConcreteMediator |
+---------------------------------------------------+
| + Notify(sender: Colleague, event: string): void |
| + RegisterColleague(colleague: Colleague): void |
+---------------------------------------------------+
+-------------------------------------------+
| Colleague |
+-------------------------------------------+
| + SetMediator(mediator: Mediator): void |
| + Send(event: string): void |
+-------------------------------------------+
^ ^
| |
+-------------------------------+ +-------------------------------+
| ColleagueA | | ColleagueB |
+-------------------------------+ +-------------------------------+
| + Send(event: string): void | | + Send(event: string): void |
+-------------------------------+ +-------------------------------+
2.17.3 示例代码
假设我们要实现一个聊天室系统,其中用户可以通过聊天室中介者互相发送消息。
中介者接口
// 中介者接口
public interface IChatMediator
{
void SendMessage(string message, User user);
void RegisterUser(User user);
}
具体中介者
// 具体中介者
public class ChatMediator : IChatMediator
{
private readonly List<User> _users = new List<User>();
public void RegisterUser(User user)
{
_users.Add(user);
}
public void SendMessage(string message, User sender)
{
foreach (var user in _users)
{
// 不要发给自己
if (user != sender)
{
user.Receive(message);
}
}
}
}
同事类
// 同事类
public abstract class User
{
protected IChatMediator _mediator;
protected string _name;
public User(IChatMediator mediator, string name)
{
_mediator = mediator;
_name = name;
}
public abstract void Send(string message);
public abstract void Receive(string message);
}
// 具体同事类
public class ConcreteUser : User
{
public ConcreteUser(IChatMediator mediator, string name) : base(mediator, name)
{
}
public override void Send(string message)
{
Console.WriteLine($"{_name} sends: {message}");
_mediator.SendMessage(message, this);
}
public override void Receive(string message)
{
Console.WriteLine($"{_name} receives: {message}");
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
IChatMediator chatMediator = new ChatMediator();
User user1 = new ConcreteUser(chatMediator, "User1");
User user2 = new ConcreteUser(chatMediator, "User2");
User user3 = new ConcreteUser(chatMediator, "User3");
chatMediator.RegisterUser(user1);
chatMediator.RegisterUser(user2);
chatMediator.RegisterUser(user3);
user1.Send("Hello, everyone!");
}
}
运行结果
User1 sends: Hello, everyone!
User2 receives: Hello, everyone!
User3 receives: Hello, everyone!
在这个例子中,ChatMediator
是中介者,负责协调 User
对象之间的通信。用户通过调用中介者的 SendMessage
方法来发送消息,中介者会将消息转发给其他用户。
2.17.4 特点
-
优点:
-
降低耦合度: 中介者模式减少了对象之间的直接依赖,各个对象不需要知道其他对象的存在,只需要与中介者交互。
-
增强可维护性: 对象之间的交互逻辑集中在中介者中,使得修改交互逻辑变得更加容易,而无需修改各个对象。
-
易于扩展: 可以通过增加新的中介者或同事类来扩展系统,而不会影响现有代码。
-
-
缺点:
-
中介者复杂性: 随着系统的复杂度增加,中介者可能变得庞大且复杂,难以维护。
-
可能引入单点故障: 中介者作为通信的中心,如果中介者失败,整个系统的通信可能会中断。
-
2.17.5 适用场景
- 复杂的对象交互: 当对象之间存在复杂的交互逻辑且相互依赖时,中介者模式可以简化这些交互。
- 系统易于扩展: 当需要在系统中添加新的交互对象而不希望影响现有代码时,中介者模式非常合适。
- 分离系统的职责: 中介者模式可以帮助将不同对象的职责清晰地分离开来,从而减少耦合。
2.17.6 总结
中介者模式通过引入中介者对象来协调多个对象之间的交互,减少了对象之间的耦合度,并使系统更具可维护性和可扩展性。尽管中介者模式能够简化对象的交互逻辑,但需要注意中介者对象的复杂性管理,以避免其变得过于庞大。
2.18 备忘录模式(Memento Pattern)
2.18.1 定义
备忘录模式的核心思想是将对象的状态保存在一个备忘录对象中,并允许在未来的某个时刻恢复该状态。备忘录模式保证了状态的封装性,外部对象无法直接访问备忘录中的内容,从而保护了原发器对象的内部细节。
2.18.2 结构
备忘录模式包含以下角色:
- 原发器(Originator): 创建备忘录以记录当前状态,并可以使用备忘录恢复先前状态。
- 备忘录(Memento): 存储原发器的内部状态,备忘录对外部是不可见的,只能由原发器访问。
- 负责人(Caretaker): 负责存储和管理备忘录,但不会修改或检查备忘录的内容。
UML 类图
+---------------------------------------+
| Originator |
+---------------------------------------+
| - state: String |
| + CreateMemento(): Memento |
| + SetMemento(memento: Memento): void |
+---------------------------------------+
|
|
+-----------------------+ +-----------------------+
| Memento | | Caretaker |
+-----------------------+ +-----------------------+
| - state: String | | - memento: Memento |
| + GetState(): String | +-----------------------+
+-----------------------+
2.18.3 示例代码
假设我们要实现一个简单的文本编辑器,它能够保存文本的状态,并在需要时撤销或恢复状态。
原发器
// 原发器类
public class TextEditor
{
private string _text;
public void SetText(string text)
{
_text = text;
}
public string GetText()
{
return _text;
}
public Memento CreateMemento()
{
return new Memento(_text);
}
public void RestoreMemento(Memento memento)
{
_text = memento.GetState();
}
}
备忘录
// 备忘录类
public class Memento
{
private readonly string _state;
public Memento(string state)
{
_state = state;
}
public string GetState()
{
return _state;
}
}
负责人
// 负责人类
public class Caretaker
{
private readonly Stack<Memento> _mementos = new Stack<Memento>();
public void SaveMemento(Memento memento)
{
_mementos.Push(memento);
}
public Memento GetMemento()
{
if (_mementos.Count > 0)
{
return _mementos.Pop();
}
return null;
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
TextEditor editor = new TextEditor();
Caretaker caretaker = new Caretaker();
editor.SetText("Version 1");
caretaker.SaveMemento(editor.CreateMemento());
editor.SetText("Version 2");
caretaker.SaveMemento(editor.CreateMemento());
editor.SetText("Version 3");
Console.WriteLine("Current Text: " + editor.GetText());
editor.RestoreMemento(caretaker.GetMemento());
Console.WriteLine("Restored Text: " + editor.GetText());
editor.RestoreMemento(caretaker.GetMemento());
Console.WriteLine("Restored Text: " + editor.GetText());
}
}
运行结果
Current Text: Version 3
Restored Text: Version 2
Restored Text: Version 1
在这个例子中,TextEditor
是原发器,负责创建和恢复文本的状态;Memento
是备忘录,保存文本的状态;Caretaker
是负责人,管理备忘录的存储和恢复。在运行过程中,我们保存了多个文本状态,并通过恢复操作撤销了修改,返回到之前的状态。
2.18.4 特点
-
优点:
-
封装性: 备忘录模式保证了原发器状态的封装性,外部对象无法直接访问备忘录的内容。
-
状态恢复: 允许对象恢复到之前的状态,提供了实现“撤销/恢复”功能的简单方法。
-
简化复杂性: 通过将状态的存储和恢复职责分离到不同的类中,简化了复杂系统的管理。
-
-
缺点:
-
开销大: 如果原发器的状态占用大量资源(如内存),备忘录模式可能会导致开销较大,尤其是在频繁保存和恢复状态时。
-
管理复杂: 如果备忘录数量众多,管理这些备忘录可能变得复杂,尤其是在涉及多线程操作时。
-
2.18.5 适用场景
- 实现撤销/恢复功能: 当需要实现“撤销/恢复”操作时,备忘录模式非常适用。
- 需要记录历史状态: 在需要保存和恢复对象的多个历史状态的场景中,可以使用备忘录模式。
- 简化复杂对象的状态管理: 在复杂对象状态变化频繁且需要保证状态一致性的情况下,备忘录模式提供了一种有效的管理方式。
2.18.6 总结
备忘录模式通过保存对象的状态,提供了恢复该状态的机制。它通过封装状态,确保了对象内部细节的保护,同时又允许状态的恢复。该模式非常适合用于实现“撤销/恢复”功能,并在需要管理复杂状态变更的场景中提供了很好的解决方案。
2.19 观察者模式(Observer Pattern)
2.19.1 定义
观察者模式的核心思想是当一个对象(被观察者)的状态改变时,所有依赖于它的对象(观察者)都会被通知并更新。这样一来,观察者模式实现了对象之间的松散耦合,使得一个对象的变化可以自动地传播到相关的对象。
2.19.2 结构
观察者模式包含以下角色:
- 主题(Subject): 被观察的对象,维护着一组观察者对象的引用,提供注册、移除观察者的接口,并在状态发生变化时通知所有观察者。
- 观察者(Observer): 定义一个更新接口,当收到通知时进行相应的更新操作。
- 具体主题(ConcreteSubject): 具体的被观察对象,通常包含状态,当状态发生变化时,通知所有已注册的观察者。
- 具体观察者(ConcreteObserver): 实现观察者接口,负责在状态变化时更新自身。
UML 类图
+---------------------------+ +-------------------+
| Subject | <------ | Observer |
+---------------------------+ +-------------------+
| + Attach(obs: Observer) | | + Update(): void |
| + Detach(obs: Observer) | +-------------------+
| + Notify(): void | ^
+---------------------------+ |
^ |
| |
+-----------------------+ +-------------------+
| ConcreteSubject | | ConcreteObserver |
+-----------------------+ +-------------------+
| - state: State | | - state: State |
| + GetState(): State | | + Update(): void |
| + SetState(State) | +-------------------+
+-----------------------+
2.19.3 示例代码
假设我们要实现一个天气站系统,天气站会记录当前的天气信息,并通知注册的显示设备(如手机应用、网站等)进行更新。
观察者接口
// 观察者接口
public interface IObserver
{
void Update(string temperature, string humidity, string pressure);
}
主题接口
// 主题接口
public interface ISubject
{
void RegisterObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers();
}
具体主题
// 具体主题
public class WeatherStation : ISubject
{
private List<IObserver> _observers;
private string _temperature;
private string _humidity;
private string _pressure;
public WeatherStation()
{
_observers = new List<IObserver>();
}
public void RegisterObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(_temperature, _humidity, _pressure);
}
}
public void SetMeasurements(string temperature, string humidity, string pressure)
{
_temperature = temperature;
_humidity = humidity;
_pressure = pressure;
NotifyObservers();
}
}
具体观察者
// 具体观察者
public class PhoneDisplay : IObserver
{
private string _temperature;
private string _humidity;
private string _pressure;
public void Update(string temperature, string humidity, string pressure)
{
_temperature = temperature;
_humidity = humidity;
_pressure = pressure;
Display();
}
public void Display()
{
Console.WriteLine($"Phone Display -> Temperature: {_temperature}, Humidity: {_humidity}, Pressure: {_pressure}");
}
}
public class WebDisplay : IObserver
{
private string _temperature;
private string _humidity;
private string _pressure;
public void Update(string temperature, string humidity, string pressure)
{
_temperature = temperature;
_humidity = humidity;
_pressure = pressure;
Display();
}
public void Display()
{
Console.WriteLine($"Web Display -> Temperature: {_temperature}, Humidity: {_humidity}, Pressure: {_pressure}");
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
WeatherStation weatherStation = new WeatherStation();
IObserver phoneDisplay = new PhoneDisplay();
IObserver webDisplay = new WebDisplay();
weatherStation.RegisterObserver(phoneDisplay);
weatherStation.RegisterObserver(webDisplay);
weatherStation.SetMeasurements("30°C", "65%", "1013 hPa");
weatherStation.RemoveObserver(phoneDisplay);
weatherStation.SetMeasurements("28°C", "70%", "1012 hPa");
}
}
运行结果
Phone Display -> Temperature: 30°C, Humidity: 65%, Pressure: 1013 hPa
Web Display -> Temperature: 30°C, Humidity: 65%, Pressure: 1013 hPa
Web Display -> Temperature: 28°C, Humidity: 70%, Pressure: 1012 hPa
在这个例子中,WeatherStation
是具体的主题,当天气数据发生变化时,它通知所有注册的观察者(如 PhoneDisplay
和 WebDisplay
)进行更新并显示新的数据。
2.19.4 特点
-
优点:
-
松散耦合: 观察者和主题之间是松散耦合的,观察者可以独立于主题的变化而变化,增加了系统的灵活性。
-
动态更新: 观察者模式使得对象之间的通信更加灵活,可以动态添加或删除观察者,实时更新数据。
-
符合开放-封闭原则: 可以在不修改现有代码的情况下,增加新的观察者。
-
-
缺点:
-
通知开销: 如果有大量的观察者,通知所有观察者可能会引起开销,影响性能。
-
可能出现循环依赖: 如果观察者之间也相互依赖,可能会导致循环依赖问题,影响系统的稳定性。
-
2.19.5 适用场景
- 事件处理系统: 当需要对某个事件发生时,触发多个对象的响应时,观察者模式非常适用。
- 数据模型与视图同步: 在模型-视图架构中,当模型的数据变化时,需要通知视图更新显示,可以使用观察者模式。
- 广播通信: 当一个对象的状态改变需要通知多个对象时,可以使用观察者模式。
2.19.6 总结
观察者模式通过定义一对多的依赖关系,实现了对象间的松散耦合和动态通信。它允许对象自动通知相关的依赖对象并更新状态,非常适合用于事件驱动的系统和需要动态更新的场景。尽管可能会带来一定的通知开销和复杂性管理,但它依然是实现对象间动态通信的强大工具。
2.20 状态模式(State Pattern)
2.20.1 定义
状态模式的核心思想是将与状态相关的行为封装在独立的状态对象中,并通过状态对象来管理对象的状态转换。这样,原始对象在其状态发生变化时,会自动切换到对应的状态对象,从而表现出不同的行为。
2.20.2 结构
状态模式包含以下角色:
- 上下文(Context): 维护一个当前状态,并在状态发生变化时,切换到新的状态对象。上下文对象向客户端暴露接口,但行为的实现由状态对象负责。
- 状态接口(State): 定义状态的接口,声明在该状态下对象可以执行的行为。
- 具体状态(ConcreteState): 实现状态接口,定义该状态下的具体行为。当上下文处于此状态时,行为由具体状态对象执行。
UML 类图
+-------------------+ +-----------------------------------+
| Context | | State |
+-------------------+ +-----------------------------------+
| - state: State |<------| + Handle(context: Context): void |
| + Request(): void | +-----------------------------------+
+-------------------+ ^ ^
| |
| |
+-----------------------------------+ +-----------------------------------+
| ConcreteStateA | | ConcreteStateB |
+-----------------------------------+ +-----------------------------------+
| + Handle(context: Context): void | | + Handle(context: Context): void |
+-----------------------------------+ +-----------------------------------+
2.20.3 示例代码
假设我们要实现一个简单的电灯开关系统,电灯可以处于“开”和“关”两种状态,并且根据当前的状态来执行不同的操作。
状态接口
// 状态接口
public interface IState
{
void Handle(Context context);
}
具体状态类
// 具体状态 - 开灯状态
public class OnState : IState
{
public void Handle(Context context)
{
Console.WriteLine("The light is already ON.");
context.SetState(new OffState());
}
}
// 具体状态 - 关灯状态
public class OffState : IState
{
public void Handle(Context context)
{
Console.WriteLine("The light is OFF. Turning it ON.");
context.SetState(new OnState());
}
}
上下文类
// 上下文类
public class Context
{
private IState _state;
public Context(IState state)
{
_state = state;
}
public void SetState(IState state)
{
_state = state;
}
public void Request()
{
_state.Handle(this);
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
Context context = new Context(new OffState());
// 初始状态为关灯状态
context.Request(); // 关灯 -> 开灯
// 再次请求
context.Request(); // 开灯 -> 关灯
// 再次请求
context.Request(); // 关灯 -> 开灯
}
}
运行结果
The light is OFF. Turning it ON.
The light is already ON.
The light is OFF. Turning it ON.
在这个例子中,Context
类维护一个当前状态,当调用 Request()
方法时,会根据当前状态执行相应的操作并切换到下一个状态。OnState
和 OffState
是两种具体的状态类,分别定义了在不同状态下的行为。
2.20.4 特点
-
优点:
-
减少复杂性: 通过将状态相关的行为封装在独立的状态对象中,状态模式消除了大量的条件分支语句,使代码更加清晰和易于维护。
-
状态转换灵活: 可以很容易地添加、删除或修改状态对象,扩展系统的功能,而无需修改上下文类。
-
符合单一职责原则: 状态模式将与状态相关的行为封装在不同的状态类中,使得每个类只负责一种状态的行为,简化了代码的管理。
-
-
缺点:
-
增加类的数量: 每个状态都需要定义一个具体的状态类,当状态过多时,可能会导致类的数量急剧增加,增加系统的复杂性。
-
状态之间的依赖: 如果状态之间存在复杂的依赖关系,可能会导致状态之间的转换逻辑变得复杂,难以维护。
-
2.20.5 适用场景
- 对象的行为依赖于其状态: 当一个对象的行为依赖于其状态,并且它的状态会在运行时改变时,状态模式非常适用。
- 状态逻辑复杂: 当状态转换的逻辑非常复杂,或者状态之间的转换规则经常变化时,状态模式能够有效地管理这些逻辑。
- 消除条件分支: 如果代码中存在大量的条件分支语句来处理不同的状态,状态模式可以通过将状态逻辑分散到不同的状态类中来简化代码。
2.20.6 总结
状态模式通过将状态相关的行为封装在独立的状态对象中,简化了对象的状态管理逻辑。它消除了大量的条件分支语句,使代码更加清晰和易于扩展。尽管状态模式可能会增加类的数量,但它为管理复杂的状态转换逻辑提供了一种灵活且有效的解决方案。
2.21 策略模式(Strategy Pattern)
2.21.1 定义
策略模式的核心思想是将不同的算法或行为封装到独立的策略类中,并通过上下文类来管理和使用这些策略。客户端可以通过上下文类来动态选择使用哪种策略,而无需关心策略的具体实现细节。
2.21.2 结构
策略模式包含以下角色:
- 上下文(Context): 维护一个策略对象的引用,供客户端使用。上下文不实现算法,而是将算法的实现委托给策略对象。
- 策略接口(Strategy): 定义一组算法的通用接口,所有具体策略类都实现这个接口。
- 具体策略(ConcreteStrategy): 实现策略接口,定义具体的算法或行为。
UML 类图
+---------------------------+ +-----------------------+
| Context | | Strategy |
+---------------------------+ +-----------------------+
| - strategy: Strategy | | + Algorithm(): void |
| + SetStrategy(Strategy) | +-----------------------+
| + ExecuteStrategy(): void | ^ ^
+---------------------------+ | |
| |
| |
+-----------------------+ +-----------------------+
| ConcreteStrategyA | | ConcreteStrategyB |
+-----------------------+ +-----------------------+
| + Algorithm(): void | | + Algorithm(): void |
+-----------------------+ +-----------------------+
2.21.3 示例代码
假设我们要实现一个简单的支付系统,它支持多种支付方式,如信用卡支付、PayPal支付和比特币支付。我们可以使用策略模式来封装这些支付方式,并让客户端在运行时选择不同的支付策略。
策略接口
// 策略接口
public interface IPaymentStrategy
{
void Pay(double amount);
}
具体策略类
// 具体策略 - 信用卡支付
public class CreditCardPayment : IPaymentStrategy
{
private string _cardNumber;
public CreditCardPayment(string cardNumber)
{
_cardNumber = cardNumber;
}
public void Pay(double amount)
{
Console.WriteLine($"Paid {amount} using Credit Card {_cardNumber}.");
}
}
// 具体策略 - PayPal支付
public class PayPalPayment : IPaymentStrategy
{
private string _email;
public PayPalPayment(string email)
{
_email = email;
}
public void Pay(double amount)
{
Console.WriteLine($"Paid {amount} using PayPal account {_email}.");
}
}
// 具体策略 - 比特币支付
public class BitcoinPayment : IPaymentStrategy
{
private string _walletAddress;
public BitcoinPayment(string walletAddress)
{
_walletAddress = walletAddress;
}
public void Pay(double amount)
{
Console.WriteLine($"Paid {amount} using Bitcoin wallet {_walletAddress}.");
}
}
上下文类
// 上下文类
public class PaymentContext
{
private IPaymentStrategy _paymentStrategy;
public void SetPaymentStrategy(IPaymentStrategy paymentStrategy)
{
_paymentStrategy = paymentStrategy;
}
public void Pay(double amount)
{
_paymentStrategy.Pay(amount);
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
PaymentContext context = new PaymentContext();
// 使用信用卡支付
context.SetPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
context.Pay(100.0);
// 使用PayPal支付
context.SetPaymentStrategy(new PayPalPayment("user@example.com"));
context.Pay(200.0);
// 使用比特币支付
context.SetPaymentStrategy(new BitcoinPayment("1BitcoinAddressXYZ"));
context.Pay(300.0);
}
}
运行结果
Paid 100 using Credit Card 1234-5678-9012-3456.
Paid 200 using PayPal account user@example.com.
Paid 300 using Bitcoin wallet 1BitcoinAddressXYZ.
在这个例子中,PaymentContext
是上下文类,它持有一个 IPaymentStrategy
策略接口的引用。客户端可以动态设置不同的支付策略,如 CreditCardPayment
、PayPalPayment
和 BitcoinPayment
,并通过 Pay()
方法执行支付操作。这样,支付方式的变化不会影响客户端代码。
2.21.4 特点
-
优点:
-
算法的灵活性: 策略模式允许在运行时选择不同的算法或行为,增加了系统的灵活性和扩展性。
-
避免使用条件语句: 通过将不同的算法封装在独立的策略类中,避免了在客户端代码中使用大量的条件分支语句。
-
遵循开放-封闭原则: 可以在不修改现有代码的情况下,通过添加新的策略类来扩展系统的功能。
-
-
缺点:
-
增加类的数量: 每个策略都需要一个具体的策略类,可能会导致类的数量增加,增加系统的复杂性。
-
策略选择的复杂性: 在一些情况下,策略的选择逻辑可能本身就比较复杂,如何选择合适的策略可能会成为一个挑战。
-
2.21.5 适用场景
- 多种算法需要互换: 当一个系统有多个相似的算法或行为,并且这些算法或行为需要在不同情况下互换使用时,策略模式非常适用。
- 消除条件分支语句: 如果代码中存在大量的条件分支语句来选择不同的算法或行为,策略模式可以通过将这些算法封装到独立的策略类中来简化代码。
- 算法的频繁变化: 当算法或行为经常发生变化时,可以使用策略模式,使得每种算法封装在独立的策略类中,便于维护和扩展。
2.21.6 总结
策略模式通过将不同的算法封装到独立的策略类中,实现了算法的灵活互换。它消除了大量的条件分支语句,使代码更加清晰和可扩展。尽管策略模式可能会增加类的数量,但它为系统的算法选择和扩展提供了一种灵活且强大的解决方案。在需要灵活选择算法或行为的场景中,策略模式是一种非常有效的设计模式。
2.22 模板方法模式(Template Method Pattern)
2.22.1 定义
模板方法模式通过在基类中定义一个模板方法,该方法封装了一个算法的固定步骤,然后允许子类实现或重写这些步骤。这样,子类可以定制算法的具体行为,而无需改变算法的整体结构。
2.22.2 结构
模板方法模式包含以下角色:
- 抽象类(AbstractClass): 定义模板方法和算法的骨架。模板方法按照固定的步骤调用抽象方法或具体方法。
- 具体类(ConcreteClass): 继承自抽象类,实现或重写抽象方法,定义算法的具体步骤。
UML 类图
+---------------------------+
| AbstractClass |
+---------------------------+
| + TemplateMethod(): void |
| + Step1(): void |
| + Step2(): void |
| - Step3(): void |
+---------------------------+
^
|
+-------------------+
| ConcreteClass |
+-------------------+
| - Step3(): void |
+-------------------+
2.22.3 示例代码
假设我们要制作一杯饮料,制作的过程包括煮水、冲泡、倒入杯中、添加配料等步骤。咖啡和茶是两种不同的饮料,它们在制作过程中的步骤基本相同,但在某些步骤上有所不同。我们可以使用模板方法模式来实现这个场景。
抽象类
// 抽象类 - 饮料制作过程
public abstract class Beverage
{
// 模板方法
public void PrepareRecipe()
{
BoilWater();
Brew();
PourInCup();
AddCondiments();
}
// 具体方法 - 煮水
private void BoilWater()
{
Console.WriteLine("Boiling water");
}
// 抽象方法 - 冲泡
protected abstract void Brew();
// 具体方法 - 倒入杯中
private void PourInCup()
{
Console.WriteLine("Pouring into cup");
}
// 抽象方法 - 添加配料
protected abstract void AddCondiments();
}
具体类
// 具体类 - 茶
public class Tea : Beverage
{
protected override void Brew()
{
Console.WriteLine("Steeping the tea");
}
protected override void AddCondiments()
{
Console.WriteLine("Adding lemon");
}
}
// 具体类 - 咖啡
public class Coffee : Beverage
{
protected override void Brew()
{
Console.WriteLine("Dripping coffee through filter");
}
protected override void AddCondiments()
{
Console.WriteLine("Adding sugar and milk");
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
Beverage tea = new Tea();
tea.PrepareRecipe(); // 制作茶
Console.WriteLine();
Beverage coffee = new Coffee();
coffee.PrepareRecipe(); // 制作咖啡
}
}
运行结果
Boiling water
Steeping the tea
Pouring into cup
Adding lemon
Boiling water
Dripping coffee through filter
Pouring into cup
Adding sugar and milk
在这个例子中,Beverage
是抽象类,定义了一个模板方法 PrepareRecipe()
,它包含了制作饮料的固定步骤。这些步骤中,有些是具体实现的(如 BoilWater()
和 PourInCup()
),而有些是抽象方法,由子类 Tea
和 Coffee
来实现(如 Brew()
和 AddCondiments()
)。客户端代码可以使用不同的具体类来制作不同的饮料,而不需要关心具体的实现细节。
2.22.4 特点
-
优点:
-
代码复用: 将通用的算法步骤封装到基类中,子类只需要实现差异化的部分,减少了重复代码。
-
扩展性强: 子类可以根据需要重写或扩展某些步骤,增加算法的灵活性。
-
控制算法结构: 父类定义了算法的骨架,确保了算法的整体结构不被子类破坏。
-
-
缺点:
-
增加代码复杂性: 由于引入了继承关系和抽象方法,可能会使代码结构变得复杂。
-
对子类的依赖: 父类依赖子类来实现某些步骤,可能导致子类必须实现某些方法,即使这些方法在特定情况下并不需要。
-
2.22.5 适用场景
- 多个类有相似的操作步骤: 当多个类的操作步骤大致相同时,可以使用模板方法模式将相同的部分提取到抽象类中。
- 算法需要多个步骤: 当算法需要分解为多个步骤,并且这些步骤中有些是固定的,有些是可变的,可以使用模板方法模式。
- 控制算法的流程: 当需要确保算法的某些步骤必须按照一定的顺序执行时,可以使用模板方法模式。
2.22.6 总结
模板方法模式通过将通用的算法步骤封装到抽象类中,允许子类重写或扩展特定的步骤,实现了算法的复用和扩展。它确保了算法的整体结构不被破坏,同时为子类提供了灵活性。在多个类具有相似的操作步骤时,模板方法模式是一种非常有效的设计模式。
2.23 访问者模式(Visitor Pattern)
2.23.1 定义
访问者模式通过引入一个访问者接口,使得你可以在元素类中接受访问者,并让访问者决定对元素的具体操作。访问者模式的关键在于分离算法和数据结构,使得新的操作可以轻松地添加而不影响已有的数据结构。
2.23.2 结构
访问者模式包含以下角色:
- 访问者接口(Visitor): 为每种元素类型定义一个访问方法。访问者接口通常提供一个
Visit
方法,针对不同的元素类有不同的实现。 - 具体访问者(ConcreteVisitor): 实现访问者接口的具体操作,对元素执行具体的操作。
- 元素接口(Element): 声明一个
Accept
方法,该方法接受访问者对象并调用访问者的Visit
方法。 - 具体元素(ConcreteElement): 实现元素接口,定义元素的具体行为,并在
Accept
方法中调用访问者的对应方法。 - 对象结构(ObjectStructure): 通常是一个包含多个不同类型元素的集合,它可以遍历这些元素并让访问者访问它们。
UML 类图
+---------------------------+ +---------------------------+
| Visitor | <------ | Element |
+---------------------------+ +---------------------------+
| + VisitElementA():void | | + Accept(v:Visitor): void |
| + VisitElementB():void | +---------------------------+
+---------------------------+ ^
^ |
| |
+---------------------------+ +---------------------------+
| ConcreteVisitor | | ConcreteElement |
+---------------------------+ +---------------------------+
| + VisitElementA():void | | + Accept(v:Visitor): void |
| + VisitElementB():void | | + OperationA(): void |
+---------------------------+ +---------------------------+
2.23.3 示例代码
假设我们要实现一个报表系统,系统中包含不同类型的员工(如工程师和经理),每种员工有不同的报表要求。我们可以使用访问者模式来实现报表的生成,使得报表的生成与员工类型的实现分离。
访问者接口
// 访问者接口
public interface IVisitor
{
void Visit(Engineer engineer);
void Visit(Manager manager);
}
具体访问者
// 具体访问者 - 报表生成器
public class ReportGenerator : IVisitor
{
public void Visit(Engineer engineer)
{
Console.WriteLine($"Generating report for Engineer: {engineer.Name}");
}
public void Visit(Manager manager)
{
Console.WriteLine($"Generating report for Manager: {manager.Name} with {manager.SubordinatesCount} subordinates.");
}
}
元素接口
// 元素接口
public interface IEmployee
{
void Accept(IVisitor visitor);
}
具体元素类
// 具体元素 - 工程师
public class Engineer : IEmployee
{
public string Name { get; private set; }
public Engineer(string name)
{
Name = name;
}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
// 具体元素 - 经理
public class Manager : IEmployee
{
public string Name { get; private set; }
public int SubordinatesCount { get; private set; }
public Manager(string name, int subordinatesCount)
{
Name = name;
SubordinatesCount = subordinatesCount;
}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
对象结构
// 对象结构 - 员工列表
public class EmployeeStructure
{
private List<IEmployee> _employees = new List<IEmployee>();
public void Attach(IEmployee employee)
{
_employees.Add(employee);
}
public void Detach(IEmployee employee)
{
_employees.Remove(employee);
}
public void Accept(IVisitor visitor)
{
foreach (var employee in _employees)
{
employee.Accept(visitor);
}
}
}
客户端代码
class Program
{
static void Main(string[] args)
{
// 创建员工结构
EmployeeStructure employeeStructure = new EmployeeStructure();
// 添加员工
employeeStructure.Attach(new Engineer("John"));
employeeStructure.Attach(new Manager("Alice", 5));
// 创建报表生成器
ReportGenerator reportGenerator = new ReportGenerator();
// 生成报表
employeeStructure.Accept(reportGenerator);
}
}
运行结果
Generating report for Engineer: John
Generating report for Manager: Alice with 5 subordinates.
在这个例子中,IVisitor
定义了对不同员工类型(Engineer
和 Manager
)的访问方法。ReportGenerator
是具体的访问者,实现了生成报表的逻辑。IEmployee
接口定义了 Accept
方法,Engineer
和 Manager
作为具体的元素,实现了接受访问者的逻辑。EmployeeStructure
作为对象结构,管理了所有的员工,并允许访问者访问这些员工。
2.23.4 特点
-
优点:
-
增加新的操作容易: 可以在不修改元素类的情况下,通过添加新的访问者类来增加新的操作。
-
将操作与对象结构分离: 访问者模式将数据结构和操作分离,使得数据结构和操作各自独立,符合单一职责原则。
-
扩展性好: 可以很容易地增加新的访问者来实现新的功能。
-
-
缺点:
-
增加元素类的复杂性: 每个元素类都必须实现接受访问者的方法,这可能会增加类的复杂性。
-
违反开闭原则: 如果需要修改元素的结构或添加新的元素类型,则需要修改所有的访问者类,这与开闭原则相违背。
-
双分派: 访问者模式要求进行双分派,即根据元素类型和访问者类型分别调用相应的方法,这可能会导致系统的复杂性增加。
-
2.23.5 适用场景
- 对象结构稳定: 当对象结构相对稳定,但操作经常变化时,使用访问者模式可以有效地管理这些变化。
- 需要对对象结构中的对象进行复杂操作: 访问者模式适用于对对象结构中的元素进行复杂操作,且这些操作可能会频繁变化的场景。
- 对象结构中包含多个不相关的类: 当对象结构中包含多个不相关的类,需要对这些类执行某些操作时,访问者模式可以通过统一的访问者接口来处理这些操作。
2.23.6 总结
访问者模式通过将操作分离到独立的访问者对象中,使得在不修改元素类的情况下,可以增加新的操作。它适用于对象结构稳定但操作经常变化的场景。然而,由于需要对每个元素类增加接受访问者的方法,并且可能导致违反开闭原则,因此在使用时需要权衡利弊。在需要对复杂对象结构进行扩展和管理时,访问者模式是一种强大的设计模式。