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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2014年5月29日

CPOL

7分钟阅读

viewsIcon

15497

downloadIcon

273

使报表更改更轻松。Unity 帮助我们

引言

我因病休假一周。我有时间更多地了解 Unity。这里介绍一个真实世界的例子,讨论我如何使用 Unity——一个报表系统,我试图让我们的工作比以前更容易。

首先,我的报表系统有哪些实际需求?

  • 在预定时间将报表发送给客户。
  • 每个客户报表都可以有其自定义设计和自定义数据要求。
  • 每天可以多次发送每个报表。
  • 可以手动发送每个报表。
  • 报表导出预定时间应可手动更改。
  • 一个日志系统,跟踪报表是否已导出。如果未导出,发生了什么以及任何异常?
二是将来如何简化该系统,以便添加更多报表。**目标是如何最大限度地减少我们在系统中添加更多报表时的工作量。**

背景

开发这样的系统并不难。即使是初级开发人员,如果自定义设计没有太多不同,也可以在一周内完成这个系统。但随着客户和报表类型的增加,C# 项目变得越来越复杂,难以管理。当我们将新报表添加到系统中时,我们可能会遇到问题

  • 每次添加新报表时,我们在现有类中添加新方法?不行!如果你不想有一个包含 10,000 行代码的类!
  • 每次添加新报表时,我们添加一个新类来导出新报表?是的,这比一个新方法要好。但是你需要修改其他部分的​​代码来定义时间、添加客户姓名吗?
  • 无论你在哪里添加新的报表代码,你都需要关心日志系统吗?如果你是团队的新成员,并且不太了解这个系统是如何工作的?
  • 你能在一个独立的项目中编写代码并测试它,然后只用 10 秒就能完美地迁移到报表系统中吗?

为了避免上述潜在问题,我们需要创建一个系统,一旦系统架构创建完成

  1. 如果我们有一个新客户,我们可以在任何地方编写代码,然后进行测试。
  2. 我们将这个新类复制到我们项目的子目录中。(不要触碰该项目的任何其他地方!)
  3. 我们只需要在类上添加一个属性来定义时间。(你始终只在该类中工作,不要触碰该项目的任何其他地方)
  4. 日志?不用担心。项目中的某些东西已经为你做好了。
  5. 我们重新构建项目并运行它。它将支持你的新报表!

显然,优点是**任何人都可以将新的报表类添加到系统中,而无需了解系统如何工作,无需了解计时器系统如何工作,也无需了解日志系统如何工作**。

使用代码

要构建这样的系统,我们有几个基本任务要做

  • 系统自动加载“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 很有意义。顺便说一句,这篇文章开头提到的问题是我遇到的实际问题。

© . All rights reserved.