Friday, July 21, 2017

Design Pattern example: Singleton and Observer (Part I)

Copyright © 2017, Steven E. Houchin. All rights reserved.

I've lately been developing a parser library for a certain standard file format used for data interchange between some programs.  At various points in the parsing process, my code detects errors, and I wanted a mechanism to log those errors.  The parser consists of a gaggle of classes (in C#) that encapsulate various data structures, any of which may generate errors or warnings about file format issues.  I certainly didn't want to pass a reference to a logger object as a parameter to every method in every class to accomplish the error logging.

Thinking about the functionality of logging errors, multiple instances of a logger object didn't make sense.  So, I designed a logger class based upon the Singleton design pattern, where one instance of the class is created at first use, and that same instance is retained for subsequent uses:


public class Logger : object
{
    // The Singleton instance
    protected static Logger _instance;

    /// Hidden constructor
    private Logger()
    {
    }

    /// Get the only instance of the class
    public static Logger Instance
    {
        get
        {
            // Get the only instance, creating new if necessary
            if (_instance == null)
            {
                _instance = new Logger();
            }
            return _instance;
        }
    }
        
    public static void Free()
    {
        // Free the only instance when done with the parser
        _instance = null;
    }

    public void LogError(string msg)
    {
        // ... do something to log the error somewhere ...
    }
} // end class Logger

The parser library methods simply get a reference to Logger's class instance like this:

   
    Logger log = Logger.Instance;
    log.LogError("Parse error.");

Each call to the Logger's Instance property returns the same object instance, no matter where in the code it is called.

My next design task was to decide how and where to log the error message itself.  Ultimately, I decided the Logger class shouldn't be burdened with the detail of "where" to log the error. What if App1 wants the library to log to a file, and App2 wants the log rendered to the UI?  At first, I just dumped the messages into an ArrayList, and the parent App would pull them out at its leisure at a later time.  But that was inelegant and didn't notify the App of the error in real time.

The key word I just used there is "notify."  That idea implies a callback, and that brought to mind the Observer design pattern, for which notification is a central part of the concept. For example, Object-A's state changes via some event, and it notifies Object-B of the change. In Observer, though, notifications may also be sent to Objects C and D and E.  So, Objects B through E must register as interested observers with Object-A, and unregister when no longer interested.

To implement Observer, my Logger class needed a callback interface for the observers, registration/deregistration methods, and a structure of some kind to keep track of each observer.

First, the callback interface:


///
/// Interface to be implemented by objects who wish to
/// observe changes on the Logger object's data.
///
public interface ILoggerObserver
{
    void didAddLogMessage(string message);
}

Each of the observer classes must derive from the above ILoggerObserver interface and implement the didAddLogMessage method, which allows the observer to do whatever it wishes to log the message.

Next, a List to keep track of the observer registrations:


public class Logger : object
{
    // The Singleton instance
    protected static Logger _instance;

    // List of Observer objects to be notified of
    // changes to the log
    protected List(lt)ILoggerObserver(gt) _observers;

    /// Hidden constructor
    private Logger()
    {
         _observers = new List(lt)ILoggerObserver(gt)();
    }

    ...

}

Of course, the (lt) and (gt) above are really < and > that don't render well in the HTML code snippet above.

Now, we need the registration/deregistration methods in the Logger class.  These will be made more sophisticated in a follow-on posting.

    /// Add an Observer object to the list of those
    /// to be notified of changes.
    public void Attach(ILoggerObserver observer)
    {
        if (null != observer)
        {
            // Add this observer to the list, but just once
            Detach(observer);
            _observers.Add(observer);
        }
    }

    /// Remove an Observer object from the list.
    public void Detach(ILoggerObserver observer)
    {
        if (null != observer)
            _observers.Remove(observer);
    }

So, finally, the user's parser code initializes logging by registering as an Observer, and calls the parser:

public class MyParser : ILoggerObserver
{
    ...

    public void MyParseLogic()
    {
        Logger log = Logger.Instance;
        log.Attach(this); // become a Logger observer

        Parser p = new Parser(my_file_path_to_be_parsed);
        p.Parse(); // will call the same Logger instance

        log.Detach(this); // resign as a Logger observer

        ...
    }

    /// Implement the ILoggerObserver interface
    public void didAddLogMessage(string message)
    {
        // Output the message to a TextBlock control or
        // pop up a Message Box.
        ...
    }

} // end class MyParser

With follow on postings, I'll show the Logger's notification logic, and a better way to unregister as an observer.

No comments: