65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Observer 和 Singleton 模式的 C# Central 日志记录机制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (34投票s)

2007年11月16日

CPOL

8分钟阅读

viewsIcon

147444

downloadIcon

1488

一种非常强大且简洁的方式,可以为应用程序添加各种日志记录

引言

任何一个有信誉的应用程序都需要进行日志记录。无论是为了跟踪应用程序的内部运行情况(以便可能的故障排除),还是为了显示我们可能需要通知用户的事件列表,或者仅仅是为了保留事件历史记录,日志记录对于编程世界来说,就像课堂笔记对于教室一样必要。如果你不做,你可能可以暂时应付过去。但要寄希望于你永远不需要回顾几分钟、几小时或几天前发生的事情。

本文介绍了一种在 C# 项目中添加日志记录的出色机制。它功能强大且用途广泛,但又非常简单易于实现和使用。它使用了两个非常常见的编程模式:单例模式和观察者模式。

背景

这里展示的思想和代码并非高深莫测,只要你至少对面向对象编程有基本的了解,并且已经使用 C# 一段时间了,我认为你就可以胜任。

说了这些,如果你想从中获得最大收益,最好还是先复习一下一些知识。我建议你阅读一下设计模式,并重点关注一些更常见的模式,如单例模式和观察者模式。

为了简明扼要,我将稍微深入地介绍一下这两种模式,让你对它们有个大概的了解。

单例模式

用通俗易懂的语言来说...

单例模式可能是最广为人知且最简单的模式之一,它是一种机制,可以确保你进程中的所有对象都能访问共享的、唯一的 数据和方法集。

想象一个十字路口,几条道路在此汇合,汽车需要确保在穿过这个十字路口时不会相撞。假设没有交通信号灯来指导它们,我们需要一个交警站在中间指挥交通。这位交警就是单例的现实生活中的近似。

交警会从所有道路接收视觉信号,告诉他哪条道路的汽车最多。根据所有这些信息,他可以决定让哪条路的汽车通行,以及阻挡哪条路的汽车。

如果有多位交警同时指挥这个十字路口,情况就会复杂得多。他们都会从各条道路接收视觉信号,彼此沟通,然后共同决定让哪条路的交通通行。

同样,单例对象是系统中所有对象共享的东西。对于日志记录机制而言,单例是唯一一个所有其他对象都会将它们想要记录的信息发送到的对象。

这不仅可以集中管理和简化日志记录机制的控制,还可以让开发人员以一种出色的方式为日志输出提供统一的格式。你可以在一个地方添加时间戳、标题、参数信息等。任何需要记录信息的对象都不必为此操心,因为单例日志记录器会自行处理。

将其放入代码...

当需要使用普通类时,它们会被实例化为一个对象,然后使用。然而,单例类不允许除了它自身以外的任何人实例化它。这保证了该对象只会有一个副本在运行,并且程序中的所有对象都将访问这一个副本。

为了实现这一点,日志记录器只有一个 `private` 构造函数和一个它自己的类型的 `private` 变量。那么,从外部真正获取该对象的唯一方法就是实例化 `Logger` 类,并将新对象赋给类中暴露的 `static` 句柄。然后,`Instance` 属性会检查 `private mLogger` 对象是否被创建过,如果没有,那么这是 `Logger` 唯一会被实例化的位置。

class Logger
{
    private static object mLock;
    private static Logger mLogger = null;
    // the public Instance property everyone uses to access the Logger
    public static Logger Instance
    {
        get 
        { 
            // If this is the first time we're referring to the
            // singleton object, the private variable will be null.
            if (mLogger == null)
            {
                // for thread safety, lock an object when
                // instantiating the new Logger object. This prevents
                // other threads from performing the same block at the
                // same time.
                lock(mLock)
                {
                    // Two or more threads might have found a null
                    // mLogger and are therefore trying to create a 
                    // new one. One thread will get to lock first, and
                    // the other one will wait until mLock is released.
                    // Once the second thread can get through, mLogger
                    // will have already been instantiated by the first
                    // thread so test the variable again. 
                    if (mLogger == null)
                    {
                        mLogger = new Logger();
                    }
                }
            }
            return mLogger; 
        }
    }
    // the constructor. usually public, this time it is private to ensure 
    // no one except this class can use it.
    private Logger()
    { 
        mLock = new object();
    }
}
public class SomeWorkerClass
{
    // any class wanting to use the Logger just has to create a 
    // Logger object by pointing it to the one and only Logger
    // instance.
    private Logger mLogger = Logger.Instance;
}

观察者模式

再次用通俗易懂的语言来说...

这里事情变得有点复杂,但也更有趣。观察者模式本质上是一种机制,其中一个对象(如我们上面例子中的交警)能够向一个或多个观察者发送消息。他们如何处理这条消息完全取决于他们,事实上,交警甚至不知道他们是谁,也不知道他们在做什么。他只知道有多少观察者,并有一个预定义的方法来向他们提供信息。

回到交通例子:每当交警需要让某条路的交通停止时,他就会向该方向张开手掌。实际上,他正在向所有观察他以获取指示的交通发送一条消息。面对他的交通会解读这条消息并停止,而其他交通则会解读这条消息并相应地忽略它,因为它对它们没有影响。

接下来,他向另一条路的交通挥手,表示司机应该开始移动。同样,他发送了另一条消息,所有司机都会以相同的方式接收到它,但以不同的方式做出反应。面对他的交通会开始移动,而其他交通则会简单地忽略这条消息。

到目前为止,我们有一个被观察的主题(交警)和几个观察者(司机)。然而,这种模式的优点在于,还可能存在其他类型的观察者。只要它们能够以与司机相同的方式接收交警的消息(也就是说,只要它们有视力并看着他),它们也可以对消息做出反应。一个简单的例子是,交警实际上是一名正在受训的学员,而一名高级警官坐在他的车里观察这位学员,并记录下停车和前进的消息。同样,这位高级警官在观察学员并以与司机相同的方式接收他的消息,但以不同的方式做出反应。

用编程的术语来说,交警会通过一个接口向观察者发送消息。如果一个给定的对象实现了某个接口,那么任何其他对象都可以通过该接口公开的属性和方法与之交互,而无需知道对象的真实本质。

代码实现也一样...

这种模式的第一部分是使用一个接口,该接口允许 `Logger` 将新的日志条目分派给观察它的对象。它的优点在于,这些对象可以是任何类型的,并提供各种功能。它们可以是窗体、文件写入器、数据库写入器等。但是 `Logger` 只知道它们实现了这个接口,并且能够通过它与它们进行通信。

interface ILogger
{
    void ProcessLogMessage(string logMessage);
}

接下来是在 `Logger` 中管理这些观察者。我们需要一个数组(或 `List<>`)来存储它们(技术上来说,我们只存储对它们的引用),以及一个 `public` 方法,通过该方法可以将它们添加到数组中。

class Logger
{
    private List<ILogger> mObservers;
    private Logger()
    { 
        mObservers = new List<ILogger>();
    }
    public void RegisterObserver(ILogger observer)
    {
        if (!mObservers.Contains(observer))
        {
            mObservers.Add (observer);
        }
    }
}

每当我们想要实现一个新的观察者时,我们只需要确保它实现了 `ILogger` 接口,并在执行 `ProcessLogMessage` 方法时执行有意义的操作。例如,可以有一个 `FileLogger` 对象,它将日志消息写入文件。

class FileLogger: ILogger
{
    private string mFileName;
    private StreamWriter mLogFile;
    public string FileName
    {
        get 
        { 
            return mFileName; 
        }
    }
    public FileLogger(string fileName)
    {
        mFileName = fileName;
    }
    public void Init()
    {
        mLogFile = new StreamWriter(mFileName);
    }
    public void Terminate()
    {
        mLogFile.Close();
    }
    public void ProcessLogMessage(string logMessage)
    {
    // FileLogger implements the ProcessLogMessage method by
    // writing the incoming message to a file.
        mLogFile.WriteLine(logMessage);
    }
}

然后,该类将被实例化并如下传递给 `Logger`:

public partial class Form1 : Form
{
    // the main Logger object
    private Logger mLogger;
    // a logger observer that will write the log entries to a file
    private FileLogger mFileLogger;
    private void Form1_Load(object sender, EventArgs e)
    {
        // instantiate the logger
        mLogger = Logger.Instance;
        // instantiate the log observer that will write to disk
        mFileLogger = new FileLogger(@"c:\temp\log.txt" );
        mFileLogger.Init();
        // Register mFileLogger as a Logger observer.
        mLogger.RegisterObserver(mFileLogger);
    }
    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        // The application is shutting down, so ensure the file 
        // logger closes the file it's been writing to.
        mFileLogger.Terminate();
    }
}

将这两种模式结合起来

我们的 `logger` 将同时使用这两种模式,并且附带的示例代码使用了两个观察者:上面提到的 `FileLogger`,以及实际的窗体本身。`FileLogger` 将消息记录到文件中,窗体则在 `textbox` 控件中显示消息。这显然是一个简单的实现,但可以在更复杂的场景中使用。我们可以有一个将日志条目写入数据库表的观察者,另一个将所有条目连接起来然后通过电子邮件发送日志,等等。

每当窗体需要记录某事时,它只需在记录器上执行 `AddLogMessage()` 并将日志条目传递给它。

class Logger
{
    public void AddLogMessage(string message)
    {
        // Apply some basic formatting like the current timestamp
        string formattedMessage = string.Format("{0} - {1}", 
		DateTime.Now.ToString(), message);
        foreach (ILogger observer in mObservers)
        {
            observer.ProcessLogMessage(formattedMessage);
        }
    }
}
public partial class Form1 : Form
{
    private void button1_Click(object sender, EventArgs e)
    {
        mLogger.AddLogMessage("The button was clicked.");
    }
}

Using the Code

示例项目不执行任何复杂操作,但展示了本文讨论的日志记录功能。主窗体有一个按钮,点击后会增加一个 `private` 计数器,并在文本框中显示该值。

然而,每次计数器增加时,都会通知日志记录器。反过来,日志记录器会格式化收到的消息,并将其分派给所有观察者进行处理。

一个不错的附加功能是,`Logger` 的 `AddLogMessage()` 方法被重写以接受一个异常。如果应用程序抛出一个异常,并且我们希望它被妥善记录,我们只需将异常传递给 `Logger`,它就会提取异常和内部异常(如果适用)中的所有消息,将它们组合起来,添加堆栈跟踪,然后记录整个过程。非常有用。

结论

我希望这篇文章和附带的示例能有所帮助。如果您有任何改进建议,我非常乐意听取,请发布一条消息,告诉我们您的想法!

历史

  • 2007 年 11 月 17 日 – 初始发布
© . All rights reserved.