自述
资料
设计模式是什么?
设计模式是软件设计中常见问题的典型解决方案。它们更像"可调整的预制蓝图",用于解决代码中反复出现的设计问题。
设计模式与方法或库的使用方式不同,你很难直接在自己的程序中"套用一段固定代码"。模式不是一段特定代码,而是解决特定问题的通用结构/思路;你需要结合自己的上下文实现。
人们常常会混淆模式和算法,因为两者都是"特定问题的典型解决方案"。但算法更强调"明确步骤",而模式更强调"结构与角色关系"。
- 算法更像菜谱:提供达成目标的明确步骤。
- 模式更像蓝图:你能看到结构与目的,但实现步骤需要自己决定。
大部分模式都有相对固定的描述方式,通常包括:
- 意图:要解决什么问题、核心目标是什么。
- 动机:问题是怎么产生的,为什么需要该模式。
- 结构:参与角色与关系(类图/对象关系)。
- 实现:在不同语言中的示例实现与变体。
学习路线(建议先学哪些)
你不需要一口气把 GoF 23 个全背完。更可行的方式是:先掌握"高频 + 容易落地"的 8~10 个模式,再通过一个小项目把它们串起来。
1) 前期优先(8~10 个高频)
- 创建型:Factory Method(工厂方法)、Abstract Factory(抽象工厂)
- 结构型:Adapter(适配器)、Facade(门面/外观)、Decorator(装饰器)
- 行为型:Strategy(策略)、Template Method(模板方法)、Observer(观察者/事件)
- 可选:Command(命令)、State(状态)
Singleton(单例)很常见,但也最容易被滥用;建议把它当成"工具",而不是"架构核心"。
2) 比模式更重要的两条主线
- SOLID(尤其 SRP、OCP、DIP)
- 组合优于继承 + 依赖注入(DI):把"变化"放到可替换的依赖里,而不是把变化写死在继承层级里
3) 用一个贯穿案例来学(推荐:多协议多PLC / HSL 封装)
目标不是"把所有协议都做完",而是先做一个最小闭环:
- 支持 1~2 种协议(例如 ModbusTcp + S7)
- 统一对外 API:Connect / Read / Write
- 可插拔扩展:新增协议不改业务代码
你会自然用上:
- Strategy:按协议切换驱动实现
- Adapter:把 HSL 不同类的接口"适配"成统一接口
- Factory:根据配置创建驱动
- Facade:对外提供更简单的入口
- Decorator:日志、重试、超时、断线重连等横切能力
4) 目录(速跳)
- 设计模式基础:学习路线 / OOP 速查
- 创建型:单例 / 工厂思想 / 工厂方法 / 抽象工厂
- 结构型:装饰器 / 适配器 / 门面
- 行为型:策略 / 模板方法 / 观察者 /(可选)命令 /(可选)状态
- 主线:SOLID / DI 与组合优于继承
OOP 速查(你提到的那些"忘了"的点)
这部分不是设计模式,但它是设计模式能否看懂/写对的地基。
1) 接口 + 多实现 + 集合接收(多态)
关键结论:
- 集合的静态类型决定"你能调用哪些成员";
- 对象的运行时类型决定"最终调用哪个实现"。
csharp
public interface IPlcDriver
{
string Name { get; }
void Connect();
}
public sealed class S7Driver : IPlcDriver
{
public string Name => "S7";
public void Connect() { /* ... */ }
}
public sealed class ModbusDriver : IPlcDriver
{
public string Name => "Modbus";
public void Connect() { /* ... */ }
}
var drivers = new List<IPlcDriver> { new S7Driver(), new ModbusDriver() };
foreach (var d in drivers)
{
Console.WriteLine(d.Name);
d.Connect(); // 这里调用的是对象实际类型的实现
}
2) 抽象类 + 虚方法 + base 调用(模板扩展)
abstract:强制子类实现(没有默认实现)。virtual:子类可以选择重写(有默认实现)。override:子类重写父类虚方法。base.Xxx():在"保留父类行为"的基础上做扩展。
这通常对应 Template Method(模板方法):父类定义稳定流程,子类定制可变步骤。
3) 组合优于继承(减少"越学越乱"的关键)
如果你的变化点很多(协议、日志、重试、缓存......),优先用组合(把能力拆成可替换的对象)而不是把变化堆进继承层级。
单接口多实例设计
概念
- 单接口多实例:一个接口(契约)对应多个实现类(具体逻辑)。用接口类型作为"容器",保存和管理不同实例,运行时通过接口调用区分实现。
- 核心原理:面向接口编程(Interface-based Programming)、多态(Polymorphism)。接口定义统一行为,实现类提供差异化逻辑。
- 适用场景:多PLC协议(如Siemens、Modbus)、多数据源等,需要统一管理不同类型实例。
实现步骤
-
定义接口:
- 抽象公共行为。
C#
csharppublic interface IPlcConnection { ConnectType ConnectionType { get; } Task<bool> ConnectAsync(); Task DisconnectAsync(); // 其他统一方法 } -
实现具体类:
- 每个协议一个类,实现接口。
C#
csharppublic class SiemensS7NetConnect : IPlcConnection { public ConnectType ConnectionType => ConnectType.SiemensS7Net; public async Task<bool> ConnectAsync() { /* Siemens逻辑 */ } } -
用字典管理实例:
- Key:唯一标识(如PLC ID)。
- Value:接口类型,存实现实例。
C#
csharppublic class PlcConnectionManager { private readonly Dictionary<string, IPlcConnection> _connections = new(); public void AddConnection(string key, IPlcConnection conn) => _connections[key] = conn; public async Task ConnectAllAsync() { foreach (var conn in _connections.Values) { await conn.ConnectAsync(); // 统一调用,多态生效 } } public IPlcConnection GetConnection(string key) => _connections[key]; } -
工厂创建实例:
- 根据配置创建对应实现。
C#
arduinopublic IPlcConnection CreateConnection(PlcConfig config) { return config.Type switch { ConnectType.SiemensS7Net => new SiemensS7NetConnect(config), ConnectType.ModbusTcpNet => new ModbusTcpConnect(config), _ => throw new NotSupportedException() }; }
示例应用
-
初始化:
C#
dartvar manager = new PlcConnectionManager(); manager.AddConnection("plc1", factory.CreateConnection(siemensConfig)); manager.AddConnection("plc2", factory.CreateConnection(modbusConfig)); await manager.ConnectAllAsync(); // 自动调用各自实现 -
调用:
manager.GetConnection("plc1").ConnectAsync()返回Siemens逻辑。
优势
- 统一管理:字典存接口,无需类型判断。
- 扩展性:加新协议,只加实现类和工厂case。
- 解耦:代码依赖接口,不绑定具体类。
- 类型安全:编译时检查接口契约。
注意事项
- 接口设计:只放公共方法,避免实现类差异过大。
- 生命周期:用DI管理实例(ABP中注册为Transient/Singleton)。
- 错误处理:在实现类中处理异常,接口方法抛NotImplementedException。
- 性能:字典查询快,但实例多时考虑优化。
为什么不能/不该注册IPlcConnection?
- 无实现:接口无代码,IOC注册它没意义(容器不知道怎么实例化)。
- 多实现冲突 :多个实现类时,注入
IPlcConnection会报错(哪个实现?)。 - 纯粹承载:它只是"类型标签",用于字典或返回类型,实际工作由实现类做。
正确做法
-
注册实现类(能力提供者):
C#
inicontext.Services.AddTransient<SiemensS7NetConnect>(); context.Services.AddTransient<ModbusTcpConnect>(); -
工厂用实现类创建接口实例:
C#
arduinopublic IPlcConnection Create(PlcConfig config) { return config.Type switch { ConnectType.SiemensS7Net => _provider.GetRequiredService<SiemensS7NetConnect>(), _ => _provider.GetRequiredService<ModbusTcpConnect>() }; }- 结果 :返回
IPlcConnection(接口),但实例是注册的实现类。
- 结果 :返回
-
管理器用字典承载:
C#
csharpprivate readonly Dictionary<string, IPlcConnection> _connections; // 接口作为承载
总结
单接口多实例通过多态实现统一管理,字典存储,接口调用。适合复杂系统,避免if-else堆砌。实践时从接口开始设计!
单例设计模式(Singleton Pattern)
1. 引言
单例设计模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点来获取该实例。它通常用于管理共享资源,如数据库连接、配置文件或日志记录器,避免重复创建实例导致的资源浪费或状态不一致。
- 起源:来自《设计模式:可复用面向对象软件的基础》(GoF, 1994)。
- 适用语言:广泛用于面向对象语言,如Java、C#、C++等。本笔记以C#为例,其他语言类似。
- 静态类:更像是一种"静态单例"的简化版本,没有实例化过程,所有东西都是静态的。它不能继承或实现接口,但对于简单的全局访问场景很方便,不完全等于单例设计模式
2. 核心要素
- 私有构造函数 :防止外部通过
new关键字直接实例化类。 - 静态实例字段:存储唯一的实例(通常是私有静态)。
- 静态获取方法 :提供全局访问点,如
GetInstance(),返回唯一实例。 - 延迟初始化(可选):实例在第一次访问时创建,而非类加载时。
基本结构(伪代码):
csharp
public class Singleton {
private static Singleton instance; // 静态实例
private Singleton() {} // 私有构造函数
public static Singleton GetInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3. 常见实现方式
以下是几种经典实现,按复杂度从低到高排序。每种方式都考虑线程安全、性能和延迟初始化。
3.1 饿汉式(Eager Initialization)
-
描述:在类加载时就创建实例。
-
优点:简单、线程安全(JVM/CLR保证类加载是线程安全的)。
-
缺点:如果实例初始化耗时或占用资源大,会造成浪费(即使从未使用)。
-
适用:实例轻量且总是需要时。
-
代码示例
csharppublic sealed class Singleton { private static readonly Singleton instance = new Singleton(); private Singleton() {} public static Singleton GetInstance() => instance; }sealed:防止继承(可选,但推荐)。
3.2 懒汉式(Lazy Initialization)
-
描述 :第一次调用
GetInstance()时才创建实例。 -
优点:节省资源,延迟初始化。
-
缺点:非线程安全。多线程下可能创建多个实例(竞态条件)。
-
适用:单线程环境或实例创建不频繁。
-
代码示例
csharppublic sealed class Singleton { private static Singleton instance; private Singleton() {} public static Singleton GetInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
3.3 懒汉式加锁(Thread-Safe Lazy with Lock)
-
描述:在懒汉式基础上加双重检查锁定(Double-Checked Locking),确保线程安全。
-
优点:线程安全、延迟初始化。
-
缺点:锁开销影响性能;代码稍复杂。
-
适用:多线程环境,需要平衡性能。
-
代码示例
csharppublic sealed class Singleton { private static Singleton instance; private static readonly object lockObj = new object(); private Singleton() {} public static Singleton GetInstance() { if (instance == null) { lock (lockObj) { if (instance == null) { // 双重检查 instance = new Singleton(); } } } return instance; } }
3.4 静态内部类(Static Inner Class / Holder Pattern)
-
描述:利用嵌套类的延迟加载特性。内部类在第一次访问时加载,创建实例。
-
优点:线程安全、延迟初始化、无锁开销(JVM/CLR保证)。
-
缺点:代码结构稍复杂。
-
适用:多线程环境,追求高效。
-
代码示例
csharppublic sealed class Singleton { private Singleton() {} private static class SingletonHolder { public static readonly Singleton instance = new Singleton(); } public static Singleton GetInstance() => SingletonHolder.instance; }
3.5 C#专用:使用 Lazy
-
描述 :利用.NET的
Lazy<T>类,自动处理线程安全和延迟初始化。 -
优点:简洁、线程安全、支持异常处理和自定义初始化逻辑。
-
缺点:依赖.NET Framework/Core,稍有性能开销。
-
适用:现代C#项目。
-
代码示例
csharppublic sealed class Singleton { private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); private Singleton() {} public static Singleton GetInstance() => lazy.Value; }
3.6 其他变体
- 枚举单例:在C#中不常见(枚举不能有私有构造函数),但在Java中可用。
- 注册表单例:维护一个实例注册表,支持多个单例类。
- 多例模式:单例的扩展,允许多个实例(如连接池)。
4. 优点与缺点
优点
- 资源控制:避免重复创建实例,节省内存和资源。
- 全局访问:提供统一访问点,便于管理。
- 延迟加载:某些实现支持按需创建。
- 简单性:实现相对容易。
缺点
- 紧耦合:全局状态可能导致代码难以测试和维护(违反单一职责)。
- 线程安全复杂:多线程下需额外处理,否则易出错。
- 继承困难:私有构造函数阻止继承。
- 序列化问题 :反序列化时可能破坏单例(需实现
ISerializable或使用[Serializable]并重写OnDeserialized)。 - 测试挑战:难以mock或替换实例。
5. 应用场景
- 资源管理:数据库连接、文件句柄、网络套接字。
- 配置管理:全局配置文件或设置。
- 日志记录:统一的日志器。
- 缓存:共享缓存实例。
- 硬件接口:如打印机管理器。
- 游戏开发:游戏管理器、音效管理器。
避免过度使用:优先考虑依赖注入(DI)框架如Autofac来管理单例。
6. 潜在问题与解决方案
6.1 反射破坏单例
-
问题:通过反射调用私有构造函数,创建多个实例。
-
示例:
ini
ConstructorInfo ctor =
typeof(Singleton).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
Singleton extra = (Singleton)ctor.Invoke(null);
-
解决方案
- 在构造函数中检查实例是否存在,抛出异常。
- 使用枚举(但C#不支持)。
- 或接受风险,在文档中说明。
6.2 多线程问题
- 解决方案 :使用锁(如双重检查)、静态内部类或
Lazy<T>。
6.3 序列化/反序列化
- 问题:反序列化时可能创建新实例。
- 解决方案 :实现
ISerializable,在GetObjectData中返回现有实例,或重写OnDeserialized。
6.4 克隆破坏
- 问题 :如果类实现
ICloneable,可能被克隆。 - 解决方案 :在
Clone()中返回自身,或不实现克隆。
6.5 垃圾回收
- 问题:静态实例可能阻止垃圾回收。
- 解决方案:考虑弱引用或手动释放(但不常见)。
工厂思想(Factory thinking)
先理解"工厂思想"------最核心的理念
想象你在开一家披萨店(Pizza Hut)。顾客来点餐时,你不用告诉他们"去厨房找面粉、番茄酱、芝士,自己做披萨"。相反,你只说:"我要一个玛格丽塔披萨!"然后店员(工厂)负责一切:选料、烤制、打包。你作为顾客,只关心"要什么",工厂负责"怎么做"。
这就是工厂思想 :统一管理对象的创建,让使用者不用操心细节。在代码里,"对象"就是类实例(比如创建一个数据库连接或一个PLC客户端)。好处是:
- 解耦:代码更灵活,改创建逻辑不影响使用的地方。
- 可扩展:想加新披萨(新对象),只改工厂,不改顾客代码。
工厂思想 vs 工厂模式 vs 工厂机制:
- 工厂思想:一种"把创建对象集中起来"的思维(不等于某个固定写法)
- 工厂模式:GoF 设计模式里一套具体写法(简单工厂 / 工厂方法 / 抽象工厂)
- 工厂机制 :像.NET的
HttpClientFactory,是框架内置的"工厂系统",你直接用,不用自己写。
工厂相关的一切,本质都在解决:把
new藏起来,让使用者只表达"我要什么",不操心"怎么造"。
为什么要"藏 new"?
如果你在业务代码到处写:
ini
var plc = new SiemensPlcClient(...);
// 或
var plc = new OmronPlcClient(...);
问题是:
- 业务代码必须知道"具体类名"(强耦合)
- 新增品牌/类型时,你得去改一堆地方的
new(不符合开闭原则) - 创建过程可能很复杂(参数多、配置多、要缓存、要日志......),散落在各处会变成灾难
所以工厂思想就是:
- 业务代码只说:我要"Siemens 的 PLC"
- 工厂负责:怎么创建、需要哪些参数、要不要缓存、失败怎么报错
工厂方法模式(Factory Method Pattern)
1. 简单工厂(Simple Factory)
1. 简单工厂(Simple Factory)------一个车间管所有车型
现实比喻:工厂只有一个车间,工人根据你说的型号("Sedan"或"SUV")组装不同车。用if-else判断:Sedan就装普通零件,SUV就装越野零件。
代码怎么写:
csharp
// 1. 定义产品接口(汽车)
public interface ICar {
void Drive(); // 车能开
}
public class Sedan : ICar { // 轿车实现
public void Drive() => Console.WriteLine("Sedan driving smoothly.");
}
public class SUV : ICar { // SUV实现
public void Drive() => Console.WriteLine("SUV driving off-road.");
}
// 2. 简单工厂类(车间)
public class CarFactory {
public static ICar CreateCar(string type) { // 静态方法,顾客入口
if (type == "Sedan") return new Sedan(); // new轿车,隐藏细节
if (type == "SUV") return new SUV(); // new SUV
throw new ArgumentException("Unknown car type"); // 错误:不支持的型号
}
}
// 3. 使用(顾客买车)
var car = CarFactory.CreateCar("Sedan"); // 只传字符串,不new
car.Drive(); // 输出:Sedan driving smoothly.
为什么这么写:
CreateCar是入口,if判断type,new对应类。顾客不用知道Sedan怎么构造。- 优点:简单,新手易写。
- 缺点:加新车型(如"Truck"),得改
CreateCar的if(违反"开放封闭"------扩展时不该改现有代码)。
如果不这么做 :顾客自己new Sedan(),工厂没用。
适用场景:产品类型少,不常加新。像计算器按钮(加减乘除)。
2. 工厂方法(Factory Method)
场景:电商支付系统
-
需求:用户选择支付方式(支付宝、微信),系统调用对应支付逻辑(扣款、回调)。
-
扩展:老板说加"PayPal"支付,不能改现有代码(避免上线风险)。
-
类结构
IPayment:支付接口(扣款)。- 具体支付:
AlipayPayment、WechatPayment、PaypalPayment(新加)。
版本1:简单工厂(你觉得"简单"的)
csharp
// 1. 支付接口
public interface IPayment {
void Pay(decimal amount); // 扣款
}
// 2. 具体支付类
public class AlipayPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Alipay.");
}
public class WechatPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Wechat.");
}
// 3. 简单工厂(一个类管所有)
public class PaymentFactory {
public static IPayment CreatePayment(string type) {
if (type == "Alipay") return new AlipayPayment();
if (type == "Wechat") return new WechatPayment();
throw new ArgumentException("Unknown payment type");
}
}
// 4. 使用(顾客下单)
public class OrderService {
public void ProcessPayment(string paymentType, decimal amount) {
var payment = PaymentFactory.CreatePayment(paymentType); // 工厂创建
payment.Pay(amount);
}
}
// 测试
var service = new OrderService();
service.ProcessPayment("Alipay", 100); // 输出:Paid 100 via Alipay.
- 扩展PayPal :得改
PaymentFactory.CreatePayment,加if (type == "Paypal") return new PaypalPayment();。这改了现有工厂代码,万一出错,影响支付宝/微信。 - 问题:你"只看到了多加了好几个接口"------简单工厂没接口,只有一个Factory类。但扩展时得改它,风险高。
版本2:工厂方法(多接口版本)
csharp
// 1. 支付接口(同上)
public interface IPayment {
void Pay(decimal amount);
}
// 2. 具体支付类(同上)
public class AlipayPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Alipay.");
}
public class WechatPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Wechat.");
}
// 3. 抽象工厂接口(每个支付有自己的工厂)
public interface IPaymentFactory {
IPayment Create(); // 工厂只管创建一种支付
}
// 4. 具体工厂类(支付宝工厂)
public class AlipayFactory : IPaymentFactory {
public IPayment Create() => new AlipayPayment();
}
// 5. 微信工厂
public class WechatFactory : IPaymentFactory {
public IPayment Create() => new WechatPayment();
}
// 6. 使用(顾客下单)
public class OrderService {
private readonly IPaymentFactory _factory; // 注入工厂
public OrderService(IPaymentFactory factory) { // 构造函数注入
_factory = factory;
}
public void ProcessPayment(decimal amount) {
var payment = _factory.Create(); // 用注入的工厂
payment.Pay(amount);
}
}
// 7. DI注册(启动时)
services.AddSingleton<IPaymentFactory, AlipayFactory>(); // 默认支付宝
// 测试(模拟DI注入)
var factory = new AlipayFactory(); // 实际项目DI自动注入
var service = new OrderService(factory);
service.ProcessPayment(100); // 输出:Paid 100 via Alipay.
// 扩展PayPal:只加新类,不改老代码
public class PaypalPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Paypal.");
}
public class PaypalFactory : IPaymentFactory {
public IPayment Create() => new PaypalPayment();
}
// DI换注册
services.AddSingleton<IPaymentFactory, PaypalFactory>(); // 换PayPal
// OrderService代码完全不变!
-
扩展PayPal :只写
PaypalPayment和PaypalFactory,加一行DI注册。老的AlipayFactory、WechatFactory、OrderService不动。符合"开放封闭"------扩展不改现有代码。 -
好处
- 安全:不改老代码,少bug。上线时,老支付不受影响。
- 灵活:DI自动注入工厂,使用者类(OrderService)不new任何东西。测试时,注入MockFactory,模拟支付。
- 团队协作:你加PayPal,别人维护支付宝,不冲突。
-
"多加接口" :是的,多了一个
IPaymentFactory接口,但它换来扩展性。在大项目里,这点"多"省下维护成本。
对比总结
| 方面 | 简单工厂 | 工厂方法 |
|---|---|---|
| 扩展新支付 | 改Factory if,风险高 | 加新类+DI,不改老代码 |
| 代码改动 | 改现有Factory | 只加新文件 |
| 测试 | 难mock支付逻辑 | 易注入MockFactory |
| DI集成 | 不易 | 完美 |
| 适用 | 小项目,支付固定 | 大项目,经常加新支付 |
在这个支付案例里,工厂方法"好用"因为电商App常加新支付(比如加Apple Pay),你不想每次上线时冒风险改核心代码。像支付宝/微信的SDK,其实背后用类似模式。
erlang
如上,最大的好处就是,统一我这一块的功能,都通过一个工厂创建出来对象.
这样我工厂创建出来的对象,直接用就好了.
然后这个对象,又是一个抽象对象,我定义固定的功能.让工厂创建我时可以直接使用了,约定俗成的功能.
然后我这个抽象对象可以有多个实现,通过替换抽象对象与实现,来达成多个对象功能的解耦.
工厂方法设计模式扩展
泛型工厂方法(Generic Factory Method)
用C#泛型,让工厂通用,避免重复代码。
扩展:工厂接口用泛型,创建任意类型。
C#
csharp
public interface IFactory<T> {
T Create();
}
public class PaymentFactory<T> : IFactory<T> where T : IPayment, new() {
public T Create() => new T();
}
// 使用
var factory = new PaymentFactory<AlipayPayment>();
var payment = factory.Create();
好处:代码复用,少写类。适合简单产品。
结合策略模式(Strategy + Factory Method)
工厂方法创建产品,策略模式让产品行为可换。
扩展:支付器用策略封装扣款逻辑。
C#
csharp
public interface IPayStrategy {
void Execute(decimal amount);
}
public class AlipayStrategy : IPayStrategy {
public void Execute(decimal amount) => Console.WriteLine($"Strategy: Paid {amount} via Alipay.");
}
public interface IPaymentFactory {
IPayment Create(IPayStrategy strategy); // 注入策略
}
public class AlipayFactory : IPaymentFactory {
public IPayment Create(IPayStrategy strategy) => new AlipayPayment(strategy);
}
public class AlipayPayment : IPayment {
private readonly IPayStrategy _strategy;
public AlipayPayment(IPayStrategy strategy) { _strategy = strategy; }
public void Pay(decimal amount) => _strategy.Execute(amount);
}
// 使用
var strategy = new AlipayStrategy();
var factory = new AlipayFactory();
var payment = factory.Create(strategy);
payment.Pay(100); // Strategy: Paid 100 via Alipay.
好处:行为可扩展,测试时换MockStrategy。
工厂方法 + 原型模式(Prototype + Factory)
产品复杂时,用原型克隆,避免重复构造。
扩展:工厂克隆原型。
C#
csharp
public interface IPayment : ICloneable {
void Pay(decimal amount);
}
public class AlipayPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Alipay.");
public object Clone() => MemberwiseClone();
}
public interface IPaymentFactory {
IPayment Create();
}
public class AlipayFactory : IPaymentFactory {
private readonly IPayment _prototype = new AlipayPayment();
public IPayment Create() => (IPayment)_prototype.Clone();
}
好处:构造成本高时,克隆快。
工厂方法在框架中的扩展(像.NET的Activator)
用反射,让工厂动态创建任意类。
C#
kotlin
public class DynamicFactory : IPaymentFactory {
public IPayment Create() {
var type = Type.GetType("AlipayPayment"); // 动态
return (IPayment)Activator.CreateInstance(type);
}
}
好处:配置驱动,运行时决定产品。
抽象工厂设计模式(Abstract Factory Pattern)
抽象工厂是什么?
- 核心 :不是创建单一产品,而是创建一组相关产品(比如支付方式+配套的回调处理器)。抽象工厂定义接口,让具体工厂实现一整族。
- 比喻:工厂方法像"每个车型有生产线"(轿车生产线只造轿车)。抽象工厂像"整个品牌工厂"(宝马工厂造轿车+发动机+轮胎,一族配套)。
- 为什么叫"抽象" :工厂本身是抽象的(接口),具体工厂实现它。
代码示例:扩展支付系统(支付 + 回调处理器)
假设支付系统不仅扣款,还要处理回调(比如支付宝回调通知支付成功)。一组产品:支付器(Payment) + 回调处理器(CallbackHandler)。要保证它们配套(支付宝支付配支付宝回调)。
抽象工厂版本
csharp
// 1. 产品接口(支付器和回调处理器)
public interface IPayment {
void Pay(decimal amount);
}
public interface ICallbackHandler {
void HandleCallback(string data); // 处理回调
}
// 2. 抽象工厂接口(定义一族产品)
public interface IPaymentFactory {
IPayment CreatePayment(); // 创建支付器
ICallbackHandler CreateCallbackHandler(); // 创建回调处理器
}
// 3. 具体工厂:支付宝族(支付 + 支付宝回调)
public class AlipayFactory : IPaymentFactory {
public IPayment CreatePayment() => new AlipayPayment();
public ICallbackHandler CreateCallbackHandler() => new AlipayCallbackHandler();
}
// 支付宝支付实现
public class AlipayPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Alipay.");
}
// 支付宝回调实现
public class AlipayCallbackHandler : ICallbackHandler {
public void HandleCallback(string data) => Console.WriteLine($"Alipay callback: {data}");
}
// 4. 微信组(支付 + 微信回调)
public class WechatFactory : IPaymentFactory {
public IPayment CreatePayment() => new WechatPayment();
public ICallbackHandler CreateCallbackHandler() => new WechatCallbackHandler();
}
// 微信支付
public class WechatPayment : IPayment {
public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} via Wechat.");
}
// 微信回调
public class WechatCallbackHandler : ICallbackHandler {
public void HandleCallback(string data) => Console.WriteLine($"Wechat callback: {data}");
}
// 5. 使用(顾客下单 + 处理回调)
public class OrderService {
private readonly IPaymentFactory _factory;
public OrderService(IPaymentFactory factory) { // DI注入
_factory = factory;
}
public void ProcessPayment(decimal amount) {
var payment = _factory.CreatePayment();
payment.Pay(amount);
}
public void ProcessCallback(string data) {
var handler = _factory.CreateCallbackHandler();
handler.HandleCallback(data);
}
}
// 6. DI注册
services.AddSingleton<IPaymentFactory, AlipayFactory>(); // 默认支付宝族
// 测试
var factory = new AlipayFactory(); // 实际DI注入
var service = new OrderService(factory);
service.ProcessPayment(100); // Paid 100 via Alipay.
service.ProcessCallback("success"); // Alipay callback: success
// 扩展PayPal族:加PaypalFactory、PaypalPayment、PaypalCallbackHandler
public class PaypalFactory : IPaymentFactory {
public IPayment CreatePayment() => new PaypalPayment();
public ICallbackHandler CreateCallbackHandler() => new PaypalCallbackHandler();
}
// DI换注册:services.AddSingleton<IPaymentFactory, PaypalFactory>();
// OrderService代码不变!
比工厂方法好在哪里?
工厂方法只创建单一产品(比如只Payment),抽象工厂创建一组产品(Payment + CallbackHandler),保证配套一致。
1. 产品族一致性(核心优势)
- 工厂方法 :
IPaymentFactory.Create()只返回Payment。回调处理器得单独处理,可能混用(支付宝支付配微信回调,错乱)。 - 抽象工厂:一族产品一起创建,确保配套(支付宝工厂出支付宝支付+支付宝回调)。切换族时,所有产品一起换,逻辑一致。
- 好处:避免错误组合,像UI框架(按钮+主题配套),或数据库(连接+命令配套)。
2. 扩展更强(加新族,不改老代码)
- 加PayPal族,只写新Factory + 新产品族,不改AlipayFactory或WechatFactory。
- 工厂方法加新产品也行,但没"组"概念。
3. 复杂场景适用(产品相关时)
- 适用:产品有依赖关系(如支付+回调必须匹配)。
- 不适用:产品独立时,用工厂方法够了。
对比工厂方法
| 方面 | 工厂方法 | 抽象工厂 |
|---|---|---|
| 创建 | 单一产品 | 一组产品 |
| 一致性 | 无 | 保证组配套 |
| 类数 | 每个产品1工厂 | 每组1工厂 |
| 适用 | 产品独立 | 产品相关,需配套 |
| 例子 | 日志工厂(只日志) | UI工厂(按钮+主题) |
抽象工厂是工厂方法的超集,在产品族场景更好。但如果产品不相关,别用,会多余。
泛型抽象工厂(怎么写与何时用)
这部分经常会让人"写着写着就泛型地狱"。结论先说:
- 抽象工厂的本质是"创建一族相关对象";
- 是否用泛型要看你是否真的需要"编译期约束 + 类型安全"。
- 在 .NET 项目里,很多"泛型抽象工厂"的需求最终会被 DI 容器更优雅地解决(注册不同实现,然后按配置选择)。
1) 非泛型版本(更直观)
例如 PLC 协议族:驱动 + 地址解析器 + 数据编解码器(同一族要配套)。
csharp
public interface IPlcFamilyFactory
{
IPlcDriver CreateDriver();
IAddressTranslator CreateAddressTranslator();
IDataCodec CreateCodec();
}
public interface IAddressTranslator { string Normalize(string address); }
public interface IDataCodec { byte[] Encode<T>(T value); T Decode<T>(byte[] raw); }
2) 泛型版本(类型更强,但更"重")
csharp
public interface IAbstractFactory<out T1, out T2>
{
T1 CreateFirst();
T2 CreateSecond();
}
// 示例:创建"驱动 + 编解码器"这一族
public interface IPlcFamilyFactory<TDriver, TCodec> : IAbstractFactory<TDriver, TCodec>
where TDriver : IPlcDriver
where TCodec : IDataCodec
{
}
什么时候值得用泛型:
- 你在写"通用库",希望调用方在编译期就拿到强类型(减少运行时错误)。
什么时候不值得:
- 业务项目里主要靠配置决定协议/型号;这类更适合"非泛型抽象 + DI + 配置选择"。
原型设计模式
要点速览
-
目的:在运行时复制已有对象,而不是通过 new + 构造器重新初始化(尤其适合创建成本高或初始化复杂的对象,或需要保留运行时状态的对象)。
-
两种拷贝:
- 浅拷贝(shallow copy):复制对象自身的字段,但引用类型字段仍指向同一实例(常用 MemberwiseClone)。
- 深拷贝(deep copy):复制整个对象图,引用类型字段也被复制为新实例(需要显式复制或借助库/序列化)。
-
C# 中的陷阱:ICloneable 接口语义不明确(没有标注深/浅),不建议直接依赖标准 ICloneable。在库层面常用自定义泛型接口或 Copy/Clone 方法明确语义。
-
第三方库(如你提到的 CloneExtensions)可以快捷实现,但需注意性能、循环引用支持、对事件/委托的处理等。
设计建议(实践)
- 明确语义:自己定义 IPrototype 或 ICloneable 并把深/浅写进文档/方法名(CloneShallow/CloneDeep)。
- 优先考虑不可变对象(immutable)以避免需要克隆。
- 如果对象图复杂,自己控制克隆逻辑(每个有引用类型字段的类都实现 clone)常最可靠。
- 序列化实现(JSON / protobuf / MessagePack)能快速实现深拷贝,但受限于可序列化性、性能和对循环引用的支持。
- 第三方库:使用前检查库是否支持循环引用、是否保留对象身份、性能基准、是否会复制事件订阅等。
示例代码(C#)
- 基本的原型接口(推荐自定义接口,明确返回类型)
C#
csharp
public interface IPrototype<T>
{
T Clone(); // 语义由实现方决定(建议注释说明是浅/深)
}
- 浅拷贝 --- 使用 MemberwiseClone(受保护,只能在类内部调用)
C#
csharp
public class Address
{
public string City { get; set; }
}
public class Person : IPrototype<Person>
{
public string Name { get; set; }
public Address Address { get; set; }
// 浅拷贝:Address 引用被共享
public Person Clone()
{
return (Person)this.MemberwiseClone();
}
}
- 深拷贝 --- 手动实现(最可控、最安全)
C#
csharp
public class Address : IPrototype<Address>
{
public string City { get; set; }
public Address Clone() => new Address { City = this.City };
}
public class Person : IPrototype<Person>
{
public string Name { get; set; }
public Address Address { get; set; }
// 深拷贝:同时克隆 Address
public Person Clone()
{
return new Person
{
Name = this.Name,
Address = this.Address?.Clone()
};
}
}
- 深拷贝 --- 使用 JSON 序列化(快捷,但有限制)
- 优点:非常方便,适合用于没有循环引用且类型可序列化的场景。
- 缺点:性能较差、不能克隆不可序列化成员、可能改变对象(例如 private 字段不可序列化)。 示例(System.Text.Json):
C#
csharp
using System.Text.Json;
using System.Text.Json.Serialization;
public static class CloneHelpers
{
public static T DeepCloneJson<T>(T source)
{
var options = new JsonSerializerOptions
{
// 如果需要保留对象引用/循环引用,请使用 ReferenceHandler.Preserve(并注意反序列化时表现)
// ReferenceHandler = ReferenceHandler.Preserve
};
var json = JsonSerializer.Serialize(source, options);
return JsonSerializer.Deserialize<T>(json, options);
}
}
注意:若对象图有循环引用,需要设置 ReferenceHandler 并确认你的 .NET 版本支持(并理解序列化格式会包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> i d / id/ </math>id/ref 等标记)。
- 使用第三方库(例如你提到的 CloneExtensions)
- 常见做法:安装 NuGet 包(Install-Package CloneExtensions 或库名),使用扩展方法如 Clone() / DeepClone()。不同库 API 不同,所以在代码里要查文档确认方法名与选项。 示例(伪代码,示例用法请参照具体库文档):
C#
csharp
// NuGet: Install-Package CloneExtensions
using CloneExtensions;
var copy = original.DeepClone(); // 或 original.Clone(),取决于库
使用第三方库前检查:
- 是否支持循环引用与对象身份维护(即同一个引用在新对象图中仍是同一新对象);
- 是否会复制事件、委托、静态/非序列化字段;
- 性能:有些库使用反射或表达式树生成器,第一次克隆可能有预热;对于热路径请做基准测试。
关于 ICloneable 的补充
-
System.ICloneable 的 Clone() 没有规定是深还是浅拷贝,导致语义不明确。建议:
- 避免直接公开 ICloneable;如果需要公开 clone 行为,自定义接口并命名为 CloneDeep、CloneShallow 或 IPrototype。
- 或者提供 Copy ctor(拷贝构造函数)和静态工厂 From(original)。
常见陷阱(Checklist)
- 事件 (event) 与委托会被复制/或共享?通常不应复制事件订阅者。
- 只读字段(readonly)和不可变内部状态如何复制?
- 循环引用(A -> B -> A)会导致无限复制:要么手动处理引用缓存(映射旧->新),要么使用支持引用处理的库/序列化器。
- 多线程场景:克隆是否需要线程安全?
- 性能与内存:深拷贝可能非常昂贵,注意在性能敏感代码不要滥用。
建议的实现策略(实际项目)
- 简单对象/值对象:使用浅拷贝(MemberwiseClone)或直接 new(若构造成本低)。
- 复杂对象图且需要完整独立副本:手动实现 Clone(每个类实现 Clone,并在顶层递归调用),或采用受信任的库(前提是你已经验证其语义和性能)。
- 若你需要跨进程或持久化:考虑序列化方式(但注意安全与性能)。
建造者模式
一句话概念
- 建造者模式:把复杂对象的构建过程从表示中分离出来,使同样的构建过程可以创建不同的表示。
要点速览
- 常用于:构造参数多、组合复杂、需要一步步配置、需要在 Build() 时统一校验/设置默认值。
- 方法链(返回 this)是常见实现方式,但不是唯一实现。
- 如果需要"强制顺序",可以用 Step Builder(用接口限制下一步可调用的方法)或 Director(指挥者)控制步骤。
示例 1:经典流式 Builder(最常见)
- 中间方法返回 builder(通常是 this),最后调用 Build() 得到目标对象。
C#
csharp
public class Person
{
public string Name { get; }
public int Age { get; }
public string Address { get; }
private Person(string name, int age, string address)
{
Name = name; Age = age; Address = address;
}
public class Builder
{
private string _name;
private int _age;
private string _address;
public Builder WithName(string name) { _name = name; return this; }
public Builder WithAge(int age) { _age = age; return this; }
public Builder WithAddress(string address) { _address = address; return this; }
public Person Build()
{
// 在这里做校验或默认值设置
if (string.IsNullOrEmpty(_name)) throw new InvalidOperationException("Name required");
return new Person(_name, _age, _address);
}
}
}
// 使用:
var p = new Person.Builder()
.WithName("Alice")
.WithAge(30)
.WithAddress("Beijing")
.Build();
解释:中间方法返回 Builder(即"返回类本身"),支持链式调用;但真正的产品是 Build() 返回的 Person。
示例 2:极简示例------Step Builder(强制顺序)
C#
csharp
public class Sandwich {
public string Bread { get; }
public string Filling { get; }
private Sandwich(string b,string f){ Bread=b; Filling=f; }
public interface IBread { IFill WithBread(string bread); }
public interface IFill { IBuild WithFilling(string filling); }
public interface IBuild { Sandwich Build(); }
class Builder : IBread, IFill, IBuild {
string b,f;
public IFill WithBread(string bread){ b=bread; return this; }
public IBuild WithFilling(string filling){ f=filling; return this; }
public Sandwich Build() => new Sandwich(b,f);
}
public static IBread NewBuilder() => new Builder();
}
// 用法(按顺序强制):
var s = Sandwich.NewBuilder().WithBread("Wheat").WithFilling("Ham").Build();
就是这样:链式方法返回 builder(this)用于连续配置,Build() 返回最终对象;若需强制步骤,用不同接口约束调用顺序。
装饰器设计模式
- 装饰器模式:在运行时"给对象外面包一层(或多层)"来增加功能,包裹后仍然像原来那个类型,可以随时叠加或移除。
生活类比
- 就像给人穿衣服:人(原始对象)不变,外面套件不同的衣服(装饰器)来增加功能(保暖、遮挡、装饰),可以随时穿/脱,多件可以叠加。
核心点(非常简短)
- 用组合(把被装饰对象存为字段)而不是继承来扩展功能;
- 装饰器实现与被装饰对象相同的接口,所以可以互换;
- 装饰器通常在调用被装饰对象前后插入额外逻辑;
- 可以在运行时按需叠加多个装饰器。
结构(4 个角色)
- Component:共同接口或抽象类(客户和装饰器都通过它交互)。
- ConcreteComponent:原始实现(要被装饰的对象)。
- Decorator(抽象装饰器):实现 Component 并持有一个 Component 引用,默认把请求转发给内部对象。
- ConcreteDecorator:具体装饰器,在委托前后添加功能。
最小可读 C# 示例(足够短,逐行注释)
C#
csharp
// 共同接口
public interface IService
{
void Execute();
}
// 原始实现:要被装饰的对象
public class RealService : IService
{
public void Execute()
{
Console.WriteLine("RealService: 执行核心逻辑");
}
}
// 抽象装饰器:持有一个 IService 并委托调用
public abstract class ServiceDecorator : IService
{
protected IService Inner;
protected ServiceDecorator(IService inner) { Inner = inner; }
// 默认行为:把调用传给内部对象
public virtual void Execute() => Inner.Execute();
}
// 具体装饰器:在调用前后增加日志
public class LoggingDecorator : ServiceDecorator
{
public LoggingDecorator(IService inner) : base(inner) { }
public override void Execute()
{
Console.WriteLine("[Log] 执行前");
base.Execute(); // 委托给被装饰对象(可能是 RealService 或另一个装饰器)
Console.WriteLine("[Log] 执行后");
}
}
// 使用示例
IService svc = new RealService();
// 在运行时把日志"包"上去
svc = new LoggingDecorator(svc);
// 还可以再包别的装饰器:svc = new AnotherDecorator(svc);
svc.Execute();
/* 输出:
[Log] 执行前
RealService: 执行核心逻辑
[Log] 执行后
*/
为什么这不是"重写(override)+ base"
- override 是通过子类在类型层次中改变行为(静态决定);base 调用的是父类实现。
- 装饰器是把现有对象当作一个实例包起来,运行时可以任意组合多个装饰器(组合 + 委托),而不是在继承链里增加一个子类版本。
- 说白了:重写是在"内部修改类的行为",装饰器是在"外面包一层、动态添加行为"。
什么时候用装饰器(简短建议)
- 需要在运行时动态加功能(比如日志、缓存、权限、重试等);
- 想避免为每种组合写大量子类;
- 希望把横切关注点拆成可复用的小块。
注意事项(简短)
- 层数太多会让调试困难;
- 装饰器只能访问接口暴露的东西,无法直接访问被装饰对象的私有实现(这是优点也是限制);
- 性能敏感路径注意调用开销。
适配器模式
一句话概念
- 适配器模式:把一个类的接口转换成客户期望的另一个接口,使原本接口不兼容的类可以一起工作。核心是"让已有代码(被适配者)在不改动其代码的情况下,被新的接口使用"。
什么时候用
- 需要复用现有类但其接口与当前系统不匹配。
- 想把第三方/遗留库接入到新的接口体系中,避免修改第三方代码。
- 想在不同模块之间做协议或数据格式的桥接。
两种常见实现方式
-
对象适配器(Object Adapter,推荐在 C#/Java 中使用)
- 通过组合:Adapter 持有一个被适配对象的引用,Adapter 实现目标接口并在内部委托给被适配对象(可能做转换)。
- 优点:灵活,不改变被适配类,支持适配多个具体对象实例。
-
类适配器(Class Adapter)
- 通过继承:Adapter 继承被适配类并实现目标接口(或反过来),直接复用被适配类实现。
- 在支持单继承的语言中(如 C#)受限:只能从一个具体类继承,不像 C++ 支持多继承那样灵活。通常不推荐在 C# 中广泛使用。
最小 C# 示例(对象适配器,易读)
C#
csharp
// 客户端期望的接口(Target)
public interface ITarget
{
void Request(); // 客户端通过这个方法使用服务
}
// 现有的、接口不匹配的类(Adaptee)
public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("Adaptee: SpecificRequest 被调用(接口不匹配)");
}
}
// 适配器:实现 ITarget,并把调用转发/转换给 Adaptee
public class Adapter : ITarget
{
private readonly Adaptee _adaptee;
public Adapter(Adaptee adaptee) { _adaptee = adaptee; }
public void Request()
{
// 在这里可以做参数/返回值/协议转换
Console.WriteLine("Adapter: 在调用前可以做一些转换或预处理");
_adaptee.SpecificRequest(); // 委托给被适配对象
Console.WriteLine("Adapter: 在调用后可做一些后处理");
}
}
// 客户端使用
ITarget target = new Adapter(new Adaptee());
target.Request();
与其它模式的区别(简短)
- 装饰器(Decorator):也是包一层,但目的是"增强/扩展行为",并且装饰器通常保留并透传原接口的行为;适配器是改变接口,让不兼容的接口能工作。两者结构相似,但意图不同。
- 代理(Proxy):控制对对象的访问(比如延迟加载、权限检查),意图不同;代理通常与目标接口完全一致并把调用转发给真实对象。
- 适配器 vs 桥(Bridge):桥模式用于把抽象与实现分离以独立扩展,适配器用于兼容接口。
注意事项与建议
- 优先用对象适配器(明确定义转换点、可在运行时传入不同被适配对象)。
- 如果需要适配多个不同接口到同一个目标,考虑写不同的适配器类或使用通用转换层。
- 适配器不应包含复杂业务逻辑,主要负责接口/协议或数据格式转换;复杂逻辑应放到更合适的层次。
常见现实例子
- 把老 API 包装成新接口(例如把旧数据库访问类适配成新的仓储接口 IRepository)。
- 将不同日志库适配成统一 ILogger 接口。
- 在系统集成中把不同格式的数据转换(XML -> 对象 -> JSON 接口等)。
策略模式
一句话概念
- 策略模式:把"可替换的算法/行为"抽成接口(策略),在运行时选择/切换不同实现;调用方只依赖抽象,不依赖具体策略。
什么时候用(很重要)
- 有一组同类行为:写法不同、目标相同(例如不同 PLC 协议的 Read/Write)。
- 你希望新增一种行为时:只"加类",不改调用方(符合 OCP)。
- 你希望单元测试时能替换行为(Mock/Fake)。
与工厂/适配器的关系
- Strategy 解决"行为可替换";
- Factory 解决"创建哪一个策略";
- Adapter 解决"把第三方库的接口变成你的策略接口"。
PLC/HSL 贯穿示例:用策略抽象不同协议驱动
csharp
public interface IPlcDriver
{
string Protocol { get; }
void Connect();
T Read<T>(string address);
void Write<T>(string address, T value);
}
public sealed class PlcClient
{
private readonly IPlcDriver _driver;
public PlcClient(IPlcDriver driver) => _driver = driver;
public void Connect() => _driver.Connect();
public T Read<T>(string address) => _driver.Read<T>(address);
public void Write<T>(string address, T value) => _driver.Write(address, value);
}
// 业务层:只依赖 IPlcDriver,不依赖具体协议
// IPlcDriver driver = new S7Driver(...); 或 new ModbusTcpDriver(...);
// var client = new PlcClient(driver);
常见坑
- 策略接口设计过大(把连接、读写、订阅、批量、诊断全塞进去)会导致实现类痛苦;建议按职责拆小接口或按阶段演进。
- 不要为了"用模式"而硬套:如果只有一个实现且不会增加,先别抽接口。
Provider模式(Provider Pattern)
可以查微软的"Provider Pattern"文档或者看《Dependency Injection in .NET》这类书。
它和DI(依赖注入)结合紧密,不是纯设计模式。
观察者模式
一句话概念
- 观察者模式:当一个对象(Subject)状态变化时,自动通知一组订阅者(Observer)。在 .NET 里最常见的落地就是事件(event)/委托(delegate),以及
IObservable<T>。
什么时候用
- 连接状态变化(断线、重连、已连接)需要通知 UI/日志/告警。
- 数据变化(轮询/订阅)需要把变化推送到多个处理模块。
.NET 事件版(最常用、最易落地)
csharp
public enum ConnectionState { Disconnected, Connecting, Connected, Faulted }
public sealed class ConnectionStateChangedEventArgs : EventArgs
{
public ConnectionStateChangedEventArgs(ConnectionState state, string? reason = null)
{
State = state;
Reason = reason;
}
public ConnectionState State { get; }
public string? Reason { get; }
}
public sealed class PlcConnectionMonitor
{
public event EventHandler<ConnectionStateChangedEventArgs>? StateChanged;
private ConnectionState _state = ConnectionState.Disconnected;
public void SetState(ConnectionState state, string? reason = null)
{
if (_state == state) return;
_state = state;
StateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(state, reason));
}
}
// 订阅者(例如 UI / 日志)
// monitor.StateChanged += (_, e) => Console.WriteLine($"State: {e.State}, reason={e.Reason}");
常见坑
- 事件订阅没取消会导致内存泄漏(特别是长生命周期对象订阅短生命周期对象,或反过来)。
- 通知里做重逻辑会阻塞发布者线程;必要时用队列/后台任务。
门面模式
一句话概念
- 门面模式:为一组复杂子系统提供一个更简单的统一入口,隐藏复杂度、降低耦合。
PLC/HSL 场景为什么很适合
- HSL 的具体类很多、连接/读写/类型转换/异常处理分散;业务层不应该了解这些细节。
- 你可以用一个
PlcFacade对外提供统一 API,把协议差异和细节留在内部。
最小门面示例(把"怎么连、怎么读写"藏起来)
csharp
public sealed class PlcFacade
{
private readonly IPlcDriver _driver;
public PlcFacade(IPlcDriver driver)
{
_driver = driver;
}
public void Connect() => _driver.Connect();
public int ReadInt32(string address) => _driver.Read<int>(address);
public void WriteInt32(string address, int value) => _driver.Write(address, value);
}
门面 vs 适配器 vs 装饰器(别混)
- Facade:对外"变简单",不一定改变内部接口;
- Adapter:对外"变兼容",把 A 接口转成 B 接口;
- Decorator:对外接口不变,但"增强行为"(日志/重试/缓存)。
模板方法模式(Template Method Pattern)
一句话概念
- 模板方法:父类定义稳定流程(算法骨架),把可变步骤留给子类重写(
abstract/virtual),必要时子类可用base保留父类行为再扩展。 - 该方式常用于单接口,多实现实例时使用,有一个抽象模板类,方便统一管理多实例,对多实例中不同的实现方法进行包裹trycatch统一处理,增加共同字段在模板类中直接处理,达到实现实例中只写不同的实例方法
什么时候用
- 多个驱动的"流程一致、步骤不同":例如连接前检查、读写前校验、异常转换、日志打点等。
PLC 驱动流程示例(抽象类定义骨架)
csharp
public abstract class PlcDriverBase : IPlcDriver
{
public abstract string Protocol { get; }
public void Connect()
{
BeforeConnect();
ConnectCore();
AfterConnect();
}
public T Read<T>(string address)
{
ValidateAddress(address);
return ReadCore<T>(address);
}
public void Write<T>(string address, T value)
{
ValidateAddress(address);
WriteCore(address, value);
}
protected virtual void BeforeConnect() { }
protected virtual void AfterConnect() { }
protected virtual void ValidateAddress(string address)
{
if (string.IsNullOrWhiteSpace(address)) throw new ArgumentException("address is required", nameof(address));
}
protected abstract void ConnectCore();
protected abstract T ReadCore<T>(string address);
protected abstract void WriteCore<T>(string address, T value);
}
使用建议
- Template Method 适合"流程稳定";如果变化点很多且组合变化多,优先用 Strategy + Decorator(更灵活)。
命令模式(Command Pattern,可选但很实用)
一句话概念
- 命令模式:把一次请求封装成对象(命令),从而支持排队、记录、重试、撤销(有些领域可撤销)等。
PLC 场景怎么落地(排队 + 重试最常见)
csharp
public interface IPlcCommand
{
void Execute(IPlcDriver driver);
}
public sealed class WriteInt32Command : IPlcCommand
{
public WriteInt32Command(string address, int value)
{
Address = address;
Value = value;
}
public string Address { get; }
public int Value { get; }
public void Execute(IPlcDriver driver) => driver.Write(Address, Value);
}
// 后续可以做:队列、失败重试、统一超时、执行统计等
状态模式(State Pattern,可选)
一句话概念
- 状态模式:把"状态相关的行为"拆到不同状态对象里,让对象在不同状态下表现不同,避免 if/else 状态机散落各处。
PLC 场景很常见的状态
- Disconnected / Connecting / Connected / Faulted
落地建议(从简到难)
- 第一阶段:用
enum ConnectionState+ 少量 if/guard 就够。 - 第二阶段:当你发现"每个状态的行为差异很大、分支爆炸",再引入 State Pattern。
SOLID(主线,比背模式更关键)
SRP:单一职责
- 一个类只做一件"变化原因一致"的事。
- 在 PLC 封装里:协议驱动负责"协议读写";日志/重试/缓存不要塞进驱动核心,应该用 Decorator 或独立服务。
OCP:开闭原则
- 对扩展开放,对修改关闭。
- 你的目标:"新增一种 PLC 协议"时,最好是加一个新驱动类 + 工厂映射/DI 注册,而不是去改一堆业务 if/else。
LSP:里氏替换
- 子类/实现必须能替换父类/接口,且不破坏调用方期望。
- 例如:某个驱动实现不应该在
Read<T>里对某些地址悄悄返回默认值而不报错(会让上层逻辑很难排查)。
ISP:接口隔离
- 不要逼迫实现类依赖它不需要的方法。
- 经验:先做小接口(连接/读/写/订阅),再组合成门面;比一开始就做"巨无霸 IPlcDriver"更稳。
DIP:依赖倒置
- 高层模块不依赖低层模块,二者都依赖抽象。
- 业务服务依赖
IPlcDriver/PlcFacade,不直接依赖 HSL 的具体通信类。
依赖注入(DI)与组合优于继承(落地方式)
核心目标
- 让"选择哪个实现"发生在启动/配置阶段,而不是散落在业务代码里。
典型做法(伪代码,展示思路)
csharp
// 注册:把抽象映射到具体实现
// services.AddSingleton<IPlcDriver, HslS7Driver>();
// services.AddSingleton<PlcFacade>();
// 使用:业务类只要声明依赖即可
public sealed class AlarmService
{
private readonly PlcFacade _plc;
public AlarmService(PlcFacade plc) => _plc = plc;
public void Ack(string address)
{
_plc.WriteInt32(address, 1);
}
}
你会发现:当你把"创建/选择"移出业务代码后,很多设计模式会变得更自然、更少焦虑。