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

使用 Unity App Block 快速实现 DI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (2投票s)

2013年9月3日

CPOL

9分钟阅读

viewsIcon

11257

如何使用 Unity 应用块识别依赖并解决它们。简单的“Hello World”。

引言

在大型企业应用程序中,许多组件相互交织。理想情况下,组件之间应该是松耦合且高度内聚的,但在快速开发模式下,我们往往会把事情搞混。

随着需求的不断变化,我们必须对系统进行相应的修改,但事后我们却后悔不已,因为系统变得过于复杂。由于模块相互依赖,在一个模块中引入更改,同时又要确保不破坏其他模块,这变得非常困难。

这个问题的解决方案是一组被命名为“控制反转”的模式。这个短语表明了思维方式的转变。以前,使用依赖的组件负责在其内部维护该依赖。现在,这种责任被剥离出来,交给了其他人。

Unity Application Block 是微软的一个工具集,它允许组件首先注册依赖;然后在需要时解析该需求;任务完成后,则释放关联。

因此,在我们开始检查 Unity 的作用及其工作原理之前,我们首先需要弄清楚到底什么是依赖,更重要的是,如何在现有架构中识别依赖。然后,我们将思考将所有东西都包含在内部有什么问题,并进行整理,以实现所有 SOLID 目标方面的恰当关注点分离。

背景

面向切面编程(Aspect-oriented programming)的核心是“关注点”(concerns),以及如何成功地管理它们。与其将应用程序视为一系列独立的对象的集合,不如将应用程序的每个功能视为一个关注点。

与特定应用程序直接相关的组件是“核心关注点”。AOP 将所有其他类型的组件和代码视为“横切关注点”(crosscutting concerns)。这些功能对于许多应用程序来说是通用的或半通用的,它们通常包括诸如验证、授权、缓存、结构化异常处理、日志记录和性能监控等熟悉的功能。面向切面编程技术旨在帮助您更有效地管理这些横切关注点。
有些需求将特定于所讨论的系统,而有些将更具通用性。您可以将一些需求归类为功能性需求,另一些则归类为非功能性需求(或质量属性)。

识别依赖 

依赖的字面意思是执行核心任务所必需的东西。

通常,我们会在自己的模块中创建/使用/释放(CUD)依赖。因此,在开发初期我们不会注意到它,直到许多其他部分也需要使用该公共函数/实体。即使在这种情况下,相同的 CUD 代码也会在所有需要的地方被复制。

这会产生大量重复的代码,并且依赖本身发生的任何更改都必须在所有地方进行复制和测试。

如果它仅仅是为了特定任务,那么它的范围仅限于该实现。但是,如果依赖是许多功能所必需的,那么它的范围就是整个模块。

如果我们有一个类具有某些函数,第一种情况通过“函数参数”来解决。如果一个以上的函数需要它,那么就使用一个属性来暴露该依赖。

因此,我们将首先构建一个非常简单的应用程序,并在其中显式地放置一个依赖。然后,我们将逐步解决这个依赖。

好的,让我们从我们总是为任何新目的设计的第一个程序开始,“上帝”的“Hello World” :)
试想一下,在一个如此过于简单的程序中,可能存在什么依赖……!!!它所做的只是在设备上打印“hello world”。如果最后一句被稍微批判性地阅读,您就会发现依赖是“设备”。
为什么???
通常,设备被认为是标准输出“std-out”,它始终是监视器。但是,我们可以将文本写入任何我们想要输出到的地方。无论我们选择何种输出,其方法论和 API 都依赖于设备。对于屏幕,是简单的“console.out”;对于文件,是“FileStream.Write”;对于数据库,是“connection.Execute(storedProc)”……等等。在这里,每个输出设备都可以被视为一个“流”(并且实际上在 API 中实现为一个)。
因此,为了定义我们将数据写入流的业务操作,我们设计了一个抽象方法;无论您将其放在抽象类还是接口中,这取决于您,那是另一个讨论。

void WriteToStream(string message);

现在,让我们暂时忘记 IoC,并决定我们将设备设置为一个文件,并将其所有相关 API 都包含在我们的主类中。因此,将我们的主应用程序视为一个 Web 窗体应用程序,我们有一个页面 Welcome.aspx。它所做的只是将“Hello World”写入一个文件。这开始显得有些粗糙,但我们会慢慢构建起来。

public partial class Welcome : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        WriteToFile();
    }

    private void WriteToFile()
    {
        string message = "Hello World";
        System.IO.File.AppendAllText(@"c:\MyTestFile.txt", 
                     message, System.Text.ASCIIEncoding.ASCII);
    }
}

这个看似简单的程序有很多问题(我们将讨论),但最关键的是“它无法运行”。这将引发一个“安全异常”,因为 Web 调试器帐户不允许写入“C:\”驱动器……

我们对此能做什么?很简单,我们只需将位置更改为“E:\”。虽然这个位置在我的系统上存在,但我永远不能确定它在您的系统中也存在。因此,如果我将编译后的应用程序发送给您,它将引发一个 PathNotFoundException,或者如果这个位置被映射到 DVD 驱动器,它将尝试写入那里但会失败,因为 DVD 不是使用标准文件 IO 写入的。如果这是一个共享驱动器,那么帐户必须具有写入该驱动器的权限(不是您,而是 Web 帐户;除非您进行模拟,否则永远不会)。

第一次尝试就出现了这么多问题,我们不知道还会出现多少问题。

但是,所有这些以及更多未知问题的解决方案只有一个。将这种麻烦移到另一个组件,让它去烦恼如何写入消息,以及想在哪里写入。甚至,它是否真的想写入……

public class FileMessenger 
{
    private string _FilePath;        

    public FileMessanger()
    {
        string filePath = @"c:\myfolder\myfile.txt";
        var dir = Path.GetDirectoryName(filePath);

        if (!string.IsNullOrWhiteSpace(dir))
        {
            if (!Directory.Exists(dir))
                Directory.CreateDirectory(dir);
            this._FilePath = filePath;
        }
    }

    public void WriteToStream(string message)
    {
        File.AppendAllText(_FilePath, message, System.Text.ASCIIEncoding.ASCII);
    }

    public string ReadFromStream()
    {
        return File.ReadAllText(this._FilePath);
    }
}

主组件将只是在需要时获取这个写入器组件,而写入器将负责所有写入消息所需的工作。它将接收消息,并可选择接收一个指示写入位置的指示器;否则将有一些默认设置。

我们准备好了这个组件,主组件然后通过实例化该类并使用其方法来消耗它。但是,如果它在稍后的某个阶段决定写入其他流,则需要更改很多内容。此外,由于该组件将在许多地方使用,因此会有很多地方需要消耗它,会创建和销毁很多实例。同时,处理这些实例的责任将由主组件承担。

string message = "Hello World";
FileMessenger messengerFile = new FileMessenger();
messengerFile.WriteToStream(message);

由于与该组件的全部交互都包含在主组件内,每次需要更改组件时,主组件也需要更改。这违反了单一职责原则。那么有什么选择呢?方法是接口隔离原则,因此我们有一个接口,它将用一个活动的实时对象进行实例化。这个对象需要由所谓的管理器从主类外部提供。

public interface IMessenger
{
    /// <summary>
    /// Writes a message to the implementing stream
    /// </summary>
    /// <param name="message">The text to be written. 
    /// If it's an object, do a preformat in the ToString override</param>
    void WriteToStream(string message);

    /// <summary>
    /// Reads a message from the stream
    /// </summary>
    /// <returns>Message stored within the stream. 
    /// Entire stream contents will be returned</returns>
    string ReadFromStream();
}

public class FileMessenger : IMessenger
{
    // Various methods
}
public class DatabaseManager : IMessenger
{
}

现在我们可以拥有一个接口实例,并在运行时接收所需的特定对象。看起来不错,对吧?在一个简单的应用程序中,是的;但是随着它的复杂性增加,就不行了。

但确切的问题是什么?

创建哪个实例的决定是由消费者做出的;因此,如果我们的组件有许多消费者,这个决定将分散在整个应用程序中,而不是在一个中心位置做出。

因此,下一步是将这个决定移到一个单独的组件,该组件可以管理实例并将它们提供给任何想要它们的消费者。该组件将公开内部组件的功能,并根据实例化的对象来调用该功能。消费者只会获得该外部组件的一个实例,并调用函数,而无需知道哪个实际实例响应了请求。

这样就完成了依赖循环,每个组件内部都是干净的,并且彼此之间的依赖性最小。

这由某个控制反转容器来实现,而对于我们来说,它将是微软的 Unity Application Block。

步骤

  1. 创建接口和实现类。一个实现没有类外部依赖(无参构造函数),另一个通过构造函数获取依赖。
  2. web.config 文件中创建条目以标识所需的组件。我们还可以根据需要定义其他规则和条件,如果决策依赖于许多变量。
  3. UnityInteraction 类中,我们将不同的类注册到 Unity,并将其与接口关联。现在,每当需要接口的实例时,Unity 将在内部创建一个配置类的实例并返回供其使用。
  4. 由于这是一个 Web 应用程序,并且 Unity 组件需要在整个应用程序中引用。该类在应用程序根目录实例化,在本例中是在 Global.asax.csApplication_Start 方法中,它将其存储在应用程序状态变量中,以便整个应用程序范围都可以访问。
  5. 创建一个接口的消费者,如上所示。它将由消费者调用,而不是直接调用接口或其实现。它将具有通过构造函数获取的依赖。在这里,我们在构造函数中传入接口的一个实例,并直接向接口公开方法。这里我们不关心具体的实例。从这个意义上说,这个类就像接口的装饰器,尽管它没有做任何额外的事情。
  6. 在 Web 代码中,解析消费者组件。Unity 会自动解析接口的底层依赖。在我们的主消费者类中,我们通过 Unity 解析我们的依赖,并请求 MessageInteraction 类的实例。区别在于,我们不实例化组件,甚至不实例化其接口,我们只是请求“装饰器”对象。Unity 会找出该装饰器需要一个接口的实例,然后它会在其注册数据库中查找合适的对象。它会找到一个配置,说明 FileMessenger 将被使用,并且它还需要一个参数。因此,它会获取参数并实例化 FileMessenger,然后进而实例化 MessageInteraction 并将其传递给消费者类。
<appSettings>
    <add key="MessengerType" value="FileMessanger"/>
<!--<add key="messengerType" value="DatabaseMessanger"/>-->
    <add key="FilePath" value="c:\myfolder\myfile.txt"/>
</appSettings>
public class UnityInitializer
{
    private IUnityContainer container;

    public IUnityContainer UnityContainer { get { return this.container; } }

    public UnityInitializer()
    {
        ConfigureUnity();
    }

    private void ConfigureUnity()
    {
        container = new UnityContainer();
        if (ConfigurationManager.AppSettings["MessengerType"] == "FileMessanger")
        {
            var filePath = ConfigurationManager.AppSettings["FilePath"];                
            container.RegisterType<IMessenger, 
               FileMessanger>(new InjectionConstructor(filePath));
        }
        else
            container.RegisterType<IMessenger, DatabaseManager>();
    }
}

void Application_Start(object sender, EventArgs e)
{
    UnityInitializer init = new UnityInitializer();
    Application["UnityInit"] = init;
}

public class MessageInteraction
{
    private IMessenger _messanger;

    public MessageInteraction(IMessenger messanger)
    {
        this._messanger = messanger;
    }
    public void WriteUsingUnity(string message)
    {
        _messanger.WriteToStream(message);
    }
}

protected void Page_Load(object sender, EventArgs e)
{
    var container = (this.Application["UnityInit"] as UnityInitializer).UnityContainer;

    MessageInteraction interact = 
      container.Resolve<MessageInteraction>() as MessageInteraction;

    interact.WriteUsingUnity("Hello World");
}
© . All rights reserved.