Lab: Observer Pattern#
Objective#
In this lab, we’ll practice the Observer pattern by building a chat room application. The chat room will notify its registered clients every time a new message is posted.
Part 1: Custom Implementation#
Step 1: Creating our Observer: IClient#
Define the observer interface IClient
which should contain a ReceiveMessage
method. The chat room will call this method to notify clients of a new message.
interface IClient
{
void ReceiveMessage(string message);
}
Step 2: Creating our Observable: ChatRoom#
Design the ChatRoom
class, which will act as our Observable. This class should have:
A private list to keep track of registered clients (observers).
A method to post a new message.
Methods to add (
RegisterClient
) and remove (UnregisterClient
) clients.A method to notify all registered clients of a new message.
class ChatRoom
{
private List<IClient> clients = new List<IClient>();
public void RegisterClient(IClient client)
=> clients.Add(client);
public void UnregisterClient(IClient client)
=> clients.Remove(client);
public void PostMessage(string message)
{
NotifyClients(message);
}
private void NotifyClients(string message)
{
foreach (var client in clients)
client.ReceiveMessage(message);
}
}
Step 3: Implementing a Concrete Observer: Client#
Implement a concrete observer, Client
, which implements the IClient
interface. When it receives a message update, it should display the new message.
class Client : IClient
{
public string Name { get; set; }
public void ReceiveMessage(string message)
=> Console.WriteLine($"{Name} received: {message}");
}
Step 4: Testing the Custom Implementation#
Test the interaction:
Instantiate a
ChatRoom
.Register a couple of
Client
instances with the chat room.Post a new message using the
PostMessage
method and observe the notifications.
ChatRoom room = new ChatRoom();
Client client1 = new Client() { Name = "Alice" };
Client client2 = new Client() { Name = "Bob" };
room.RegisterClient(client1);
room.RegisterClient(client2);
room.PostMessage("Hello, world!");
Alice received: Hello, world!
Bob received: Hello, world!
Part 2: Refactoring with .NET Built-in Interfaces#
Of course! Let’s dive into the details for refactoring with .NET’s built-in interfaces for the Observer pattern.
Step 1: Refactor ChatRoom to use IObservable#
Update the
ChatRoom
class to implementIObservable<string>
.Instead of our custom list of clients (
IClient
), we will use a list ofIObserver<string>
.The
IObservable<T>
interface requires an implementation of theSubscribe
method which replaces ourRegisterClient
method. TheSubscribe
method will return anIDisposable
, which can be used to unsubscribe or unregister a client from the chat room.
class ChatRoom : IObservable<string>
{
private List<IObserver<string>> observers = new List<IObserver<string>>();
public IDisposable Subscribe(IObserver<string> observer)
{
if (!observers.Contains(observer))
observers.Add(observer);
return new Unsubscriber(observers, observer);
}
public void PostMessage(string message)
{
foreach (var observer in observers.ToArray())
if (observers.Contains(observer))
observer.OnNext(message);
}
private class Unsubscriber : IDisposable
{
private List<IObserver<string>> observers;
private IObserver<string> observer;
public Unsubscriber(List<IObserver<string>> observers, IObserver<string> observer)
{
this.observers = observers;
this.observer = observer;
}
public void Dispose()
{
if (observer != null && observers.Contains(observer))
observers.Remove(observer);
}
}
}
Step 2: Refactor IClient to use IObserver#
Update the
IClient
interface to extendIObserver<string>
. TheIObserver<T>
interface has three methods:OnNext
,OnError
, andOnCompleted
. For our chat room application, we will mainly focus on theOnNext
method which is invoked to provide the subscribed observer with new data (in our case, new chat messages).Implement the
IObserver<string>
interface in ourClient
class.
class Client : IObserver<string>
{
public string Name { get; set; }
public void OnNext(string message)
=> Console.WriteLine($"{Name} received: {message}");
public void OnError(Exception e)
=> Console.WriteLine($"{Name} experienced an error: {e.Message}");
public void OnCompleted()
=> Console.WriteLine($"{Name} has left the chat room.");
}
Step 3: Testing the Refactored Implementation#
Test the refactored chat room application.
ChatRoom room = new ChatRoom();
Client client1 = new Client() { Name = "Alice" };
Client client2 = new Client() { Name = "Bob" };
IDisposable aliceSubscription = room.Subscribe(client1);
IDisposable bobSubscription = room.Subscribe(client2);
room.PostMessage("Hello, world!");
// Simulate Alice leaving the chat room.
aliceSubscription.Dispose();
room.PostMessage("Is Alice still here?");
Alice received: Hello, world!
Bob received: Hello, world!
Bob received: Is Alice still here?
By following these steps, we’ve successfully refactored our custom chat room application to use the built-in IObservable<T>
and IObserver<T>
interfaces from .NET. This provides a standardized way of implementing the Observer pattern and may offer better interoperability with other .NET components and libraries.
Challenge#
How would you implement a feature for clients to post messages back to the chat room, which then gets broadcasted to all clients?
How could we include the name of the sender in the broadcasted message?
Add a new type of client, such as a
BotClient
, which automatically responds to certain keywords in the chat room. Implement this and test its integration.
🤔 Reflection
Reflect on the difference between implementing the Observer pattern using custom interfaces versus using the built-in .NET interfaces. What are the advantages and disadvantages of each approach?