관찰자 디자인 패턴을 사용하면 구독자가 공급자에 등록하고 공급자로부터 알림을 받을 수 있습니다. 푸시 기반 알림이 필요한 모든 시나리오에 적합합니다. 이 패턴은 공급자 ( 주체 또는 관찰 가능 개체라고도 함) 및 0개, 하나 이상의 관찰자를 정의합니다. 관찰자는 공급자에 등록하고 미리 정의된 조건, 이벤트 또는 상태 변경이 발생할 때마다 공급자는 대리자를 호출하여 모든 관찰자에게 자동으로 알깁니다. 이 메서드 호출에서 공급자는 관찰자에게 현재 상태 정보를 제공할 수도 있습니다. .NET에서 관찰자 디자인 패턴은 제네릭 System.IObservable<T> 및 System.IObserver<T> 인터페이스를 구현하여 적용됩니다. 제네릭 형식 매개 변수는 알림 정보를 제공하는 형식을 나타냅니다.
패턴을 적용하는 경우
관찰자 디자인 패턴은 데이터 원본(비즈니스 논리) 계층과 사용자 인터페이스(표시) 계층과 같은 서로 다른 두 구성 요소 또는 애플리케이션 계층 간의 깨끗한 분리를 지원하기 때문에 분산 푸시 기반 알림에 적합합니다. 공급자가 콜백을 사용하여 클라이언트에 현재 정보를 제공할 때마다 패턴을 구현할 수 있습니다.
패턴을 구현하려면 다음 세부 정보를 제공해야 합니다.
관찰자에게 알림을 보내는 개체인 공급자 또는 주체입니다. 공급자는 인터페이스를 구현하는 클래스 또는 구조체입니다 IObservable<T> . 공급자는 공급자로부터 알림을 받으려는 관찰자가 호출하는 단일 메서드 IObservable<T>.Subscribe를 구현해야 합니다.
공급자로부터 알림을 받는 개체인 관찰자입니다. 관찰자는 인터페이스를 구현하는 클래스 또는 구조체입니다 IObserver<T> . 관찰자는 세 가지 메서드를 구현해야 하며, 이 메서드는 모두 공급자가 호출합니다.
- IObserver<T>.OnNext새 정보 또는 현재 정보를 관찰자에게 제공하는 입니다.
- IObserver<T>.OnError- 관찰자에게 오류가 발생했음을 알릴 수 있습니다.
- IObserver<T>.OnCompleted- 공급자가 알림 보내기를 완료했음을 나타냅니다.
공급자가 관찰자를 추적할 수 있도록 하는 메커니즘입니다. 일반적으로 공급자는 알림을 구독한 System.Collections.Generic.List<T> 구현에 대한 참조를 저장하기 위해 IObserver<T> 객체와 같은 컨테이너 객체를 사용합니다. 이 목적을 위해 스토리지 컨테이너를 사용하면 공급자가 0부터 무제한 관찰자까지 처리할 수 있습니다. 관찰자가 알림을 받는 순서는 정의되지 않았습니다. 공급자는 모든 메서드를 사용하여 주문을 결정할 수 있습니다.
IDisposable 알림이 완료되면 공급자가 관찰자를 제거할 수 있도록 하는 구현입니다. 관찰자는 IDisposable 메서드를 통해 Subscribe 구현에 대한 참조를 받으므로, 공급자가 알림을 보내는 것을 완료하기 전에 IDisposable.Dispose 메서드를 호출하여 구독을 취소할 수도 있습니다.
공급자가 관찰자에게 보내는 데이터를 포함하는 개체입니다. 이 개체의 형식은 IObservable<T> 및 IObserver<T> 인터페이스의 제네릭 형식 매개 변수에 해당합니다. 이 개체는 IObservable<T> 구현과 같을 수도 있지만, 일반적으로는 별도의 타입입니다.
비고
관찰자 디자인 패턴을 구현하는 것 외에도, IObservable<T> 및 IObserver<T> 인터페이스를 사용하여 구축된 라이브러리를 탐색하는 데 관심이 있을 수 있습니다. 예를 들어 Rx(.NET용 Reactive Extensions) 는 비동기 프로그래밍을 지원하는 일련의 확장 메서드와 LINQ 표준 시퀀스 연산자로 구성됩니다.
패턴 구현
다음 예제에서는 관찰자 디자인 패턴을 사용하여 공항 수하물 찾는 정보 시스템을 구현합니다. 클래스는 BaggageInfo
도착 항공편 및 각각의 항공편에서 수하물을 픽업할 수 있는 수하물 회전대에 대한 정보를 제공합니다. 다음 예제에 나와 있습니다.
namespace Observables.Example;
public readonly record struct BaggageInfo(
int FlightNumber,
string From,
int Carousel);
Public Class BaggageInfo
Private flightNo As Integer
Private origin As String
Private location As Integer
Friend Sub New(ByVal flight As Integer, ByVal from As String, ByVal carousel As Integer)
Me.flightNo = flight
Me.origin = from
Me.location = carousel
End Sub
Public ReadOnly Property FlightNumber As Integer
Get
Return Me.flightNo
End Get
End Property
Public ReadOnly Property From As String
Get
Return Me.origin
End Get
End Property
Public ReadOnly Property Carousel As Integer
Get
Return Me.location
End Get
End Property
End Class
BaggageHandler
클래스는 도착 항공편 및 수하물 회수대에 대한 정보를 수신할 책임이 있습니다. 내부적으로 다음 두 컬렉션을 유지 관리합니다.
-
_observers
: 업데이트된 정보를 관찰하는 클라이언트의 컬렉션입니다. -
_flights
: 항공편과 할당된 수하물 컨베이어 벨트의 모음입니다.
클래스의 BaggageHandler
소스 코드는 다음 예제에 나와 있습니다.
namespace Observables.Example;
public sealed class BaggageHandler : IObservable<BaggageInfo>
{
private readonly HashSet<IObserver<BaggageInfo>> _observers = new();
private readonly HashSet<BaggageInfo> _flights = new();
public IDisposable Subscribe(IObserver<BaggageInfo> observer)
{
// Check whether observer is already registered. If not, add it.
if (_observers.Add(observer))
{
// Provide observer with existing data.
foreach (BaggageInfo item in _flights)
{
observer.OnNext(item);
}
}
return new Unsubscriber<BaggageInfo>(_observers, observer);
}
// Called to indicate all baggage is now unloaded.
public void BaggageStatus(int flightNumber) =>
BaggageStatus(flightNumber, string.Empty, 0);
public void BaggageStatus(int flightNumber, string from, int carousel)
{
var info = new BaggageInfo(flightNumber, from, carousel);
// Carousel is assigned, so add new info object to list.
if (carousel > 0 && _flights.Add(info))
{
foreach (IObserver<BaggageInfo> observer in _observers)
{
observer.OnNext(info);
}
}
else if (carousel is 0)
{
// Baggage claim for flight is done.
if (_flights.RemoveWhere(
flight => flight.FlightNumber == info.FlightNumber) > 0)
{
foreach (IObserver<BaggageInfo> observer in _observers)
{
observer.OnNext(info);
}
}
}
}
public void LastBaggageClaimed()
{
foreach (IObserver<BaggageInfo> observer in _observers)
{
observer.OnCompleted();
}
_observers.Clear();
}
}
Public Class BaggageHandler : Implements IObservable(Of BaggageInfo)
Private observers As List(Of IObserver(Of BaggageInfo))
Private flights As List(Of BaggageInfo)
Public Sub New()
observers = New List(Of IObserver(Of BaggageInfo))
flights = New List(Of BaggageInfo)
End Sub
Public Function Subscribe(ByVal observer As IObserver(Of BaggageInfo)) As IDisposable _
Implements IObservable(Of BaggageInfo).Subscribe
' Check whether observer is already registered. If not, add it
If Not observers.Contains(observer) Then
observers.Add(observer)
' Provide observer with existing data.
For Each item In flights
observer.OnNext(item)
Next
End If
Return New Unsubscriber(Of BaggageInfo)(observers, observer)
End Function
' Called to indicate all baggage is now unloaded.
Public Sub BaggageStatus(ByVal flightNo As Integer)
BaggageStatus(flightNo, String.Empty, 0)
End Sub
Public Sub BaggageStatus(ByVal flightNo As Integer, ByVal from As String, ByVal carousel As Integer)
Dim info As New BaggageInfo(flightNo, from, carousel)
' Carousel is assigned, so add new info object to list.
If carousel > 0 And Not flights.Contains(info) Then
flights.Add(info)
For Each observer In observers
observer.OnNext(info)
Next
ElseIf carousel = 0 Then
' Baggage claim for flight is done
Dim flightsToRemove As New List(Of BaggageInfo)
For Each flight In flights
If info.FlightNumber = flight.FlightNumber Then
flightsToRemove.Add(flight)
For Each observer In observers
observer.OnNext(info)
Next
End If
Next
For Each flightToRemove In flightsToRemove
flights.Remove(flightToRemove)
Next
flightsToRemove.Clear()
End If
End Sub
Public Sub LastBaggageClaimed()
For Each observer In observers
observer.OnCompleted()
Next
observers.Clear()
End Sub
End Class
업데이트된 정보를 수신하려는 클라이언트는 메서드를 호출합니다 BaggageHandler.Subscribe
. 클라이언트가 이전에 알림을 구독하지 않은 경우 클라이언트의 IObserver<T> 구현에 대한 참조가 컬렉션에 _observers
추가됩니다.
오버로드된 메서드를 BaggageHandler.BaggageStatus
호출하여 항공편의 수하물이 언로드 중이거나 더 이상 언로드되지 않음을 나타낼 수 있습니다. 첫 번째 경우, 메서드는 항공편 번호와 항공편이 출발한 공항, 수하물이 내려지는 회전목마를 전달합니다. 두 번째 경우 메서드는 플라이트 번호만 전달됩니다. 언로드되는 수하물을 위해, 메서드는 메서드에 전달된 BaggageInfo
정보가 _flights
컬렉션에 존재하는지 확인합니다. 그렇지 않은 경우 메서드는 정보를 추가하고 각 관찰자의 OnNext
메서드를 호출합니다. 수하물이 더 이상 언로드되지 않는 항공편의 경우, 이 메서드는 해당 항공편의 정보가 컬렉션에 저장 _flights
되어 있는지 여부를 확인합니다. 이 경우 메서드는 각 관찰자의 OnNext
메서드를 호출한 후 BaggageInfo
객체를 _flights
컬렉션에서 제거합니다.
하루의 마지막 비행이 착륙하고 수하물이 처리되면 메서드가 BaggageHandler.LastBaggageClaimed
호출됩니다. 이 메서드는 각 관찰자의 OnCompleted
메서드를 호출하여 모든 알림이 완료되었음을 나타내고 컬렉션을 지웁니다 _observers
.
공급자의 Subscribe 메서드는 관찰자가 IDisposable 메서드가 호출되기 전에 알림 수신을 중지할 수 있도록 하는 OnCompleted 구현을 반환합니다. 이 Unsubscriber(Of BaggageInfo)
클래스의 소스 코드는 다음 예제에 나와 있습니다. 메서드에서 BaggageHandler.Subscribe
클래스가 인스턴스화되면 컬렉션에 대한 참조와 컬렉션에 _observers
추가된 관찰자에 대한 참조가 전달됩니다. 이러한 참조는 지역 변수에 할당됩니다. 개체의 Dispose
메서드가 호출되면 관찰자가 컬렉션에 _observers
여전히 존재하는지 확인하고, 이 경우 관찰자를 제거합니다.
namespace Observables.Example;
internal sealed class Unsubscriber<BaggageInfo> : IDisposable
{
private readonly ISet<IObserver<BaggageInfo>> _observers;
private readonly IObserver<BaggageInfo> _observer;
internal Unsubscriber(
ISet<IObserver<BaggageInfo>> observers,
IObserver<BaggageInfo> observer) => (_observers, _observer) = (observers, observer);
public void Dispose() => _observers.Remove(_observer);
}
Friend Class Unsubscriber(Of BaggageInfo) : Implements IDisposable
Private _observers As List(Of IObserver(Of BaggageInfo))
Private _observer As IObserver(Of BaggageInfo)
Friend Sub New(ByVal observers As List(Of IObserver(Of BaggageInfo)), ByVal observer As IObserver(Of BaggageInfo))
Me._observers = observers
Me._observer = observer
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
If _observers.Contains(_observer) Then
_observers.Remove(_observer)
End If
End Sub
End Class
다음 예제에서는 수하물 클레임 정보를 표시하는 기본 클래스인 IObserver<T> 구현인 ArrivalsMonitor
을 제공합니다. 정보는 원래 도시의 이름으로 사전순으로 표시됩니다. 메서드 ArrivalsMonitor
는 Visual Basic에서는 overridable
으로, C#에서는 virtual
로 표시되므로 파생 클래스에서 재정의할 수 있습니다.
namespace Observables.Example;
public class ArrivalsMonitor : IObserver<BaggageInfo>
{
private readonly string _name;
private readonly List<string> _flights = new();
private readonly string _format = "{0,-20} {1,5} {2, 3}";
private IDisposable? _cancellation;
public ArrivalsMonitor(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
_name = name;
}
public virtual void Subscribe(BaggageHandler provider) =>
_cancellation = provider.Subscribe(this);
public virtual void Unsubscribe()
{
_cancellation?.Dispose();
_flights.Clear();
}
public virtual void OnCompleted() => _flights.Clear();
// No implementation needed: Method is not called by the BaggageHandler class.
public virtual void OnError(Exception e)
{
// No implementation.
}
// Update information.
public virtual void OnNext(BaggageInfo info)
{
bool updated = false;
// Flight has unloaded its baggage; remove from the monitor.
if (info.Carousel is 0)
{
string flightNumber = string.Format("{0,5}", info.FlightNumber);
for (int index = _flights.Count - 1; index >= 0; index--)
{
string flightInfo = _flights[index];
if (flightInfo.Substring(21, 5).Equals(flightNumber))
{
updated = true;
_flights.RemoveAt(index);
}
}
}
else
{
// Add flight if it doesn't exist in the collection.
string flightInfo = string.Format(_format, info.From, info.FlightNumber, info.Carousel);
if (_flights.Contains(flightInfo) is false)
{
_flights.Add(flightInfo);
updated = true;
}
}
if (updated)
{
_flights.Sort();
Console.WriteLine($"Arrivals information from {_name}");
foreach (string flightInfo in _flights)
{
Console.WriteLine(flightInfo);
}
Console.WriteLine();
}
}
}
Public Class ArrivalsMonitor : Implements IObserver(Of BaggageInfo)
Private name As String
Private flightInfos As New List(Of String)
Private cancellation As IDisposable
Private fmt As String = "{0,-20} {1,5} {2, 3}"
Public Sub New(ByVal name As String)
If String.IsNullOrEmpty(name) Then Throw New ArgumentNullException("The observer must be assigned a name.")
Me.name = name
End Sub
Public Overridable Sub Subscribe(ByVal provider As BaggageHandler)
cancellation = provider.Subscribe(Me)
End Sub
Public Overridable Sub Unsubscribe()
cancellation.Dispose()
flightInfos.Clear()
End Sub
Public Overridable Sub OnCompleted() Implements System.IObserver(Of BaggageInfo).OnCompleted
flightInfos.Clear()
End Sub
' No implementation needed: Method is not called by the BaggageHandler class.
Public Overridable Sub OnError(ByVal e As System.Exception) Implements System.IObserver(Of BaggageInfo).OnError
' No implementation.
End Sub
' Update information.
Public Overridable Sub OnNext(ByVal info As BaggageInfo) Implements System.IObserver(Of BaggageInfo).OnNext
Dim updated As Boolean = False
' Flight has unloaded its baggage; remove from the monitor.
If info.Carousel = 0 Then
Dim flightsToRemove As New List(Of String)
Dim flightNo As String = String.Format("{0,5}", info.FlightNumber)
For Each flightInfo In flightInfos
If flightInfo.Substring(21, 5).Equals(flightNo) Then
flightsToRemove.Add(flightInfo)
updated = True
End If
Next
For Each flightToRemove In flightsToRemove
flightInfos.Remove(flightToRemove)
Next
flightsToRemove.Clear()
Else
' Add flight if it does not exist in the collection.
Dim flightInfo As String = String.Format(fmt, info.From, info.FlightNumber, info.Carousel)
If Not flightInfos.Contains(flightInfo) Then
flightInfos.Add(flightInfo)
updated = True
End If
End If
If updated Then
flightInfos.Sort()
Console.WriteLine("Arrivals information from {0}", Me.name)
For Each flightInfo In flightInfos
Console.WriteLine(flightInfo)
Next
Console.WriteLine()
End If
End Sub
End Class
클래스에는 ArrivalsMonitor
및 Subscribe
메서드가 포함됩니다.
Subscribe
메서드를 사용하면, 클래스는 IDisposable 호출에서 반환된 구현을 프라이빗 변수에 저장할 Subscribe 수 있습니다. 이 Unsubscribe
메서드를 사용하면 공급자의 Dispose 구현을 호출하여 클래스가 알림에서 구독을 취소할 수 있습니다.
ArrivalsMonitor
는 OnNext, OnError, 및 OnCompleted 메서드의 구현도 제공합니다. 구현에 OnNext 만 상당한 양의 코드가 포함됩니다. 이 방법은 도착 항공편의 출발지 공항과 수하물을 받을 수 있는 회전식 컨베이어에 대한 정보를 유지하는 개인 정렬된 일반 List<T> 객체와 함께 작동합니다. 클래스가 BaggageHandler
새 플라이트 도착을 보고하는 경우 메서드 구현은 OnNext 해당 플라이트 정보를 목록에 추가합니다. 클래스가 BaggageHandler
항공편의 수하물이 언로드되었다고 OnNext 보고하는 경우, 이 메서드는 해당 항공편을 목록에서 제거합니다. 변경될 때마다 목록이 정렬되어 콘솔에 표시됩니다.
다음 예제에서는 BaggageHandler
클래스와 ArrivalsMonitor
클래스의 두 인스턴스를 인스턴스화하는 애플리케이션 진입점을 포함하고, BaggageHandler.BaggageStatus
메서드를 사용하여 도착 항공편에 대한 정보를 추가하고 제거합니다. 각 경우에 관찰자는 업데이트를 받고 수하물 클레임 정보를 올바르게 표시합니다.
using Observables.Example;
BaggageHandler provider = new();
ArrivalsMonitor observer1 = new("BaggageClaimMonitor1");
ArrivalsMonitor observer2 = new("SecurityExit");
provider.BaggageStatus(712, "Detroit", 3);
observer1.Subscribe(provider);
provider.BaggageStatus(712, "Kalamazoo", 3);
provider.BaggageStatus(400, "New York-Kennedy", 1);
provider.BaggageStatus(712, "Detroit", 3);
observer2.Subscribe(provider);
provider.BaggageStatus(511, "San Francisco", 2);
provider.BaggageStatus(712);
observer2.Unsubscribe();
provider.BaggageStatus(400);
provider.LastBaggageClaimed();
// Sample output:
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
//
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
// Kalamazoo 712 3
//
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
//
// Arrivals information from SecurityExit
// Detroit 712 3
//
// Arrivals information from SecurityExit
// Detroit 712 3
// Kalamazoo 712 3
//
// Arrivals information from SecurityExit
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
//
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from SecurityExit
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from BaggageClaimMonitor1
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from SecurityExit
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from BaggageClaimMonitor1
// San Francisco 511 2
Module Example
Public Sub Main()
Dim provider As New BaggageHandler()
Dim observer1 As New ArrivalsMonitor("BaggageClaimMonitor1")
Dim observer2 As New ArrivalsMonitor("SecurityExit")
provider.BaggageStatus(712, "Detroit", 3)
observer1.Subscribe(provider)
provider.BaggageStatus(712, "Kalamazoo", 3)
provider.BaggageStatus(400, "New York-Kennedy", 1)
provider.BaggageStatus(712, "Detroit", 3)
observer2.Subscribe(provider)
provider.BaggageStatus(511, "San Francisco", 2)
provider.BaggageStatus(712)
observer2.Unsubscribe()
provider.BaggageStatus(400)
provider.LastBaggageClaimed()
End Sub
End Module
' The example displays the following output:
' Arrivals information from BaggageClaimMonitor1
' Detroit 712 3
'
' Arrivals information from BaggageClaimMonitor1
' Detroit 712 3
' Kalamazoo 712 3
'
' Arrivals information from BaggageClaimMonitor1
' Detroit 712 3
' Kalamazoo 712 3
' New York-Kennedy 400 1
'
' Arrivals information from SecurityExit
' Detroit 712 3
'
' Arrivals information from SecurityExit
' Detroit 712 3
' Kalamazoo 712 3
'
' Arrivals information from SecurityExit
' Detroit 712 3
' Kalamazoo 712 3
' New York-Kennedy 400 1
'
' Arrivals information from BaggageClaimMonitor1
' Detroit 712 3
' Kalamazoo 712 3
' New York-Kennedy 400 1
' San Francisco 511 2
'
' Arrivals information from SecurityExit
' Detroit 712 3
' Kalamazoo 712 3
' New York-Kennedy 400 1
' San Francisco 511 2
'
' Arrivals information from BaggageClaimMonitor1
' New York-Kennedy 400 1
' San Francisco 511 2
'
' Arrivals information from SecurityExit
' New York-Kennedy 400 1
' San Francisco 511 2
'
' Arrivals information from BaggageClaimMonitor1
' San Francisco 511 2
관련 문서
제목 | 설명 |
---|---|
관찰자 디자인 패턴 모범 사례 | 관찰자 디자인 패턴을 구현하는 애플리케이션을 개발할 때 채택할 모범 사례를 설명합니다. |
방법: 공급자 구현하기 | 온도 모니터링 애플리케이션에 대한 공급자의 단계별 구현을 제공합니다. |
방법: 관찰자 구현 | 온도 모니터링 애플리케이션에 대한 관찰자의 단계별 구현을 제공합니다. |
.NET