paint-brush
.NET C#의 관찰자 디자인 패턴에 대한 필수 가이드by@ahmedtarekhasan
3,180
3,180

.NET C#의 관찰자 디자인 패턴에 대한 필수 가이드

Ahmed Tarek Hasan19m2023/04/10
Read on Terminal Reader
Read this story w/o Javascript

이 문서에서는 몇 가지 향상된 기능을 갖춘 .NET C#의 관찰자 디자인 패턴에 대해 알아봅니다. 관찰자 디자인 패턴을 사용하면 구독자가 공급자에 등록하고 공급자로부터 알림을 받을 수 있습니다. 푸시 기반 알림이 필요한 모든 시나리오에 적합합니다. 패턴을 독특하게 만드는 것은 이를 사용하면 긴밀하게 결합된 관계 없이 이를 달성할 수 있다는 것입니다.
featured image - .NET C#의 관찰자 디자인 패턴에 대한 필수 가이드
Ahmed Tarek Hasan HackerNoon profile picture
0-item

이 문서에서는 몇 가지 향상된 기능을 갖춘 .NET C#의 관찰자 디자인 패턴에 대해 알아봅니다.


관찰자 디자인 패턴 정의

관찰자 디자인 패턴은 가장 중요하고 일반적으로 사용되는 디자인 패턴 중 하나입니다.


먼저 Observer Design Pattern 의 형식적 정의를 확인해 보겠습니다.


에 따라 마이크로소프트의 문서 :


관찰자 디자인 패턴을 사용하면 구독자가 공급자에 등록하고 공급자로부터 알림을 받을 수 있습니다. 푸시 기반 알림이 필요한 모든 시나리오에 적합합니다. 패턴은 공급자(주체 또는 관찰 가능 항목이라고도 함)와 0개, 1개 이상의 관찰자를 정의합니다. 관찰자는 공급자에 등록하고 미리 정의된 조건, 이벤트 또는 상태 변경이 발생할 때마다 공급자는 메서드 중 하나를 호출하여 모든 관찰자에게 자동으로 알립니다. 이 메서드 호출에서 공급자는 관찰자에게 현재 상태 정보를 제공할 수도 있습니다. .NET에서는 일반 System.IObservable<T>System.IObserver<T> 인터페이스를 구현하여 관찰자 디자인 패턴이 적용됩니다. 일반 유형 매개변수는 알림 정보를 제공하는 유형을 나타냅니다.


따라서 위의 정의를 통해 다음을 이해할 수 있습니다.

  1. 우리는 두 개의 당사자 또는 모듈을 가지고 있습니다.
  2. 제공할 정보 스트림이 있는 모듈입니다. 이 모듈은 Provider (정보 제공), Subject (정보를 외부 세계에 제공) 또는 Observable (외부 세계에서 관찰할 수 있음)이라고 합니다.
  3. 다른 곳에서 오는 정보 스트림에 관심이 있는 모듈입니다. 이 모듈은 Observer (정보를 관찰하므로)라고 합니다.

사진: Den Harrson, Unsplash

관찰자 디자인 패턴의 장점

우리가 지금 알고 있듯이 Observer 디자인 패턴은 ObservableObserver 모듈 간의 관계를 공식화합니다. 관찰자 디자인 패턴을 독특하게 만드는 것은 이를 사용하면 긴밀하게 결합된 관계 없이 이를 달성할 수 있다는 것입니다.


패턴이 작동하는 방식을 분석하면 다음을 찾을 수 있습니다.

  1. ObservableObserver 에 대해 필요한 최소한의 정보를 알고 있습니다.
  2. ObserverObservable 에 대해 필요한 최소한의 정보를 알고 있습니다.
  3. 상호 지식조차도 구체적인 구현이 아닌 추상화를 통해 달성됩니다.
  4. 결국 두 모듈 모두 해당 작업을 수행할 수 있으며 해당 작업만 수행할 수 있습니다.

사진: Lucas Santos, Unsplash

사용된 추상화

이는 .NET C# 에서 관찰자 디자인 패턴을 구현하는 데 사용되는 추상화 입니다.



IObservable<out T>

이것은 모든 Observable을 나타내는 Covariant 인터페이스입니다. .NET의 Variance에 대해 더 자세히 알고 싶다면 기사를 확인하세요. .NET C#의 공분산 및 반공분산 .


이 인터페이스에 정의된 멤버는 다음과 같습니다.


 public IDisposable Subscribe (IObserver<out T> observer);


Subscribe 메소드는 일부 Observer가 정보 스트림에 관심이 있음을 Observable에 알리기 위해 호출되어야 합니다.


Subscribe 메서드는 IDisposable 인터페이스를 구현하는 개체를 반환합니다. 이 객체는 Observable 이 제공하는 정보 스트림의 구독을 취소하기 위해 Observer 에 의해 사용될 수 있습니다. 이 작업이 완료되면 관찰자는 정보 스트림에 대한 업데이트에 대한 알림을 받지 않습니다.



IObserver<T>

이는 모든 Observer 를 나타내는 Contravariant 인터페이스입니다. .NET의 Variance에 대해 더 자세히 알고 싶다면 기사를 확인하세요. .NET C#의 공분산 및 반공분산 .


이 인터페이스에 정의된 멤버는 다음과 같습니다.


 public void OnCompleted (); public void OnError (Exception error); public void OnNext (T value);


OnCompleted 메서드는 Observable 에 의해 호출되어 Observer에게 정보 스트림이 완료되었으며 Observer가 더 이상 정보를 기대하지 않아야 함을 알려야 합니다.


OnError 메소드는 Observable 에 의해 호출되어 Observer 에게 오류가 발생했음을 알려야 합니다.


OnNext 메소드는 Observable 에 의해 호출되어 Observer 에게 새로운 정보가 준비되었으며 스트림에 추가되고 있음을 알려야 합니다.


사진: Tadas Sar, Unsplash

마이크로소프트의 구현

이제 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(); } } }


여기서 우리가 알 수 있는 것은:

  1. WeatherForecast 클래스는 IObservable<WeatherInfo> 구현하고 있습니다.
  2. Subscribe 메소드 구현에서는 전달된 Observer가 이전에 이미 등록되었는지 확인합니다. 그렇지 않은 경우 로컬 m_Observers 관찰자 목록에 추가합니다. 그런 다음 로컬 m_WeatherInfoList 목록에 있는 모든 WeatherInfo 항목을 하나씩 반복하고 Observer의 OnNext 메서드를 호출하여 Observer에 이에 대해 알립니다.
  3. 마지막으로 정보 스트림 구독을 취소하기 위해 관찰자가 사용할 WeatherForecastUnsubscriber 클래스의 새 인스턴스를 반환합니다.
  4. RegisterWeatherInfo 메소드는 메인 모듈이 새로운 WeatherInfo 등록할 수 있도록 정의됩니다. 실제로 이는 내부 예약 API 호출 이나 SignalR Hub 에 대한 수신기 또는 정보 소스 역할을 하는 다른 것으로 대체될 수 있습니다.



구독 취소<T>

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


여기서 우리가 알 수 있는 것은:

  1. 이는 모든 구독 취소자의 기본 클래스입니다.
  2. Disposable Design Pattern을 적용하여 IDisposable 구현합니다.
  3. 생성자를 통해 Observer의 전체 목록과 해당 Observer가 생성된 대상을 가져옵니다.
  4. 폐기하는 동안 Observer 전체 목록에 Observer가 이미 존재하는지 확인합니다. 그렇다면 목록에서 제거됩니다.



일기예보구독 취소자

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


여기서 우리가 알 수 있는 것은:

  1. 이는 Unsubscriber<T> 클래스에서 상속됩니다.
  2. 특별한 처리는 이루어지지 않습니다.



일기예보관찰자

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


여기서 우리가 알 수 있는 것은:

  1. WeatherForecastObserver 클래스는 IObserver<WeatherInfo> 구현하고 있습니다.
  2. OnNext 메서드에서는 콘솔에 온도를 씁니다.
  3. OnCompleted 메서드에서는 콘솔에 "Completed"를 씁니다.
  4. OnError 메서드에서는 콘솔에 "Error"를 씁니다.
  5. 우리는 메인 모듈이 등록 프로세스를 트리거할 수 있도록 void Subscribe(WeatherForecast provider) 메소드를 정의했습니다. 반환된 구독 취소 개체는 구독 취소 시 사용할 수 있도록 내부에 저장됩니다.
  6. 동일한 개념을 사용하여 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(); } } }


여기서 우리가 알 수 있는 것은:

  1. 공급자의 인스턴스를 만들었습니다.
  2. 그런 다음 3개의 정보를 등록했습니다.
  3. 지금까지는 관찰자가 정의되지 않았으므로 콘솔에 아무 것도 기록되어서는 안 됩니다.
  4. 그런 다음 관찰자의 인스턴스를 만들었습니다.
  5. 그런 다음 관찰자를 스트림에 구독했습니다.
  6. 이 순간 콘솔에서 기록된 온도 3개를 찾아야 합니다. 왜냐하면 관찰자가 구독할 때 이미 존재하는 정보에 대해 알림을 받게 되는데 우리의 경우에는 3가지 정보이기 때문입니다.
  7. 그런 다음 2가지 정보를 등록합니다.
  8. 따라서 콘솔에 2개의 메시지가 더 기록됩니다.
  9. 그런 다음 구독을 취소합니다.
  10. 그런 다음 1개의 정보를 등록합니다.
  11. 그러나 관찰자가 이미 구독을 취소했으므로 이 정보는 콘솔에 기록되지 않습니다.
  12. 그런 다음 관찰자는 다시 구독합니다.
  13. 그런 다음 1개의 정보를 등록합니다.
  14. 따라서 이 정보는 콘솔에 기록됩니다.


마지막으로 이를 실행하면 다음과 같은 결과가 나옵니다.


이미지 제공: Ahmed Tarek


Unsplash에 Bruno Yamazaky가 찍은 사진

내 확장 구현

Microsoft의 구현을 확인했을 때 몇 가지 우려 사항을 발견했습니다. 그래서 저는 약간의 변화를 주기로 결정했습니다.


IExtendedObservable<out T>

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


여기서 우리가 알 수 있는 것은:

  1. IExtendedObservable<out T> 인터페이스는 IObservable<T> 인터페이스를 확장합니다.
  2. 공변량 입니다. 이에 대해 더 알고 싶다면 기사를 확인하세요. .NET C#의 공분산 및 반공분산 .
  3. 우리는 다른 모듈이 구독하지 않고도 이미 존재하는 정보 항목의 즉시 목록을 얻을 수 있도록 IReadOnlyCollection<T> Snapshot 속성을 정의했습니다.
  4. 또한 관찰자가 구독하는 순간 이미 존재하는 정보 항목에 대한 알림을 받을지 여부를 결정할 수 있도록 추가 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); } } }


여기서 우리가 알 수 있는 것은:

  1. 이제 Unsubscriber 클래스는 일반적이지 않습니다.
  2. 정보 엔터티의 유형을 아는 데 더 이상 필요하지 않기 때문입니다.
  3. Observer의 전체 목록과 이를 생성한 Observer에 액세스하는 대신 Observable이 폐기될 때 Observable에 알리고 Observable은 등록 취소 프로세스를 자체적으로 처리합니다.
  4. 이런 식으로 이전보다 작업량이 줄어들고 본연의 역할만 수행하게 됩니다.



일기예보구독 취소자

 using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }


여기서 우리가 알 수 있는 것은:

  1. Unsubscriber<T> 에서 <T> 부분을 제거했습니다.
  2. 이제 생성자는 폐기 시 호출할 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(); } } }


여기서 우리가 알 수 있는 것은:

  1. 내부 m_WeatherInfoList 목록을 IReadOnlyCollection 으로 반환하는 IReadOnlyCollection<WeatherInfo> Snapshot 속성을 제외하면 거의 동일합니다.
  2. 그리고 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(); } } }


이전과 동일합니다.




마지막으로 이를 실행하면 이전과 동일한 결과가 나타나야 합니다.


이미지 제공: Ahmed Tarek


Unsplash의 Emily Morter 사진, Ahmed Tarek 조정

무엇 향후 계획

이제 .NET C#의 Observer 디자인 패턴 의 기본 사항을 알게 되었습니다. 그러나 이것이 이야기의 끝은 아닙니다.


IObservable<T>IObserver<T> 인터페이스를 기반으로 구축되어 유용할 수 있는 더 멋진 기능을 제공하는 라이브러리가 있습니다.


이 라이브러리 중에는 .NET(Rx)용 반응성 확장 도서관. 비동기 프로그래밍을 지원하기 위한 확장 메서드 집합과 LINQ 표준 시퀀스 연산자로 구성됩니다.


그러므로 이 라이브러리를 탐색하고 시도해 보시기 바랍니다. 나는 당신이 그들 중 일부를 좋아할 것이라고 확신합니다.


여기에도 게시되었습니다 .