Press "Enter" to skip to content

Month: December 2016

Publish/Subscribe Design Pattern in C#

Video version here:

 

Where to use it

In many programs, one object needs to know when something happens to another object.

One way to do this is to have the first object check the second object – usually by getting a value from one of the second object’s properties. However, there are two problems with this technique.

First, the first object needs to check the second object every time it does something that affects the second object. This leads to a lot of duplicated code – which is easy to forget to do every time.

Second, a third object might change the second object – and the first object would not know about the change.

A way to avoid these problems is to use the publish/subscribe pattern.

The first object can “subscribe” to an event on the second object. When the second object has a value change, it can “publish” an event to any objects that are subscribed.

This way, the first object will always know about the event – no matter what object, or function, causes the event to happen.

 

For this demonstration, we’ll use classes from a role-playing game. A GameSession object, which manages how the user input works with the other game objects (like a ViewModel, in MVVM). It needs to watch the Player object, to see if the player’s hit points are ever zero, or lower.

 

 

Non-Pattern Version

The GameSession class has two functions that subtract hit points from the player. The first, when a monster attacks the player. The second, if the player moves to a location with a poisonous atmosphere.

Without using the publish/subscribe design pattern, each of those functions needs to check if the player has zero hit points. If we add more functions that subtract hit points from the player, we would need to add this same check in each function.

Location.cs

namespace Engine.PublishSubscribePattern.NonPatternVersion.Models
{
    public class Location
    {
        public enum AtmosphereType
        {
            Normal,
            Poisonous
        }
        public AtmosphereType Atmosphere { get; set; }
    }
}

 

Player.cs

namespace Engine.PublishSubscribePattern.NonPatternVersion.Models
{
    public class Player
    {
        public int HitPoints { get; set; }
    }
}

 

GameSession.cs

using System.Collections.Generic;
using Engine.PublishSubscribePattern.NonPatternVersion.Models;
namespace Engine.PublishSubscribePattern.NonPatternVersion.ViewModels
{
    public class GameSession
    {
        public List<string> Messages { get; set; }
        public Player CurrentPlayer { get; set; }
        public Location CurrentLocation { get; set; }
        public GameSession()
        {
            Messages = new List<string>();
            CurrentPlayer = new Player {HitPoints = 10};
            CurrentLocation = new Location {Atmosphere = Location.AtmosphereType.Normal};
        }
        public void MonsterAttackPlayer(int amountOfDamage)
        {
            CurrentPlayer.HitPoints -= amountOfDamage;
            CheckIfPlayerWasKilled();
        }
        public void MoveToLocation(Location location)
        {
            CurrentLocation = location;
            if(CurrentLocation.Atmosphere == Location.AtmosphereType.Poisonous)
            {
                CurrentPlayer.HitPoints -= 1;
                CheckIfPlayerWasKilled();
            }
        }
        private void CheckIfPlayerWasKilled()
        {
            if(CurrentPlayer.HitPoints <= 0)
            {
                Messages.Add("You were killed");
            }
        }
    }
}

 

 

Pattern Version

With the publish/subscribe design pattern, we add an “event” to the Player class. Other objects can “subscribe” to this event.

If the player’s hit points ever reach zero, the player object “publishes” a message to any objects subscribed to the PlayerKilled EventHandler.

The GameSession object only needs to subscribe to this EventHandler once. If the player ever reaches zero hit points, from any part of the program, the player object will publish the message.

If we add new functions, where the player will receive damage, we don’t need to have those functions check for zero hit points. The GameSession object will automatically be notified from the Player object.

This makes it much easier to add new game features and capabilities.

Location.cs

namespace Engine.PublishSubscribePattern.PatternVersion.Models
{
    public class Location
    {
        public enum AtmosphereType
        {
            Normal,
            Poisonous
        }
        public AtmosphereType Atmosphere { get; set; }
    }
}

 

Player.cs

using System;
namespace Engine.PublishSubscribePattern.PatternVersion.Models
{
    public class Player
    {
        private int _hitPoints;
        public EventHandler PlayerKilled;
        public int HitPoints
        {
            get { return _hitPoints; }
            set
            {
                _hitPoints = value;
                if(_hitPoints <= 0)
                {
                    // When the player's HitPoint property is zero, or lower,
                    // the player object will raise a PlayerKilled notification to all subscribed objects.
                    OnPlayerKilled();
                }
            }
        }
        private void OnPlayerKilled()
        {
            // If there are no subscribed objects,
            // the PlayerKilled EventHandler will be null, 
            // and nothing will be notified of this event.
            PlayerKilled?.Invoke(this, EventArgs.Empty);
        }
    }
}

 

GameSession.cs

using System;
using System.Collections.Generic;
using Engine.PublishSubscribePattern.PatternVersion.Models;
namespace Engine.PublishSubscribePattern.PatternVersion.ViewModels
{
    public class GameSession
    {
        public List<string> Messages { get; set; }
        public Player CurrentPlayer { get; set; }
        public Location CurrentLocation { get; set; }
        public GameSession()
        {
            Messages = new List<string>();
            CurrentPlayer = new Player {HitPoints = 10};
            CurrentLocation = new Location {Atmosphere = Location.AtmosphereType.Normal};
            // "Subscribe" to the PlayerKilled event.
            //
            // When the GameSession object receives this notification,
            // it will run the HandlePlayerKilled function.
            CurrentPlayer.PlayerKilled += HandlePlayerKilled;
        }
        public void MonsterAttackPlayer(int amountOfDamage)
        {
            CurrentPlayer.HitPoints -= amountOfDamage;
        }
        public void MoveToLocation(Location location)
        {
            CurrentLocation = location;
            if(CurrentLocation.Atmosphere == Location.AtmosphereType.Poisonous)
            {
                CurrentPlayer.HitPoints -= 1;
            }
        }
        private void HandlePlayerKilled(object sender, EventArgs eventArgs)
        {
            Messages.Add("You were killed");
        }
    }
}

 

Pattern Version (with “event”)

When you define the PlayerKilled EventHandler, you can also declare it as an event. This is a more common way to do it.

In the Player.cs class, when you change this line:

public EventHandler PlayerKilled;

to this:

public event EventHandler PlayerKilled;

IntelliSense will recognize that the PlayerKilled EventHandler is an event. So, it will show a lightning bolt when you hover over it, like this:

 

 

 

instead of showing it only as a field, like this:

 

 

 

Pattern Version (with custom EventArgs)

You might want to include additional information with your events. To do that, you can create a custom EventArgs class.

Your custom EventArgs class must inherit from the base EventArgs class. Then, you can add whatever properties you want, to hold the additional information.

In this example, the PlayerKilledEventArgs class has a constructor that takes the number of deaths and populates a property with that value.

In the Player class (the publisher), we change the EventHandler to include the type of EventArgs object it will pass – our new PlayerKilledEventArgs object. Then, in the OnPlayerKilled function, we create a new instance of the PlayerKilledEventArgs object, passing in the number of deaths parameter.

In the GameSession class (the subscriber), we change the HandlePlayerKilled function to accept the PlayerKilledEventArgs object. Then, we can add a new line that displays the number of deaths in the list of messages.

 

Location.cs

namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Models
{
    public class Location
    {
        public enum AtmosphereType
        {
            Normal,
            Poisonous
        }
        public AtmosphereType Atmosphere { get; set; }
    }
}

 

PlayerKilledEventArgs.cs

using System;
namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Common
{
    public class PlayerKilledEventArgs : EventArgs
    {
        public int NumberOfDeaths { get; private set; }
        public PlayerKilledEventArgs(int numberOfDeaths)
        {
            NumberOfDeaths = numberOfDeaths;
        }
    }
}

 

Player.cs

using System;
using Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Common;
namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Models
{
    public class Player
    {
        private int _hitPoints;
        private int _numberOfDeaths;
        public EventHandler<PlayerKilledEventArgs> PlayerKilled;
        public int HitPoints
        {
            get { return _hitPoints; }
            set
            {
                _hitPoints = value;
                if(_hitPoints <= 0)
                {
                    // Increase the "number of deaths" counter.
                    _numberOfDeaths++;
                    // When the player's HitPoint property is zero, or lower,
                    // the player object will raise a PlayerKilled notification to all subscribed objects.
                    OnPlayerKilled();
                }
            }
        }
        protected virtual void OnPlayerKilled()
        {
            PlayerKilled?.Invoke(this, new PlayerKilledEventArgs(_numberOfDeaths));
        }
    }
}

 

GameSession.cs

using System.Collections.Generic;
using Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Common;
using Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Models;
namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.ViewModels
{
    public class GameSession
    {
        public List<string> Messages { get; set; }
        public Player CurrentPlayer { get; set; }
        public Location CurrentLocation { get; set; }
        public GameSession()
        {
            Messages = new List<string>();
            CurrentPlayer = new Player {HitPoints = 10};
            CurrentLocation = new Location {Atmosphere = Location.AtmosphereType.Normal};
            // "Subscribe" to the PlayerKilled event.
            //
            // When the GameSession object receives this notification,
            // it will run the HandlePlayerKilled function.
            CurrentPlayer.PlayerKilled += HandlePlayerKilled;
        }
        public void MonsterAttackPlayer(int amountOfDamage)
        {
            CurrentPlayer.HitPoints -= amountOfDamage;
        }
        public void MoveToLocation(Location location)
        {
            CurrentLocation = location;
            if(CurrentLocation.Atmosphere == Location.AtmosphereType.Poisonous)
            {
                CurrentPlayer.HitPoints -= 1;
            }
        }
        private void HandlePlayerKilled(object sender, PlayerKilledEventArgs eventArgs)
        {
            Messages.Add("You were killed");
            Messages.Add($"This was death number: {eventArgs.NumberOfDeaths}");
        }
    }
}

 

 What to watch out for

You should be aware of possible problems with multi-threaded programs.

If you are using C# 6.0, the code in OnPlayerKilled (in the Pattern Version – with custom EventArgs) is thread-safe. So, if an object subscribes to the event from another thread, and later un-subscribes from it, the program will not throw an exception when the OnPlayerKilled function tries to call Invoke on the PlayerKilled EventHandler.

If you want more details, please see this post on Jon Skeet’s site. That post also shows how to raise events in a thread-safe way, in previous versions of C#.

 

 

All my design pattern lessons

Source code for my design pattern lessons

 

8 Comments