很多情况下,软件需要将运行过程中产生的必要信息(日志或报警信息)实时输出,以便用户及时关注到系统健康状态,如下图。
在客户端软件中,一般有专门的窗口来显示报警信息,但报警信息的产生却可能发生在系统的各个地方,如UI层的不同窗口,业务层的方法。
经常见到的一种实现方式是使用事件机制,如下示例代码,在产生报警信息的窗口Form1、Form2...中定义事件:
public partial class Form1 : Form
{
///
/// 定义产生日志的事件
///
public event LogEventHandler LogGenerate;
///
/// 实例对象,便于访问
///
public static Form1 Instance = new Form1();
public Form1()
{
InitializeComponent();
}
}
public partial class Form2 : Form
{
///
/// 定义产生日志的事件
///
public event LogEventHandler LogGenerate;
///
/// 实例对象,便于访问
///
public static Form2 Instance = new Form2();
public Form2()
{
InitializeComponent();
}
}
...
然后在显示报警信息的窗口中,给这些事件注册统一的报警输出方法:
public partial class OutLogForm : UserControl
{
public OutLogForm()
{
InitializeComponent();
LogRegister();
}
///
/// 给Form1、Form2、Form3注册显示报警信息的方法
///
private void LogRegister()
{
Form1.Instance.LogGenerate += ShowLog;
Form2.Instance.LogGenerate += ShowLog;
Form3.Instance.LogGenerate += ShowLog;
}
///
/// 显示报警信息
///
///
///
private void ShowLog(object sender, LogEventArgs e)
{
this.richTextBox1.AppendText($"{e.Log}{Environment.NewLine}");
}
}
这样,在Form1、Form2中如果产生了报警信息,则直接触发事件LogGenerate,便能实现报警信息的显示。
上述实现方式还可以做进一步优化,比如给产生报警信息的类定义统一的接口,然后通过反射统一注册,还可以将事件定义为静态事件,这样就不用公开实例对象。
事件机制实现比较简单直接,但上面的代码存在一些设计上的问题:
显示报警信息的类需要知道所有会产生报警的类;
如果显示报警的类发生修改,或者是更换成其他实现,也会影响到报警事件的注册代码;
因为事件注册的原因,事件源Form1、Form2持有对窗口OutLogForm的引用,可能造成OutLogForm窗口资源无法释放的问题。
这里面其实就涉及到一些设计原则:迪米特法则(也叫最少知道原则)和开放封闭原则,即降低耦合、提高可扩展性。
我们从降低耦合的角度重新去思考:报警信息的产生者无需知道报警信息最终会如何显示、由谁显示,它仅仅需要将报警信息抛出来(上面的事件机制基本已经达到这个目的了,就是麻烦了些);报警信息的显示者也无需知道报警信息由谁产生,它要做的只是提供一个接口,用来显示信息而已。
按照这个思想,我们就不能让报警显示类去依赖产生类;反过来,如果让产生类去依赖显示类,每次有报警产生时,主动调用显示类的方法,这样的话,依赖不仅没有消除,还被分散在各处,并且底层模块产生报警时,是无法直接访问上层显示类的,再者,如果后续显示类发生修改,那系统中各个产生类都有可能受到影响。分析到这一步,我们意识到---不能依赖具体的显示类,这就是设计原则中的依赖倒置原则。依赖倒置原则指导我们面向接口编程,因为接口是相对独立且稳定的,不仅能避免与具体实现类间的耦合,对接口的不同实现也提高了功能的可扩展性。接口与实现的绑定,我们可以使用IOC容器去实现。这看起来是个不错的想法,我们下面进行代码实现,这里使用Autofac容器。
首先定义记录报警信息的接口ISystemLog,这个接口建议定义在基础服务层,这样UI和业务服务都能访问到
public interface ISystemLog
{
///
/// 记录报警信息
///
///
void Log(string log);
}
在UI层具体显示报警信息的窗口中实现该接口
public partial class OutLogForm : UserControl, ISystemLog
{
public static OutLogForm Instance = new OutLogForm();
public OutLogForm()
{
InitializeComponent();
}
///
/// 显示报警信息,实现ISystemLog接口的方法
///
///
public void Log(string log)
{
// 考虑存在非UI线程产生的日志,这里使用UI线程
this.Invoke(new Action(() =>
{
this.richTextBox1.AppendText($"{log}{Environment.NewLine}");
}));
}
}
在系统启动时,使用OutLogForm的实例来注册记录报警信息的服务ISystemLog
using Autofac;
using LogDemo.Service;
using System;
using System.Windows.Forms;
namespace LogDemo
{
internal static class Program
{
///
/// 应用程序的主入口点。
///
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ContainerRegister();// 容器注册
Application.Run(new MainForm());
}
///
/// 容器注册
///
private static void ContainerRegister()
{
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterInstance(OutLogForm.Instance).As();
ContainerHelper.Container = builder.Build();
}
}
}
其中ContainerHelper类是为了方便访问容器服务而创建的帮助类:
public static class ContainerHelper
{
public static IContainer Container { get; set; }
public static T GetService()
{
return (T)Container?.Resolve(typeof(T));
}
}
至此,记录报警信息的容器服务已经有了。为了方便访问ISystemLog服务,我们使用SystemLogService类来做一个简单的封装。同ISystemLog接口一样,SystemLogService也建议定义在基础服务层,这样UI和业务服务都能直接调用:
public class SystemLogService
{
///
/// 记录报警信息
///
///
public static void Log(string log)
{
ISystemLog systemLog = ContainerHelper.GetService();
systemLog.Log(log);
}
}
现在,我们准备在Form1、Form2...中记录报警信息,之前声明的事件都可以删掉了,直接调用SystemLogService中的Log方法即可。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, System.EventArgs e)
{
SystemLogService.Log("产生报警");
}
}
这样我们便利用容器实现了完整的报警信息的产生与显示。
我们来看下这种实现方式的优点:
低耦合:具体的报警信息类OutLogForm不再与产生报警的类有耦合,产生报警的类也不需要关心报警信息输出到哪里了;
可扩展性好:如果再增加产生报警的类,直接调用服务即可,显示报警的类不需要做任何修改;如果后面哪天需要换个窗口显示报警信息,或者换成了文件记录,只需要注册新的服务即可,这也是开放封闭原则中“对修改关闭,对扩展开放”的体现。
举例:如果后面需求变更,要将报警信息输出到文本文件中,只需要新增一个写文件的类OutLogToFile,实现ISystemLog接口,然后在容器注册的地方,用这个写文件的类来注册ISystemLog服务即可。代码如下:
public class OutLogToFile : ISystemLog
{
private static string logFile = "log.txt";
private static readonly object logLock = new object();
static OutLogToFile()
{
File.Create(logFile).Dispose();
}
///
/// 显示报警信息,实现ISystemLog接口的方法
///
///
public void Log(string log)
{
lock (logLock)
{
File.AppendAllText(logFile, log + Environment.NewLine);
}
}
}
///
/// 容器注册
///
private static void ContainerRegister()
{
ContainerBuilder builder = new ContainerBuilder();
//builder.RegisterInstance(OutLogForm.Instance).As();
// 换成用OutLogToFile来注册ISystemLog服务
builder.RegisterType().As();
ContainerHelper.Container = builder.Build();
}
以上就是对系统报警信息输出的实现方式探讨。