Speaking of observer mode, you can probably find a lot of them in the garden. So the purpose of writing this blog is twofold:
1. The observer pattern is a necessary pattern for writing loosely coupled code. Its importance is self-evident. Regardless of the code level, many components adopt the Publish-Subscribe pattern. So I want to redesign a usage scenario according to my own understanding and use the observer pattern flexibly in it
2. I want to make a summary of the three solutions to implement the observer pattern in C#. I haven’t seen such a summary yet
Now let’s assume such a scenario and use the observer mode to realize the requirements:
In the future, smart homes will enter every household, and each home will have APIs for customers to customize and integrate, so the first smart alarm clock ( smartClock) comes on the scene first. The manufacturer provides a set of APIs for this alarm clock. When an alarm time is set, the alarm clock will notify you at this time. Our smart milk heater, bread baking machine, and toothpaste squeezing equipment all need to be subscribed. This alarm clock alarm message automatically prepares milk, bread, toothpaste, etc. for the owner.
This scenario is a very typical observer mode. The alarm clock of the smart alarm clock is a subject, and the milk warmer, bread baking machine, and toothpaste squeezing equipment are observers. They only need to subscribe to this topic. Implement a loosely coupled coding model. Let's implement this requirement through three options one by one.
1. Use the Event model of .net to implement
The Event model in .net is a typical observer pattern. It has been widely used in codes after .net was born. Let’s see how the event model can be used in this For use in scenarios,
First introduce the smart alarm clock. The manufacturer provides a set of very simple API
public void SetAlarmTime(TimeSpan timeSpan) { _alarmTime = _now().Add(timeSpan); RunBackgourndRunner(_now, _alarmTime); }
SetAlarmTime(TimeSpan timeSpan) is used for timing. When the user sets a time, the alarm clock will run a loop similar to while(true) in the background to compare the time. When the alarm time is up, a notification event will be sent out
protected void RunBackgourndRunner(Func<DateTime> now,DateTime? alarmTime ) { if (alarmTime.HasValue) { var cancelToken = new CancellationTokenSource(); var task = new Task(() => { while (!cancelToken.IsCancellationRequested) { if (now.AreEquals(alarmTime.Value)) { //闹铃时间到了 ItIsTimeToAlarm(); cancelToken.Cancel(); } cancelToken.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(2)); } }, cancelToken.Token, TaskCreationOptions.LongRunning); task.Start(); } }
Other codes are not Important, the key point is to execute ItIsTimeToAlarm() when the alarm time is up; We send events here to notify subscribers. There are three elements to implement the event model in .net,
1. Define an event for the subject, public event Action
2. Define an EventArgs for the subject’s information, namely AlarmEventArgs, which contains all the information of the event
3. The subject emits events in the following ways
var args = new AlarmEventArgs(_alarmTime.Value, 0.92m); OnAlarmEvent(args);
Definition of the OnAlarmEvent method
public virtual void OnAlarm(AlarmEventArgs e) { if(Alarm!=null) Alarm(this,e); }
Pay attention to naming here, event content-AlarmEventArgs, event-Alarm (verb, such as KeyPress), method to trigger the event void
OnAlarm(), these elements must comply with the naming convention of the event model.
The smart alarm clock (SmartClock) has been implemented. We subscribe to this Alarm message in the milk heater (MilkSchedule):
public void PrepareMilkInTheMorning() { _clock.Alarm += (clock, args) => { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( args.AlarmTime, args.ElectricQuantity*100); Console.WriteLine(Message); }; _clock.SetAlarmTime(TimeSpan.FromSeconds(2)); }
It can also be used in the bread baking machine _clock.Alarm+=(clock,args)=> {//it is time to roast bread}subscribe to alarm messages.
At this point, the event model has been introduced. The implementation process is still a bit cumbersome, and improper use of the event model will cause memory The problem of leak is that when the observer subscribes to a topic with a long life cycle (the topic life cycle is longer than the observer), the observer will not be memory recycled (because there are still references to the topic), see Understanding for details and Avoiding Memory Leaks with Event Handlers and Event Aggregators, developers need to explicitly unsubscribe from the topic (-=).
Old A in the garden also wrote a blog on how to use weak references to solve this problem: How to solve the Memory Leak problem caused by events: Weak Event Handlers.
2. Use IObservable
IObservable
In our scenario, the smart alarm clock is IObservable. This interface only defines one method IDisposable Subscribe(IObserver
public IDisposable Subscribe(IObserver<AlarmData> observer) { if (!_observers.Contains(observer)) { _observers.Add(observer); } return new DisposedAction(() => _observers.Remove(observer)); }
You can see that an observer list_observers is maintained here. After the alarm clock reaches the time, it will traverse all the observer lists and notify the observers one by one of the messages
public override void ItIsTimeToAlarm() { var alarm = new AlarmData(_alarmTime.Value, 0.92m); _observers.ForEach(o=>o.OnNext(alarm)); }
Obviously, the observer has an OnNext method. The method signature is an AlarmData, which represents the message data to be notified. Next, let’s look at the implementation of the milk heater. As an observer, the milk heater must of course implement IObserver Interface
public void Subscribe(TimeSpan timeSpan) { _unSubscriber = _clock.Subscribe(this); _clock.SetAlarmTime(timeSpan); } public void Unsubscribe() { _unSubscriber.Dispose(); } public void OnNext(AlarmData value) { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( value.AlarmTime, value.ElectricQuantity * 100); Console.WriteLine(Message); }
In addition, in order to facilitate the use of the bread baker, we have also added two methods, Subscribe() and Unsubscribe(), see the calling process
var milkSchedule = new MilkSchedule(); //Act milkSchedule.Subscribe(TimeSpan.FromSeconds(12));
3. Action functional solution
Before introducing the solution I need to explain that this solution is not an observer model, but it can achieve the same function and is simpler to use, which is also one of my favorite uses.
这种方案中,智能闹钟(smartClock)提供的API需要设计成这样:
public void SetAlarmTime(TimeSpan timeSpan,Action<AlarmData> alarmAction) { _alarmTime = _now().Add(timeSpan); _alarmAction = alarmAction; RunBackgourndRunner(_now, _alarmTime); }
方法签名中要接受一个Action
public override void ItIsTimeToAlarm() { if (_alarmAction != null) { var alarmData = new AlarmData(_alarmTime.Value, 0.92m); _alarmAction(alarmData); } }
牛奶加热器中使用这种API也很简单:
_clock.SetAlarmTime(TimeSpan.FromSeconds(1), (data) => { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( data.AlarmTime, data.ElectricQuantity * 100); });
在实际使用过程中我会把这种API设计成fluent模型,调用起来代码更清晰:
智能闹钟(smartClock)中的API:
public Clock SetAlarmTime(TimeSpan timeSpan) { _alarmTime = _now().Add(timeSpan); RunBackgourndRunner(_now, _alarmTime); return this; } public void OnAlarm(Action<AlarmData> alarmAction) { _alarmAction = alarmAction; }
牛奶加热器中进行调用:
_clock.SetAlarmTime(TimeSpan.FromSeconds(2)) .OnAlarm((data) => { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( data.AlarmTime, data.ElectricQuantity * 100); });
显然改进后的写法语义更好:闹钟.设置闹铃时间().当报警时(()=>{执行以下功能})
这种函数式写法更简练,但是也有明显的缺点,该模型不支持多个观察者,当面包烘烤机使用这样的API时,会覆盖牛奶加热器的函数,即每次只支持一个观察者使用。
结束语,本文总结了.net下的三种观察者模型实现方案,能在编程场景下选择最合适的模型当然是我们的最终目标。