与买桂花同载酒 🌙

设计模式学习

MVC

这个例子展示了数据(Model)、显示(View)和调度控制(Controller)是如何彻底分开的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;

// 1. Model (后厨):只负责存储和管理核心数据,完全不知道界面长什么样
public class UserModel
{
public string UserName { get; set; }
}

// 2. View (摆盘):只负责把数据打印到屏幕上,没有任何复杂的业务逻辑
public class UserView
{
public void PrintUserDetails(string name)
{
Console.WriteLine($"[UI界面] 欢迎您,尊贵的客户:{name}");
}
}

// 3. Controller (服务员):负责接收指令,并把 Model 和 View 连起来
public class UserController
{
private UserModel _model;
private UserView _view;

public UserController(UserModel model, UserView view)
{
_model = model;
_view = view;
}

// 接收外部输入,更新模型
public void UpdateName(string newName)
{
_model.UserName = newName;
}

// 叫视图去刷新显示
public void RefreshScreen()
{
_view.PrintUserDetails(_model.UserName);
}
}

// === 测试运行 ===
// var controller = new UserController(new UserModel(), new UserView());
// controller.UpdateName("张三");
// controller.RefreshScreen();

DAO (数据访问对象)

这个例子展示了业务层如何通过一个“接口(采购经理)”与数据库打交道,从而彻底屏蔽掉底层的 SQL 细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;

public class User { public string Name { get; set; } }

// 1. DAO 接口 (定义采购标准):业务层只认这个接口
public interface IUserDao
{
void Save(User user);
}

// 2. DAO 具体实现 (采购经理):真正去和数据库打交道的地方
public class SqlServerUserDao : IUserDao
{
public void Save(User user)
{
// 只有这里才会出现真正的底层数据库操作代码
string sql = $"INSERT INTO Users (Name) VALUES ('{user.Name}')";
Console.WriteLine($"[DAO层] 执行底层命令: {sql}");
Console.WriteLine($"[DAO层] 成功将 {user.Name} 保存到 SQL Server。");
}
}

// 3. 业务逻辑层 (后厨):完全看不到 SQL 语句
public class UserService
{
private IUserDao _dao; // 依赖注入 DAO

public UserService(IUserDao dao)
{
_dao = dao;
}

public void RegisterUser(string name)
{
Console.WriteLine($"[业务层] 正在校验 {name} 的注册信息...");
var newUser = new User { Name = name };

// 直接让 DAO 去存,业务层根本不管是用 MySQL 还是 SQL Server
_dao.Save(newUser);
}
}

API Gateway (API 网关)

这是一个简化的网关模型,展示了如何用一个统一的入口接收请求,先做安全校验,再“路由转发”给后台的具体微服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;

// 后台微服务 A 和 B
public class OrderService { public void Handle() => Console.WriteLine("--> [订单服务] 正在处理订单..."); }
public class PaymentService { public void Handle() => Console.WriteLine("--> [支付服务] 正在处理扣款..."); }

// 网关 (前台总机)
public class ApiGateway
{
private OrderService _orderSvc = new OrderService();
private PaymentService _paymentSvc = new PaymentService();

// 外部用户永远只调用这一个方法
public void RouteRequest(string url, string token)
{
Console.WriteLine($"\n[网关] 收到外部请求: {url}");

// 1. 统一鉴权 (拦截非法请求)
if (token != "ValidToken_123")
{
Console.WriteLine("[网关] 警告:身份验证失败,拒绝访问!");
return;
}
Console.WriteLine("[网关] 鉴权通过,开始路由转发...");

// 2. 路由分发
if (url.StartsWith("/api/order"))
{
_orderSvc.Handle();
}
else if (url.StartsWith("/api/payment"))
{
_paymentSvc.Handle();
}
else
{
Console.WriteLine("[网关] 404: 找不到对应的微服务。");
}
}
}

Circuit Breaker (断路器)

这是一个手写的极简断路器。在 .NET 中,我们通常直接使用大名鼎鼎的第三方库 Polly,但底层的核心状态机原理和这段代码是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;

public class SimpleCircuitBreaker
{
private int _failureThreshold = 3; // 容忍连续失败的次数
private int _failureCount = 0;
private bool _isOpen = false; // 是否跳闸 (Open状态表示电路断开,阻断请求)

// 把需要保护的代码(Action)传进来执行
public void Execute(Action remoteCall)
{
if (_isOpen)
{
// 如果已经跳闸,直接报错拦截,不再让底层线程去傻等超时 (Fail Fast)
Console.WriteLine("❌ [断路器] 已经跳闸!保护性拒绝请求,防止系统雪崩。");
return;
}

try
{
remoteCall(); // 尝试调用不稳定的远程微服务

// 如果成功了,重置失败计数
_failureCount = 0;
Console.WriteLine("✅ [断路器] 远程调用成功。");
}
catch (Exception ex)
{
_failureCount++;
Console.WriteLine($"⚠️ [断路器] 调用失败 ({ex.Message})。当前连续失败次数: {_failureCount}");

if (_failureCount >= _failureThreshold)
{
_isOpen = true; // 超过阈值,立刻跳闸!
Console.WriteLine("💥 [断路器] 失败次数过多,正式跳闸!");
}
}
}
}

// === 测试运行 ===
// var breaker = new SimpleCircuitBreaker();
// 模拟连续三次网络超时,你会发现第四次调用时,甚至都不会触发真实的 Exception,直接被断路器秒拒。

MVVM (Model-View-ViewModel)

核心思想: View(界面)和 ViewModel(逻辑)通过“数据绑定(Data Binding)”自动同步,互相不写死对方的代码。

在 C# 中,MVVM 的灵魂是 INotifyPropertyChanged 接口。当数据变了,它会大喊一声“我变了!”,界面听到后就会自动更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

// 1. Model (纯数据):只关心数据本身
public class User
{
public string Name { get; set; }
}

// 2. ViewModel (逻辑大脑):包装 Model,提供给 View 绑定,并实现通知接口
public class UserViewModel : INotifyPropertyChanged
{
private User _user = new User();
public event PropertyChangedEventHandler PropertyChanged;

// 当这个属性被修改时,会自动触发通知
public string UserName
{
get => _user.Name;
set
{
if (_user.Name != value)
{
_user.Name = value;
// 通知界面:UserName 这个属性变啦,赶紧刷新!
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UserName)));
}
}
}
}

// 3. View (界面):假装这是一个 WPF 的文本框
public class UserView
{
public UserView(UserViewModel viewModel)
{
// 界面“绑定” ViewModel 的事件
viewModel.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == "UserName")
{
Console.WriteLine($"[UI界面] 检测到数据变化,屏幕已自动刷新为:{viewModel.UserName}");
}
};
}
}

// === 测试 ===
// var vm = new UserViewModel { UserName = "张三" }; // 初始状态
// var view = new UserView(vm); // 界面绑定
// vm.UserName = "李四"; // 只要改逻辑层数据,UI界面就会自动打印刷新

生产者-消费者模式 (Producer-Consumer)

核心思想: 解决速度不匹配。制造数据的(生产者)和处理数据的(消费者)通过一个“阻塞队列”解耦。这就是我们之前重点讨论过的机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public class ProducerConsumerExample
{
public static void Run()
{
// 核心:C# 官方提供的线程安全、自带阻塞功能的队列
var queue = new BlockingCollection<string>(boundedCapacity: 10);

// 1. 消费者线程 (一直在后台等吃的)
Task.Run(() =>
{
try
{
// Take 会自动阻塞,没数据就睡觉,有数据就醒来拿
while (true)
{
string item = queue.Take();
Console.WriteLine($"[消费者] 消费了: {item}");
Thread.Sleep(500); // 假装处理得很慢
}
}
catch (InvalidOperationException)
{
Console.WriteLine("[消费者] 队列已关闭,下班!");
}
});

// 2. 生产者线程 (拼命做吃的)
for (int i = 1; i <= 5; i++)
{
string item = $"包子 {i} 号";
queue.Add(item);
Console.WriteLine($"[生产者] 生产了: {item}");
}

// 生产完毕,给队列贴封条
queue.CompleteAdding();
}
}

读写锁 (Read-Write Lock)

核心思想: 针对“读多写少”的场景进行性能优化。允许多个线程同时读,但只要有人在写,其他所有读和写都必须排队。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.Threading;
using System.Threading.Tasks;

public class ReadWriteLockExample
{
// 官方推荐的读写锁对象
private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private static int _sharedData = 0;

public static void Run()
{
// 开启 3 个读线程:它们可以瞬间“同时”进去读取,互不干扰
for (int i = 0; i < 3; i++)
{
Task.Run(() => ReadData());
}

// 开启 1 个写线程:它一进来,必须等前面的读完,并且它写的时候,别人都不准读
Task.Run(() => WriteData());
}

static void ReadData()
{
_rwLock.EnterReadLock(); // 获取【读锁】
try
{
Console.WriteLine($"[读线程 {Thread.CurrentThread.ManagedThreadId}] 正在读取数据: {_sharedData}");
Thread.Sleep(1000); // 假装读得很慢
}
finally { _rwLock.ExitReadLock(); }
}

static void WriteData()
{
_rwLock.EnterWriteLock(); // 获取【写锁】 (排他锁)
try
{
_sharedData = 100;
Console.WriteLine($"🚨 [写线程 {Thread.CurrentThread.ManagedThreadId}] 修改了数据为: {_sharedData}");
}
finally { _rwLock.ExitWriteLock(); }
}
}

线程池 (Thread Pool)

预先建好一批线程随时待命。任务来了就拿一个去用,用完还给池子,避免了频繁“创建/销毁线程”的巨大系统开销。

现代 C# 最常使用的 Task.Run 底层其实就是偷偷交给了系统级的 ThreadPool 来跑的。这里我展示两种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Threading;
using System.Threading.Tasks;

public class ThreadPoolExample
{
public static void Run()
{
// 写法 1:最老牌、最底层的原生线程池调用
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine($"[底层线程池] 正在执行任务。使用的是线程池线程吗?{Thread.CurrentThread.IsThreadPoolThread}");
});

// 写法 2:现代 C# 最标准的写法 (基于 TPL 任务并行库)
// Task.Run 的底层,99% 的情况下也是自动去 ThreadPool 里抓一个线程来用
Task.Run(() =>
{
Console.WriteLine($"[Task 任务] 正在执行任务。使用的是线程池线程吗?{Thread.CurrentThread.IsThreadPoolThread}");
});
}
}

观察者模式 (Observer)

A 对象发生变化时,自动通知所有关注了 A 的 B/C/D 对象。正如我们前面探讨的,C# 的 event 关键字就是宇宙最正宗的观察者模式语法糖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using System;

// 1. 目标 (Subject):负责产生事件
public class Heater
{
private int _temperature;

// 声明一个标准的事件(底层就是一个多播委托/观察者列表)
public event EventHandler<int> TemperatureChanged;

public void BoilWater()
{
for (int i = 98; i <= 100; i++)
{
_temperature = i;
Console.WriteLine($"[热水器] 当前温度:{i}度");

// 温度一旦改变,亲自主动去通知所有注册了的观察者
TemperatureChanged?.Invoke(this, _temperature);
}
}
}

// 2. 观察者 A (Observer)
public class Alarm
{
public void MakeAlert(object sender, int temperature)
{
if (temperature >= 100)
Console.WriteLine("🚨 [报警器] 嘀嘀嘀!水开了!");
}
}

// 3. 观察者 B (Observer)
public class Display
{
public void ShowMsg(object sender, int temperature)
{
if (temperature >= 100)
Console.WriteLine("📺 [显示屏] 水已烧开,请注意安全。");
}
}

// === 测试 ===
// var heater = new Heater();
// var alarm = new Alarm();
// var display = new Display();
//
// 观察者通过 += 向目标注册自己(物理耦合绑定)
// heater.TemperatureChanged += alarm.MakeAlert;
// heater.TemperatureChanged += display.ShowMsg;
//
// heater.BoilWater(); // 只要一烧开,警报器和显示屏会自动响应