In my last two postings on this subject, I outlined a Logger class used by my Parser class for logging errors. In this article, I show a more sophisticated approach for the application to register and unregister as an interested Observer with Logger, so it is notified when an error message is posted to Logger.
First, the Observer class must derive from the
ILoggerObserver
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);
}
In my original posting, I showed the following code snippet showing the Observer register and unregister sequence:
public class MyParser : ILoggerObserver
{
...
public void MyParseLogic()
{
// Register to log any parsing errors
Logger log = Logger.Instance;
log.Attach(this);
// Parse my data file
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
Note that before parsing is done, the
MyParser
class calls Logger's Attach
method to register as an observer, and after finishing with the parsing, calls Detach
to unregister. But I later came up with a better design so Detach
can be done automatically. I accomplish this by taking advantage of the .NET IDisposable
interface.I created a new container class named
LoggerObserverDisposer
, derived from IDisposable
. Its only job is to call Logger's Detach
for the Observer when it's being disposed by .NET. The target Observer object is provided to its constructor and remembered for when the Detach
is done.Here is that new Disposer class:
/// Container class for an observer, which terminates its observing
/// when out of scope.
public class LoggerObserverDisposer : IDisposable
{
protected ILoggerObserver _observer = null;
public LoggerObserverDisposer(ILoggerObserver observer)
{
// Remember the observer object to be disposed
_observer = observer;
}
public void Dispose()
{
Dispose(true); // call our Dispose method below
// prevent redundant Dispose during garbage collection
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Get the Logger object
Logger logger = Logger.Instance;
// Make operation atomic
Monitor.Enter(logger);
if (_observer != null)
{
// Remove the observer from Logger
logger.Detach(_observer);
_observer = null;
}
Monitor.Exit(logger);
}
}
} // end class LoggerObserverDisposer
Previously, here is what Logger's
Attach
method looked like:
/// 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);
}
}
Now, it is changed to make use of our new
LoggerObserverDisposer
class. Attach
returns a new LoggerObserverDisposer
instance for the Observer, but only if requested by the caller.
/// Add an Observer object to the list of those to be notified
/// of changes, and return a Disposer object for the observer,
/// if requested.
///
/// Params:
/// "observer" - The Observer object to add to the list.
/// "doMakeDisposer" - true to return a Disposer object.
/// Returns: A Disposer object, if requested, or null
public LoggerObserverDisposer Attach(ILoggerObserver observer,
bool doMakeDisposer = false)
{
if (null != observer)
{
// Add this observer to the list
Detach(observer);
_observers.Add(observer);
}
// Return an object the caller can include in a
// 'using' statement so the observer is automatically
// removed when the Disposer goes out of scope
return (doMakeDisposer) ?
new LoggerObserverDisposer(observer) : null;
}
Finally, the
MyParser
class changes to use this new version of Logger's Attach
, but never needs to explicitly call Detach
:
public void MyParseLogic()
{
// Register to log any parsing errors
Logger log = Logger.Instance;
using (logger.Attach(this, true))
{
// Parse my data file
Parser p = new Parser(my_file_path_to_be_parsed);
p.Parse(); // will call the same Logger instance
}
...
}
The changed code takes advantage of the C# "using" statement, which expects an IDisposable-derived object. When the "using" block goes out of scope, that object's Dispose
method is called. In our case, the new Attach
method returns a LoggerObserverDisposer
object (rather than void as before), which remains in scope throughout the "using", even though I never assign it to a variable. Thus, LoggerObserverDisposer::Dispose
is called when the "using" block loses scope, which in turn calls Logger's Detach
for the Observer MyParser
class.This logic saves the programmer from having to remember to manually
Detach
, and possibly leave a dangling Observer that has since been destroyed.