zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C#下 观察者模式的另一种实现方式IChangeToken和ChangeToken.OnChange

c#模式 实现 方式 一种 观察者 onchange
2023-09-11 14:21:53 时间

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,到这里看结果,如下:

 

 没问题,到这里结束,如有问题,请指正,谢谢.