使用 Unity 构建简单的报告系统





5.00/5 (4投票s)
使报表更改更轻松。Unity 帮助我们
引言
我因病休假一周。我有时间更多地了解 Unity。这里介绍一个真实世界的例子,讨论我如何使用 Unity——一个报表系统,我试图让我们的工作比以前更容易。
首先,我的报表系统有哪些实际需求?
- 在预定时间将报表发送给客户。
- 每个客户报表都可以有其自定义设计和自定义数据要求。
- 每天可以多次发送每个报表。
- 可以手动发送每个报表。
- 报表导出预定时间应可手动更改。
- 一个日志系统,跟踪报表是否已导出。如果未导出,发生了什么以及任何异常?
背景
开发这样的系统并不难。即使是初级开发人员,如果自定义设计没有太多不同,也可以在一周内完成这个系统。但随着客户和报表类型的增加,C# 项目变得越来越复杂,难以管理。当我们将新报表添加到系统中时,我们可能会遇到问题
- 每次添加新报表时,我们在现有类中添加新方法?不行!如果你不想有一个包含 10,000 行代码的类!
- 每次添加新报表时,我们添加一个新类来导出新报表?是的,这比一个新方法要好。但是你需要修改其他部分的代码来定义时间、添加客户姓名吗?
- 无论你在哪里添加新的报表代码,你都需要关心日志系统吗?如果你是团队的新成员,并且不太了解这个系统是如何工作的?
- 你能在一个独立的项目中编写代码并测试它,然后只用 10 秒就能完美地迁移到报表系统中吗?
为了避免上述潜在问题,我们需要创建一个系统,一旦系统架构创建完成
- 如果我们有一个新客户,我们可以在任何地方编写代码,然后进行测试。
- 我们将这个新类复制到我们项目的子目录中。(不要触碰该项目的任何其他地方!)
- 我们只需要在类上添加一个属性来定义时间。(你始终只在该类中工作,不要触碰该项目的任何其他地方)
- 日志?不用担心。项目中的某些东西已经为你做好了。
- 我们重新构建项目并运行它。它将支持你的新报表!
显然,优点是**任何人都可以将新的报表类添加到系统中,而无需了解系统如何工作,无需了解计时器系统如何工作,也无需了解日志系统如何工作**。
使用代码
要构建这样的系统,我们有几个基本任务要做
- 系统自动加载“Reporting”子目录下的报表类(或者我们可以说在“xxxx.Reporting”命名空间下)。然后将它们注册到 UnityContainer
- 当调用任何报表实例时,系统应负责日志系统。使用 Unity 的拦截。
- 如果类中的某些方法不想被记录,我们该怎么做?使用 Unity 的 CustomAttribute 或 IAttributeHandler。
- 系统自动加载报表的默认导出时间。自定义属性并注册一个 Time Manager 实例到 UnityContainer
将报表类加载到 Unity Container
加载类通常是反射的工作。但与其加载程序集,重要的是将我们加载的内容注册到 Unity Container。
var classes = AllClasses.FromLoadedAssemblies().Where(t => t.Namespace == "ReportingWithUnity.Reporting" && t.IsClass && t.GetInterfaces().Contains(typeof(IReporting)));
foreach (Type type in classes)
{
_container.RegisterType(typeof(IReporting),
type,
type.FullName,
new InterceptionBehavior<PolicyInjectionBehavior>(),
new InterceptionBehavior<LoggingInterceptionBehavior>(),
new Interceptor<InterfaceInterceptor>());
RegisterDefaultClocks(type);
}
上面的代码使用 AllClasses 来加载所有继承 IReporting 接口的报表类。IReporting 接口包含一个 GenerateReport() 方法供系统使用。所有报表类都应继承此接口,以便它们能被报表系统识别。换句话说,报表系统将只运行继承了 IReporting 接口并且位于 ReportingWithUnity.Reporting 命名空间下的类。
日志系统
当容器注册所有报表类时,它会使用 InterceptionBehavior<LoggingInterceptionBehavior> 来注册它们。这有助于系统在调用报表类的方法时记录它们。有关 使用 Unity 进行拦截 的更多信息,请访问官方网站 http://msdn.microsoft.com/en-us/library/dn178466(v=pandp.30).aspx。
简而言之,这种拦截行为允许日志功能在方法执行之前和之后运行。它有助于将日志系统和报表类分离开来。即使我们在自定义报表类中没有编写任何日志信息,系统也会跟踪这些信息。
下面的代码显示了 LoggingInterceptionBehavior 中定义的 Invoke 方法。
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
// Before invoking the method on the original target.
if(!input.InvocationContext.ContainsKey("IfLog"))
{
WriteLog(string.Format("Invoking method {0} in Class {1}", input.MethodBase, input.Target.ToString()));
}
Stopwatch sw = new Stopwatch();
sw.Start();
var result = getNext()(input, getNext);
sw.Stop();
// After invoking the method on the original target.
if (result.Exception != null)
WriteLog(string.Format("Method {0} threw an Exception {1}",
input.MethodBase, result.Exception.Message));
else if (!input.InvocationContext.ContainsKey("IfLog"))
{
WriteLog(string.Format("Method {0} in Class {1} returned {2} ; Time used {3}",
input.MethodBase,
input.Target.ToString(),
result.ReturnValue ?? "",
sw.Elapsed));
}
return result;
}
在上面的代码中,getNext()(input, getNext) 用于跳转到下一个拦截行为。如果当前行为之后还有其他行为,则下一个行为将执行。如果没有其他行为,则调用 GenerateReport 方法。拦截行为和目标方法之间的执行顺序是:<Behavior1 <Behavior2 <Behavior3 <Target Method> Behavior3> Behavior2> Behavior1>。拦截行为被注册到一个管道中。
如果我们不想记录 Unity Container 中注册的所有报表类方法,我们该怎么办?
我尝试使用自定义属性,并分析这个属性是否在 LoggingInterceptionBehavior 执行期间调用的方法上。不幸的是,进入 LoggingInterceptionBehavior 的所有类都是 IReporting 类型。我无法捕获任何自定义属性。
因此,我不得不采用另一种方式来实现我的目标:一个写在报表类方法上的属性,用于区分该方法是否需要被记录。这种方式是通过使用 InterceptionBehavior<PolicyInjectionBehavior> 来实现的。当客户端调用容器实例化的对象时,此行为利用策略定义将处理程序插入管道。我们的处理程序是什么?
class IgnoreLogHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
input.InvocationContext.Add("IfLog", false);
var result = getNext().Invoke(input, getNext);
return result;
}
public int Order
{
get;
set;
}
}
我们如何调用这个处理程序?我们需要一个自定义属性,但它需要继承 Unity 的拦截处理程序属性。
public class IgnoreLogAttribute : HandlerAttribute
{
private readonly int _order;
public IgnoreLogAttribute(int order = 1)
{
_order = order;
}
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new IgnoreLogHandler() { Order = _order };
}
}
我们需要使用此属性调用 PolyciInjectionBehavior,以便将 IgnoreLogHandler 行为插入拦截管道。如果我们在此属性放在方法开头,该方法将不会被记录。这是在使用 IgnoreLogAttribute 的报表类中的代码。
public class Elgoog : IReporting
{
public Elgoog()
{
}
[IgnoreLog]
public bool GenerateReport()
{
SingletonLogger.Instance.AddLog("I'm Elgoog, inverse is Google");
return true;
}
}
从现在开始,我们可以**将新的报表类添加到系统中,而无需了解系统如何工作,也无需了解日志系统如何工作**。
预定义计时器和计时器设置与报表类之间的松散耦合
一切都完成了?不,还没有。在系统中,以灵活的时间生成报表非常重要。我们系统的目标是只修改报表类,而不触碰系统的其他部分。这意味着我们只能在报表类中定义默认生成报表的时间。
如果我们有一个用户界面,我们想更改计时器设置,这与我们的报表类无关,这是系统的任务。因此,在系统启动之初,系统只从报表类加载默认计时器设置。然后系统会将此计时器设置注册到另一个部分。如果我们想在程序执行期间修改计时器设置,我们可以在这个“另一个”部分更改计时器设置。

我们只需要另一个自定义属性。这与 Unity Container 没有真正的关系,但 TimerManager 被注册为 Unity Container 中的一个实例。
[AttributeUsage(AttributeTargets.Class)]
public class ReportClockAttribute : Attribute
{
/// <summary>
///
/// </summary>
/// <param name="dateTimes">Format like 06:23:12</param>
public ReportClockAttribute(params string[] dateTimes)
{
foreach (var dt in dateTimes)
{
if (dt.Length != 8)
throw new ArgumentException("Format is not good. 00:01:12 is a good format");
}
}
}
下面的代码显示了我们在报表类中如何使用此属性。
[ReportClock("12:15:35", "12:15:40", "12:15:45")]
public class MacrohardReporting : IReporting
{
public MacrohardReporting()
{
}
public bool GenerateReport()
{
SingletonLogger.Instance.AddLog("I'm Macrohard, inverse is Microsoft");
return true;
}
}
到目前为止,所有困难的都已完成。
关注点
一个使用 Unity 的真实世界例子。我没有使用太多 Unity 的技术。但对我来说仍然很酷。因为我项目里不常使用 Unity。因为有时 Unity 会降低程序的性能。我在一家金融公司工作,我的大部分项目都需要非常高性能的程序。我不能随意使用 Unity 的功能,找到一个好的方法来练习使用 Unity 很有意义。顺便说一句,这篇文章开头提到的问题是我遇到的实际问题。