이 문서에서는 몇 가지 향상된 기능을 갖춘 .NET C#의 관찰자 디자인 패턴에 대해 알아봅니다.
관찰자 디자인 패턴은 가장 중요하고 일반적으로 사용되는 디자인 패턴 중 하나입니다.
먼저 Observer Design Pattern 의 형식적 정의를 확인해 보겠습니다.
에 따라
관찰자 디자인 패턴을 사용하면 구독자가 공급자에 등록하고 공급자로부터 알림을 받을 수 있습니다. 푸시 기반 알림이 필요한 모든 시나리오에 적합합니다. 패턴은 공급자(주체 또는 관찰 가능 항목이라고도 함)와 0개, 1개 이상의 관찰자를 정의합니다. 관찰자는 공급자에 등록하고 미리 정의된 조건, 이벤트 또는 상태 변경이 발생할 때마다 공급자는 메서드 중 하나를 호출하여 모든 관찰자에게 자동으로 알립니다. 이 메서드 호출에서 공급자는 관찰자에게 현재 상태 정보를 제공할 수도 있습니다. .NET에서는 일반 System.IObservable<T> 및 System.IObserver<T> 인터페이스를 구현하여 관찰자 디자인 패턴이 적용됩니다. 일반 유형 매개변수는 알림 정보를 제공하는 유형을 나타냅니다.
우리가 지금 알고 있듯이 Observer 디자인 패턴은 Observable 과 Observer 모듈 간의 관계를 공식화합니다. 관찰자 디자인 패턴을 독특하게 만드는 것은 이를 사용하면 긴밀하게 결합된 관계 없이 이를 달성할 수 있다는 것입니다.
패턴이 작동하는 방식을 분석하면 다음을 찾을 수 있습니다.
이는 .NET C# 에서 관찰자 디자인 패턴을 구현하는 데 사용되는 추상화 입니다.
이것은 모든 Observable을 나타내는 Covariant 인터페이스입니다. .NET의 Variance에 대해 더 자세히 알고 싶다면 기사를 확인하세요.
이 인터페이스에 정의된 멤버는 다음과 같습니다.
public IDisposable Subscribe (IObserver<out T> observer);
Subscribe
메소드는 일부 Observer가 정보 스트림에 관심이 있음을 Observable에 알리기 위해 호출되어야 합니다.
Subscribe
메서드는 IDisposable
인터페이스를 구현하는 개체를 반환합니다. 이 객체는 Observable 이 제공하는 정보 스트림의 구독을 취소하기 위해 Observer 에 의해 사용될 수 있습니다. 이 작업이 완료되면 관찰자는 정보 스트림에 대한 업데이트에 대한 알림을 받지 않습니다.
이는 모든 Observer 를 나타내는 Contravariant 인터페이스입니다. .NET의 Variance에 대해 더 자세히 알고 싶다면 기사를 확인하세요.
이 인터페이스에 정의된 멤버는 다음과 같습니다.
public void OnCompleted (); public void OnError (Exception error); public void OnNext (T value);
OnCompleted
메서드는 Observable 에 의해 호출되어 Observer에게 정보 스트림이 완료되었으며 Observer가 더 이상 정보를 기대하지 않아야 함을 알려야 합니다.
OnError
메소드는 Observable 에 의해 호출되어 Observer 에게 오류가 발생했음을 알려야 합니다.
OnNext
메소드는 Observable 에 의해 호출되어 Observer 에게 새로운 정보가 준비되었으며 스트림에 추가되고 있음을 알려야 합니다.
이제 Microsoft가 C#에서 관찰자 디자인 패턴 구현을 권장하는 방법을 살펴보겠습니다. 나중에 제가 직접 구현한 몇 가지 사소한 개선 사항을 보여 드리겠습니다.
우리는 간단한 일기예보 콘솔 애플리케이션을 구축할 것입니다. 이 애플리케이션에는 WeatherForecast 모듈(Observable, Provider, Subject)과 WeatherForecastObserver 모듈(Observer)이 있습니다.
그럼 구현에 대해 살펴보겠습니다.
namespace Observable { public class WeatherInfo { internal WeatherInfo(double temperature) { Temperature = temperature; } public double Temperature { get; } } }
이는 정보 스트림에 흐르는 정보 조각을 나타내는 엔터티입니다.
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecast : IObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IDisposable Subscribe(IObserver<WeatherInfo> observer) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } return new WeatherForecastUnsubscriber(m_Observers, observer); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
여기서 우리가 알 수 있는 것은:
WeatherForecast
클래스는 IObservable<WeatherInfo>
구현하고 있습니다.Subscribe
메소드 구현에서는 전달된 Observer가 이전에 이미 등록되었는지 확인합니다. 그렇지 않은 경우 로컬 m_Observers
관찰자 목록에 추가합니다. 그런 다음 로컬 m_WeatherInfoList
목록에 있는 모든 WeatherInfo
항목을 하나씩 반복하고 Observer의 OnNext
메서드를 호출하여 Observer에 이에 대해 알립니다.WeatherForecastUnsubscriber
클래스의 새 인스턴스를 반환합니다.RegisterWeatherInfo
메소드는 메인 모듈이 새로운 WeatherInfo
등록할 수 있도록 정의됩니다. 실제로 이는 내부 예약 API 호출 이나 SignalR Hub 에 대한 수신기 또는 정보 소스 역할을 하는 다른 것으로 대체될 수 있습니다.
using System; using System.Collections.Generic; namespace Observable { public class Unsubscriber<T> : IDisposable { private readonly List<IObserver<T>> m_Observers; private readonly IObserver<T> m_Observer; private bool m_IsDisposed; public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer) { m_Observers = observers; m_Observer = observer; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Observers.Contains(m_Observer)) { m_Observers.Remove(m_Observer); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
여기서 우리가 알 수 있는 것은:
IDisposable
구현합니다.
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecastUnsubscriber : Unsubscriber<WeatherInfo> { public WeatherForecastUnsubscriber( List<IObserver<WeatherInfo>> observers, IObserver<WeatherInfo> observer) : base(observers, observer) { } } }
여기서 우리가 알 수 있는 것은:
Unsubscriber<T>
클래스에서 상속됩니다.
using System; namespace Observable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
여기서 우리가 알 수 있는 것은:
WeatherForecastObserver
클래스는 IObserver<WeatherInfo>
구현하고 있습니다.OnNext
메서드에서는 콘솔에 온도를 씁니다.OnCompleted
메서드에서는 콘솔에 "Completed"를 씁니다.OnError
메서드에서는 콘솔에 "Error"를 씁니다.void Subscribe(WeatherForecast provider)
메소드를 정의했습니다. 반환된 구독 취소 개체는 구독 취소 시 사용할 수 있도록 내부에 저장됩니다.void Unsubscribe()
메서드를 정의하고 내부에 저장된 구독 취소 개체를 사용합니다.
using System; namespace Observable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
여기서 우리가 알 수 있는 것은:
Microsoft의 구현을 확인했을 때 몇 가지 우려 사항을 발견했습니다. 그래서 저는 약간의 변화를 주기로 결정했습니다.
using System; using System.Collections.Generic; namespace ExtendedObservable { public interface IExtendedObservable<out T> : IObservable<T> { IReadOnlyCollection<T> Snapshot { get; } IDisposable Subscribe(IObserver<T> observer, bool withHistory); } }
여기서 우리가 알 수 있는 것은:
IExtendedObservable<out T>
인터페이스는 IObservable<T>
인터페이스를 확장합니다.IReadOnlyCollection<T> Snapshot
속성을 정의했습니다.bool withHistory
매개 변수를 사용하여 IDisposable Subscribe(IObserver<T> observer, bool withHistory)
메서드를 정의했습니다.
using System; namespace ExtendedObservable { public class Unsubscriber : IDisposable { private readonly Action m_UnsubscribeAction; private bool m_IsDisposed; public Unsubscriber(Action unsubscribeAction) { m_UnsubscribeAction = unsubscribeAction; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing) { m_UnsubscribeAction(); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
여기서 우리가 알 수 있는 것은:
Unsubscriber
클래스는 일반적이지 않습니다.
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }
여기서 우리가 알 수 있는 것은:
Unsubscriber<T>
에서 <T>
부분을 제거했습니다.Action
취합니다.
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecast : IExtendedObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IReadOnlyCollection<WeatherInfo> Snapshot => m_WeatherInfoList; public IDisposable Subscribe(IObserver<WeatherInfo> observer) { return Subscribe(observer, false); } public IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); if (withHistory) { foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } } return new WeatherForecastUnsubscriber( () => { if (m_Observers.Contains(observer)) { m_Observers.Remove(observer); } }); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
여기서 우리가 알 수 있는 것은:
m_WeatherInfoList
목록을 IReadOnlyCollection
으로 반환하는 IReadOnlyCollection<WeatherInfo> Snapshot
속성을 제외하면 거의 동일합니다.withHistory
매개 변수를 사용하는 IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
메서드입니다.
using System; namespace ExtendedObservable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this, true); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
여기서 알 수 있는 것은 이제 기록으로 Subscribe
할지 여부를 결정하는 Subscribe(WeatherForecast provider)
제외하면 거의 동일하다는 것입니다.
using System; namespace ExtendedObservable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
이전과 동일합니다.
이제 .NET C#의 Observer 디자인 패턴 의 기본 사항을 알게 되었습니다. 그러나 이것이 이야기의 끝은 아닙니다.
IObservable<T>
및 IObserver<T>
인터페이스를 기반으로 구축되어 유용할 수 있는 더 멋진 기능을 제공하는 라이브러리가 있습니다.
이 라이브러리 중에는
그러므로 이 라이브러리를 탐색하고 시도해 보시기 바랍니다. 나는 당신이 그들 중 일부를 좋아할 것이라고 확신합니다.
여기에도 게시되었습니다 .