Speaking of observer mode, I guess a lot of them can be found in the garden. So there are two purposes for writing this blog:
1. Observer mode is a necessary mode for writing loosely coupled code. The importance is self-evident. Putting aside the code level, many components adopt the Publish-Subscribe mode, so I want to redesign a usage scenario according to my own understanding and use the Observer mode flexibly in it.
2. I want to summarize 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 model to achieve the requirements:
In the future, smart homes will enter every household, and each home will have an API for customers to customize and integrate, so the first smart alarm clock (smartClock) will be launched first. The manufacturer provides a set of APIs for this alarm clock. When an alarm time is set, the alarm clock will be notified at this time. Our smart milk heater, bread baker, and toothpaste squeezing equipment must subscribe to this alarm clock alarm message and automatically prepare milk, bread, toothpaste, etc. for the owner.
This scenario is a typical observer mode. The alarm of the smart alarm is a subject, a milk heater, a bread baker, and a toothpaste squeezing device are an observer. They only need to subscribe to this topic to achieve a loosely coupled coding model. Let's implement this requirement one by one through three solutions.
1. Use the .net Event model to implement
The Event model in .net is a typical observer model. After being born in .net, it has been widely used in code. Let’s see how the event model is used in this scenario.
First, let’s introduce the smart alarm clock. The manufacturer provides a very simple set of APIs
public void SetAlarmTime(TimeSpan timeSpan)
{
_alarmTime = _now().Add(timeSpan);
RunBackgourndRunner(_now, _alarmTime);
}
SetAlarmTime(TimeSpan timeSpan) is used to schedule. When the user sets a time, the alarm clock will run a loop similar to while(true) in the background. When the alarm time reaches, a notification event will be issued.
protected void RunBackgourndRunner(Func<DateTime> now,DateTime? alarmTime )
{
if ()
{
var cancelToken = new CancellationTokenSource();
var task = new Task(() =>
{
while (!)
{
if (())
{
//The alarm time is up
ItIsTimeToAlarm();
();
}
((2));
}
}, , );
();
}
}
The other code is not important, the focus is to execute ItIsTimeToAlarm() when the alarm time is up; we issue events here to notify subscribers. There are three elements to implement the event model in .net.
1. For the subject (subject), you need to define an event, public event Action<Clock, AlarmEventArgs> Alarm;
2. Define an EventArgs for the subject information, that is, AlarmEventArgs, which contains all the information of the event.
3. Subject issues events in the following ways
var args = new AlarmEventArgs(_alarmTime.Value, 0.92m);
OnAlarmEvent(args);
Definition of 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 events void OnAlarm(), these elements must comply with the naming specifications of the event model.
The smart alarm clock (SmartClock) has been implemented, and we subscribe to this Alarm message in the MilkSchedule:
public void PrepareMilkInTheMorning()
{
_clock.Alarm += (clock, args) =>
{
Message =
"Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith(
, *100);
(Message);
};
_clock.SetAlarmTime((2));
}
In the bread baker, you can also use _clock.Alarm+=(clock,args)=>{//it is time to roast bread} to subscribe to alarm messages.
At this point, the event model has been introduced, and the implementation process is still a bit cumbersome, and the event model is not used properly will have memory leaks. When the observer subscribes to a topic with a long life cycle (the life cycle of the topic is longer than the observer), the observer will not be recycled by memory (because there are also references to the topic). For details, seeUnderstanding and Avoiding Memory Leaks with Event Handlers and Event Aggregators, the developer needs to display unsubscribe to the topic (-=).
Old A, Yuanzili, also wrote a blog about how to use weak quotes to solve this problem:How to resolve Memory Leak issues caused by events: Weak Event Handlers。
2. Use IObservable<out T> and IObserver<in T> in .net to implement the observer mode
IObservable<out T> Just as the name means - observable things, that is, subject, Observer is obviously the observer.
In our scenario, the smart alarm clock is IObservable. This interface only defines one method IDisposable Subscribe(IObserver<T> observer); the name of this method makes people feel a little dizzy. Subscribe means subscription, which is different from the previously mentioned observer (observer) subscription topic (subject). Here is the subject to subscribe to the observer (observer), which makes sense here, because under this model, the subject maintains an observer (observer) list, so there is a saying of subscribe to the observer. Let's look at the IDisposable Subscribe (IObserver<T> observer) implementation of the alarm clock:
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 is maintained here. After the alarm clock reaches the junction, it will traverse all observer lists and notify the observer one by one.
public override void ItIsTimeToAlarm()
{
var alarm = new AlarmData(_alarmTime.Value, 0.92m);
_observers.ForEach(o=>(alarm));
}
It is obvious that the observer has an OnNext method, and the method signature is an AlarmData, which represents the message data to be notified. Let’s take a look at the implementation of the milk heater. As an observer (observer), the milk heater must of course implement the 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(
, * 100);
(Message);
}
In addition, in order to facilitate the use of bread baker, we also added two methods Subscribe() and Unsubscribe() to see the call process.
var milkSchedule = new MilkSchedule();
//Act
((12));
3. Action functional solution
Before introducing this solution, I need to explain that this solution is not an observer model, but it can achieve the same function and is more concise to use, which is also my favorite usage.
In this solution, the API provided by smart alarm clock needs to be designed like this:
public void SetAlarmTime(TimeSpan timeSpan,Action<AlarmData> alarmAction)
{
_alarmTime = _now().Add(timeSpan);
_alarmAction = alarmAction;
RunBackgourndRunner(_now, _alarmTime);
}
In the method signature, you must accept an Action<T>, and then execute the Action<T> directly after the alarm clock reaches the point:
public override void ItIsTimeToAlarm()
{
if (_alarmAction != null)
{
var alarmData = new AlarmData(_alarmTime.Value, 0.92m);
_alarmAction(alarmData);
}
}
It is also easy to use this API in a milk heater:
_clock.SetAlarmTime((1), (data) =>
{
Message =
"Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith(
, * 100);
});
In actual use, I will design this API as a fluent model, and the code will be clearer when calling:
API in smart alarm clock (smartClock):
public Clock SetAlarmTime(TimeSpan timeSpan)
{
_alarmTime = _now().Add(timeSpan);
RunBackgourndRunner(_now, _alarmTime);
return this;
}
public void OnAlarm(Action<AlarmData> alarmAction)
{
_alarmAction = alarmAction;
}
Calling in the milk heater:
_clock.SetAlarmTime((2))
.OnAlarm((data) =>
{
Message =
"Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith(
, * 100);
});
Obviously, the improved writing semantics are better: alarm clock. Set alarm time(). When alarm is called (()=>{execute the following functions})
This functional writing is more concise, but it also has obvious disadvantages. The model does not support multiple observers. When the bread roaster uses such an API, it will cover the function of the milk heater, that is, only one observer is supported at a time.