Three implementations of the Observer pattern in C

  • 2021-01-18 06:37:07
  • OfStack

Speaking of the observer mode, I think I could find a pile in the garden. So the purpose of writing this blog is twofold:

1. Observer mode is a necessary mode for writing loosely coupled code, and its importance is self-evident. Regardless of the code level, many components adopt Publish-Subscribe mode, so I want to re-design a usage scenario according to my own understanding and flexibly use Observer mode in it
2. I would like to make a summary of the three implementations of the Observer pattern in C#, but I haven't seen such a summary yet

Now let's assume a scenario like this and implement the requirements using the Observer pattern:

Future smart home has entered every household, every household have API custom integration for customers, so the first intelligent alarm clock (smartClock) first, manufacturers to provide 1 set of API alarm clock, when setting an alarm clock time after the alarm clock will make a notice at this time, the intelligence of our milk heaters, bread baking machine, squeezing devices to subscribe to this alarm clock alarm messages, automatically give priority to people ready to milk, bread, toothpaste, etc.

This scenario is a typical observer mode. The alarm of the intelligent alarm clock is a topic (subject), and the milk heater, bread toaster and toothpaste extrusion equipment are observers (observer). They only need to subscribe to this topic to realize the loosely coupled coding model. Let's implement this requirement one by one through three scenarios.

1. Use the Event model of.net to achieve

The Event model in.net is a typical observer pattern that has been widely used in code since.net came into being. Let's look at how the event model can be used in this scenario.

First of all, introduce the intelligent alarm clock, manufacturers provide a group 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 cycle of comparison time similar to while(true) in the background. When the alarm time is up, a notification event will be issued


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))
                        {
                            // It's time for the alarm
                            ItIsTimeToAlarm();
                            cancelToken.Cancel();
                        }
                        cancelToken.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(2));
                    }
                }, cancelToken.Token, TaskCreationOptions.LongRunning);
                task.Start();
            }
        }

The rest of the code is not important, it is important to execute ItIsTimeToAlarm() when the alarm time is up; We issue an event here to inform subscribers that there are three elements in.net to implement the ES46en model,

1. For the topic (subject), you need to define 1 event, public event Action < Clock, AlarmEventArgs > Alarm;

2. Define an EventArgs (AlarmEventArgs) for the information of the topic (subject), which contains all the information of the event

3. Topics (subject) emit events in the following way


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);
       }

Note the naming, the event content -AlarmEventArgs, the event -Alarm (verb, for example, KeyPress), and the method that triggers the event, void OnAlarm(), all of which conform to 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));
 
        }

In the bread oven, you can also use _clock.Alarm+=(clock,args)= > {//it is time to roast bread} subscribe alarm messages.

When an observer subscribes to a topic with a long lifetime (the topic has a longer lifetime than the observer), the observer will not be collected (because there is also a reference to the topic). Understanding and Memory Leaks with Event Handlers and Event Aggregators For more information, see Understanding and Memory with Handlers and Event Aggregators.

The old A in the garden also wrote a blog on how to solve the problem with weak references: How to solve the problem with Memory Leak caused by the event: Weak Event Handlers.

2. Use IObservable from net < out T > And IObserver < in T > Implementing the Observer pattern

IObservable < out T > Just as the name implies the observable, namely the subject (subject),Observer is clearly the observer.

In our scenario, the smart alarm clock is IObservable. This interface defines only one method, IDisposable Subscribe(IObserver) < T > observer); This method is a bit confusingly named,Subscribe stands for subscription, as opposed to the previously mentioned observer (observer) subscription topic (subject). In this case, the topic (subject) is used to subscribe to the observer (observer), which actually makes sense because in this model, the topic (subject) maintains a list of observers (observer), so with the topic subscription observer, let's look at the alarm clock's IDisposable, Subscribe(IObserver) < T > observer) implementation:


public IDisposable Subscribe(IObserver<AlarmData> observer)
        {
            if (!_observers.Contains(observer))
            {
                _observers.Add(observer);
            }
            return new DisposedAction(() => _observers.Remove(observer));
        }

You can see that there is a list of observers _observers maintained, and the alarm clock will traversal the list of all observers and notify the observer one by one when the time is up


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, and 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. The milk heater, as the observer (observer), of course implements 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(
                      value.AlarmTime, value.ElectricQuantity * 100);
           Console.WriteLine(Message);
       }

To make it easier to use the bread toaster, we have added two methods, Subscribe() and Unsubscribe(), to see how it is called


var milkSchedule = new MilkSchedule();
//Act
milkSchedule.Subscribe(TimeSpan.FromSeconds(12));

3. Action functional scheme

Before I introduce this solution, I should note that it is not an observer model, but it does the same thing and is much more concise to use, which is one of my favorite uses.

In this scenario, the API provided by the Smart Alarm Clock (smartClock) needs to be designed like this:


public void SetAlarmTime(TimeSpan timeSpan,Action<AlarmData> alarmAction)
       {
           _alarmTime = _now().Add(timeSpan);
           _alarmAction = alarmAction;
           RunBackgourndRunner(_now, _alarmTime);
       }

One Action is accepted in the method signature < T > , the alarm clock executes the Action directly after the clock has hit < T > You can:


public override void ItIsTimeToAlarm()
       {
           if (_alarmAction != null)
           {
               var alarmData = new AlarmData(_alarmTime.Value, 0.92m);
               _alarmAction(alarmData);   
           }
       }

It is also easy to use API in milk heaters:


_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);
            });

In actual use, I will design this API model as fluent model, and the code is clearer to call:

Intelligent Alarm Clock (smartClock) API:


public Clock SetAlarmTime(TimeSpan timeSpan)
        {
            _alarmTime = _now().Add(timeSpan);
            RunBackgourndRunner(_now, _alarmTime);
            return this;
        }
 
        public void OnAlarm(Action<AlarmData> alarmAction)
        {
            _alarmAction = alarmAction;
        }

The milk heater is called:


_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);
                });

Apparently the improved version has better semantics: alarm clock. Set the alarm time (). When the alarm (()= > {perform the following functions})

This function is more concise, but has a significant drawback. The model does not support multiple observers. When the bread toaster uses API, it overwrites the milk heater function, which supports only one observer at a time.

In conclusion, this paper summarizes three implementations of Observer Model under net, and it is our ultimate goal to choose the most appropriate model in programming scenarios. This article provides the source code used to download this article, if you need to reprint, please indicate the source


Related articles: