.NET 4 Baby Steps: Part IV - Observer

_13042010196_Note: This post is part of a series and you can find the rest of the parts in the series index.

The pub/sub pattern is a simple concept: you have a provider that pushes data to subscribers, and .NET 4 introduces two new interfaces, IObservable<T> and IObserver<T>, which make implementing this pattern very easy.

To see the usage of these interfaces, I’d like to use an example of a device with a GPS sensor that checks its position every second. If the GPS position leaves a specified area, we want to notify the cops.

GPSPosition

First, we need a small class to represent our location data—a simple data class with these key points:

class GPSPosition : IComparable<GPSPosition>
{
    public int Lat { get; set; }
    public int Long { get; set; }

    public GPSPosition() { }

    public GPSPosition(int lat, int @long)
    {
        this.Lat = lat;
        this.Long = @long;
    }

    public override string ToString()
    {
        return string.Format("Latitude = {0:N4}, Longitude = {1:N4}", Lat, Long);
    }

    public int CompareTo(GPSPosition other)
    {
        if (this.Lat > other.Lat)
        {
            return 1;
        }

        if (this.Lat < other.Lat)
        {
            return -1;
        }

        // Latitude is the same

        if (this.Long > other.Long)
        {
            return 1;
        }

        if (this.Long < other.Long)
        {
            return -1;
        }

        // Longitude is the same
        return 0;
    }
}

GPSSensor

Next, we create a fake sensor that reports our position. Key points:

class GPSSensor : IObservable<GPSPosition>
{
    private readonly List<IObserver<GPSPosition>> observers = new List<IObserver<GPSPosition>>();
    private readonly Random random = new Random();
    public GPSPosition Position { get; set; }
    private GPSPosition boundary;

    public GPSSensor()
    {
        this.Position = new GPSPosition();
        this.Position.Lat = random.Next(0, 181);
        this.Position.Long = random.Next(0, 361); // Fixed: longitude range (0-360)

        boundary = new GPSPosition(this.Position.Lat + 10, this.Position.Long + 10);
    }

    public void GetPosition()
    {
        GPSPosition current = new GPSPosition(this.Position.Lat, this.Position.Long);

        Position.Lat = (Position.Lat + random.Next(0, 5)) % 181; // Fixed: wrap around
        Position.Long = (Position.Long + random.Next(0, 5)) % 361; // Fixed: wrap around

        foreach (IObserver<GPSPosition> observer in observers)
        {
            observer.OnNext(this.Position);
            if (this.Position.CompareTo(boundary) > 0) // Fixed: compare current position
            {
                observer.OnCompleted();
            }
        }
    }

    public IDisposable Subscribe(IObserver<GPSPosition> observer)
    {
        observers.Add(observer);
        observer.OnNext(this.Position);
        return observer as IDisposable;
    }
}

Map

The Map class is a subscriber that handles output and alerts when the boundary is crossed. Key points:

class Map : IObserver<GPSPosition>
{
    private GPSPosition lastKnown;

    public bool StillTracking { get; private set; }

    public void OnCompleted()
    {
        Console.WriteLine("The device has moved beyond the bounds of our checking. Notify the cops: last seen at: {0}", lastKnown);
        StillTracking = false;
    }

    public void OnError(Exception error)
    {
        Console.WriteLine("SkyNet has taken over and shut down the GPS");
    }

    public void OnNext(GPSPosition value)
    {
        lastKnown = value;
        Console.WriteLine("At {0} we have moved to {1}", DateTime.Now, value);
        StillTracking = true;
    }
}

Main

Finally, we tie everything together in Main():

public static void Main()
{
    GPSSensor sensor = new GPSSensor();
    Map map = new Map();
    sensor.Subscribe(map);

    do
    {
        sensor.GetPosition();
        Thread.Sleep(1000);
    } while (map.StillTracking);
}

This produces output like the example below:

image