C#下 观察者模式的另一种实现方式IChangeToken和ChangeToken.OnChange
1、观察者模式
关于观察者模式,不在赘述,本质是利用event和handler配合,event本质是handler的实例,通过在event挂载事件实现内存级别的程序的通知机制.下面通过代码来展示常用观察者模式的构建方式.
(1)、需求
假设X工厂有一台锅炉,现在需要一个监测程序,该程序监测锅炉的温度,当温度达到66读,提醒相关的控制电脑.
核心代码如下:
public delegate void EventHandler(); /// <summary> /// 机器类 /// </summary> public class TraditionMachine { public string Name { get { return "传统锅炉"; } } public event EventHandler _temperatureWarnHandler; public void Run() { for (var i = 0; i < 100; i++) { //模拟温度达到66读触发警报 if (i == 66) { //经过一系列的计算 var computeValue = i; _temperatureWarnHandler.Invoke(); } } } } /// <summary> /// 温度警报参数,该参数存放一些不属于Machine属性的值,这里面的参数通常是经过计算得到的 /// 如果传递的是Machine属性,则直接通过this传递了(因为sender类型是object所以可以传递任意类型) /// </summary> public class TemperatureWarnEventArgs : EventArgs { public int ComputeVlaue { get; set; } public TemperatureWarnEventArgs(int computeVlaue) { ComputeVlaue = computeVlaue; } } /// <summary> /// 电脑A /// </summary> public class ComputerA { /// <summary> /// 电脑弹框警报 /// </summary> public void ShowWarnBox() { Console.WriteLine($"电脑A发出警报温度过高"); } } /// <summary> /// 电脑B /// </summary> public class ComputerB { /// <summary> /// 电脑弹框警报 /// </summary> public void ShowWarnBox() { Console.WriteLine($"电脑B发出警报温度过高"); } }
调用代码如下:
var machine = new TraditionMachine(); var computerA = new ComputerA(); var computerB = new ComputerB(); machine._temperatureWarnHandler += computerA.ShowWarnBox; machine._temperatureWarnHandler += computerB.ShowWarnBox; machine.Run();
输出如下:
(2)、通过IChangeToken的模式改写
在进行改写时,需要知道以下几个要点
i、CancellationChangeToken的用法,如下代码:
var tokenSource = new CancellationTokenSource(); var changeToken = new CancellationChangeToken(tokenSource.Token); changeToken.RegisterChangeCallback(obj => { Console.WriteLine(obj.ToString()); }, "传入的参数"); tokenSource.Cancel();
简单解释下这段代码这里通过CancellationTokenSource创建一个CancellationToken实例,将实例传给CancellationChangeToken,这个类会做什么呢?看如下核心代码:
public IDisposable RegisterChangeCallback(Action<object> callback, object state) { try { return Token.UnsafeRegister(callback, state); } catch (ObjectDisposedException) { ActiveChangeCallbacks = false; } return NullDisposable.Instance; }
通过CancellationChangeToken注册回调,实际是向CancellationToken实例注册回调
Token.UnsafeRegister(callback, state);
,而CancellationToken实例是被CancellationTokenSource持有,所以调用CancellationTokenSource实例的Cancel方法时,会触发CancellationChangeToken注册的回调方法,所以输出如下:
上面的实现,时C# fcl提供的,netcore大量的核心组件通过这个结构实现了观察者模式,如配置文件组件,和缓存组件,有兴趣的话可以研究下,CancellationTokenSource和CancellationToken时如何实现这种机制的,有机会的话,本人会写一篇博客介绍.
ok,知道CancellationTokenSource和CancellationToken的机制后,改写代码就很容易了.核心代码如下:
/// <summary> /// 机器类 /// </summary> public class TraditionMachine { private CancellationTokenSource _computerATokenSource; private CancellationTokenSource _computerBTokenSource; public TraditionMachine(CancellationTokenSource computerBTokenSource, CancellationTokenSource computerATokenSource) { _computerBTokenSource = computerBTokenSource; _computerATokenSource = computerATokenSource; } public string Name { get { return "传统锅炉"; } } public void Run() { for (var i = 0; i < 100; i++) { //模拟温度达到66读触发警报 if (i == 66) { //经过一系列的计算 var computeValue = i; _computerATokenSource.Cancel(); _computerBTokenSource.Cancel(); } } } } /// <summary> /// 电脑A /// </summary> public class ComputerA { /// <summary> /// 电脑弹框警报 /// </summary> public static void ShowWarnBox() { Console.WriteLine($"电脑A发出警报温度过高"); } } /// <summary> /// 电脑B /// </summary> public class ComputerB { /// <summary> /// 电脑弹框警报 /// </summary> public static void ShowWarnBox() { Console.WriteLine($"电脑B发出警报温度过高"); } }
调用代码如下:
static void Main() { var computerATokenSource = new CancellationTokenSource(); var computerBTokenSource = new CancellationTokenSource(); //这里只是测试下CreateLinkedTokenSource方法的用法,和核心逻辑无关 var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(computerATokenSource.Token, computerBTokenSource.Token); var computerAChangeToken = new CancellationChangeToken(computerATokenSource.Token); var computerBChangeToken = new CancellationChangeToken(computerBTokenSource.Token); computerAChangeToken.RegisterChangeCallback(obj => { ComputerA.ShowWarnBox(); }, null); computerBChangeToken.RegisterChangeCallback(obj => { ComputerB.ShowWarnBox(); }, null); tokenSource.Token.Register(() => { Console.WriteLine("有且有一台电脑报警了"); }); var machine = new TraditionMachine(computerATokenSource, computerBTokenSource); machine.Run(); Console.ReadKey(); }
如果非说在两者中找一个优缺点,说实话,本人看不出来,但是后者看上去更加的直观,且CancellationTokenSource,可以作成集合形式,且配合其他数据结构如字典,那么主程序修改订阅方更加的方便且灵活性更高,我猜这也是netcore底层大量采用这种设计的原因.灵活且直观.
(3)、IChangeToken进一步的用法 ChangeToken.OnChange的用法
到这里,上面的代码完成了观察者模式的需求,但是有一个问题,调用代码如下:
static void Main() { var computerATokenSource = new CancellationTokenSource(); var computerBTokenSource = new CancellationTokenSource(); //这里只是测试下CreateLinkedTokenSource方法的用法,和核心逻辑无关 var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(computerATokenSource.Token, computerBTokenSource.Token); var computerAChangeToken = new CancellationChangeToken(computerATokenSource.Token); var computerBChangeToken = new CancellationChangeToken(computerBTokenSource.Token); computerAChangeToken.RegisterChangeCallback(obj => { ComputerA.ShowWarnBox(); }, null); computerBChangeToken.RegisterChangeCallback(obj => { ComputerB.ShowWarnBox(); }, null); tokenSource.Token.Register(() => { Console.WriteLine("有且有一台电脑报警了"); }); var machine = new TraditionMachine(computerATokenSource, computerBTokenSource); machine.Run(); machine.Run(); Console.ReadKey(); }
调用了两次Run方法,理论上回报警两次,但是输出如下:
说明CancellationToken是一次性,第一次调用完之后,第二次不在会被触发.这样肯定不符合我们的需求,ok,那如何解决这个问题,FCL中提供了ChangeToken.OnChange方法,介绍下核心用法,源码如下:
// // 摘要: // Registers the changeTokenConsumer action to be called whenever the token produced // changes. // // 参数: // changeTokenProducer: // Produces the change token. // // changeTokenConsumer: // Action called when the token changes. public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer) { if (changeTokenProducer == null) { throw new ArgumentNullException("changeTokenProducer"); } if (changeTokenConsumer == null) { throw new ArgumentNullException("changeTokenConsumer"); } return new ChangeTokenRegistration<Action>(changeTokenProducer, delegate (Action callback) { callback(); }, changeTokenConsumer); }
上来还是参数校验,接着生成了一个ChangeTokenRegistration的实例,看这个实例的源码:
public ChangeTokenRegistration(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state) { _changeTokenProducer = changeTokenProducer; _changeTokenConsumer = changeTokenConsumer; _state = state; IChangeToken token = changeTokenProducer(); RegisterChangeTokenCallback(token); }
构造函数中调用了ChangeToken令牌生产者生成一个ChangeToken实例,接着看RegisterChangeTokenCallback方法干了什么,源码如下:
private void RegisterChangeTokenCallback(IChangeToken token) { IDisposable disposable = token.RegisterChangeCallback(delegate (object s) { ((ChangeTokenRegistration<TState>)s).OnChangeTokenFired(); }, this); SetDisposable(disposable); }
这里就熟悉了,调用了ChangeToken实例的注册回调方法,上面说了当CancellationTokenSource触发Cancel方法是,ChangeToken实例会触发注册的回调,也就是上面的代码.其实就是触发ChangeTokenRegistration实例的OnChangeTokenFired方法,接着看源码,如下:
private void OnChangeTokenFired() { IChangeToken token = _changeTokenProducer(); try { _changeTokenConsumer(_state); } finally { RegisterChangeTokenCallback(token); } }
这里再次调用ChangeToken生产者,再次生成其实例,接着执行ChangeToken.OnChange的回调,然后再次注册ChangeToken实例的注册回调方法,这里很明显是递归的.下面检验下.
var tokenSource = new CancellationTokenSource(); ChangeToken.OnChange(() => new CancellationChangeToken(tokenSource.Token), () => { Console.WriteLine("解决CancellationChangeToken注册的回调,注册一次只能被触发一次的问题,但是当前用法会递归"); }); tokenSource.Cancel();
果然递归了,说明不能直接使用虽然ChangeToken.OnChange虽然解决了CancellationChangeToken注册的回调,注册一次只能被触发一次的问题,但是这里他是先触发一次传入的自定义回调后,接着重新注册我们的回调到ChangeToken实例,但是其实例被CancellationTokenSource持有,当调用其Cancel方法时,所有ChangeToken实例注册的回调全都会被触发,导致递归且内存溢出.这间接说明了通过CancellationTokenSource和CancellationChangeToken实现的观察者模式,其注册的回调只能被触发一次,不能像传统的模式那样使用,但是可以将他们当作一个实现观察者的实例类型来使用,并放到集合里面.接着改造代码:
static void Main() { var machine = new TraditionMachine(); ChangeToken.OnChange(() => machine.Watch("A", 66), () => { ComputerA.ShowWarnBox(); }); ChangeToken.OnChange(() => machine.Watch("B", 66), () => { ComputerB.ShowWarnBox(); }); machine.Run(); machine.Run(); Console.ReadKey(); } /// <summary> /// 机器类 /// </summary> public class TraditionMachine { private ConcurrentDictionary<string, WatchTokenInfo> _watchTokens = new ConcurrentDictionary<string, WatchTokenInfo>(); public string Name { get { return "传统锅炉"; } } private int _condition; public IChangeToken Watch(string computerName,int condition) { _condition = condition; if (!_watchTokens.TryGetValue(computerName,out var _watchTokenInfo)) { var tokenSource = new CancellationTokenSource(); var changeToken = new CancellationChangeToken(tokenSource.Token); _watchTokens.TryAdd(computerName, new WatchTokenInfo() { CancellationTokenSource = tokenSource, ChangeToken = changeToken }); return changeToken; } return _watchTokenInfo.ChangeToken; } public void Run() { for (var i = 0; i < 100; i++) { //模拟温度达到66读触发警报 if (i == _condition) { foreach (var token in _watchTokens) { _watchTokens.TryRemove(token.Key, out var _); token.Value.CancellationTokenSource.Cancel(); } } } } /// <summary> /// CancellationTokenSource和IChangeToken组合实例来实现观察者模式 /// </summary> public class WatchTokenInfo { public CancellationTokenSource CancellationTokenSource { get; set; } public IChangeToken ChangeToken { get; set; } } } /// <summary> /// 电脑A /// </summary> public class ComputerA { /// <summary> /// 电脑弹框警报 /// </summary> public static void ShowWarnBox() { Console.WriteLine($"电脑A发出警报温度过高"); } } /// <summary> /// 电脑B /// </summary> public class ComputerB { /// <summary> /// 电脑弹框警报 /// </summary> public static void ShowWarnBox() { Console.WriteLine($"电脑B发出警报温度过高"); } }
这里的设计就很巧妙了,通过调用Watch方法,来生成CancellationTokenSource和IChangeToken组合实例,并写入集合中,让TraditionMachine的Run方法达到指定的条件时,触发所有的注入的回调,但是触发回调前,先移除组合实例,因为如果不移除,还是会导致递归,移除之后,调用Cancel方法,会重新调用ChangeTokeon生产者方法重新生成ChangeToken实例,将CancellationTokenSource和IChangeToken组合实例重新写入集合.此时的CancellationTokenSource和IChangeToken组合实例是全新的实例,changeToken实例也是新创建的,不会发生递归的问题.第二次调用Run方法时,触发的是新的CancellationTokenSource实例的Cancel方法,以此类推,这样就保证了ChangeToken.OnChange注册的自定义回调不会丢失,实现观察者
ok,到这里看结果,如下:
没问题,到这里结束,如有问题,请指正,谢谢.