【问题标题】:Dependency Injection for singelton class with properties具有属性的单例类的依赖注入
【发布时间】:2021-05-16 03:53:14
【问题描述】:

我的 ASP.NET 4.8 Framework 网站中有一个自定义上下文类:

public sealed class MyCustomContext
{
    private static readonly Lazy<MyCustomContext> staticContext =
        new Lazy<MyCustomContext>(() => new MyCustomContext());

    private MyCustomContext()
    {
    }

    public static MyCustomContext Current => staticContext.Value;

    public HttpContext Context => HttpContext.Current;

    // Logic to return current user based on logged in user
    public User LoggedInUser => ...
    
    // Logic to return SiteWideSettings
    public Collection<SiteWideSettings> SiteWideSettings => ...
}

上面的类是一个Singleton,上面的类在我的服务类方法中的用法是这样的:

public class MyService : IMyService
{
    public MyService()
    {
    }

    public void DoWork()
    {
        var current = MyCustomContext.Current;
        var loggedInUser = current.LoggedInUser;
        var siteWideSettings = current.SiteWideSettings;
        var currentContext = current.Context;
        // use the above properties further for this method
    }
}

我的目标是删除 MyService 类的 DoWork 方法中硬编码的 MyCustomContext 类依赖项,使其看起来像这样:

public class MyService : IMyService
{
    private readonly IMyCustomContext _myCustomContext;
    public MyService(IMyCustomContext myCustomContext)
    {
        _myCustomContext = myCustomContext;
    }

    public void DoWork()
    {
        var current = _myCustomContext.Current;
        var loggedInUser = current.LoggedInUser;
        var siteWideSettings = current.SiteWideSettings;
        var currentContext = current.Context;
        // use the above properties further for this method
    }
}

您能否分享如何转换我的MyCustomContext 类,以便可以通过依赖注入将其注入MyService

我还有一个问题,MyCustomContext类的LoggedInUserSiteWideSettingsContext等属性应该写成属性还是转换成依赖注入的方法?

【问题讨论】:

  • "你能分享一下如何转换我的 MyCustomContext 类,以便它可以通过依赖注入注入到 MyService 中吗?"你能证明你已经尝试过什么,以及你在转换过程中遇到了什么问题吗?

标签: c# dependency-injection unity-container


【解决方案1】:

对于依赖注入,您需要一个初始化的接口,因此您的 MyCustomContext 类需要实现一个名为 IMyCustomContext 的新接口。界面如下所示:

public interface IMyCustomContext
{
    HttpContext Context { get; }
    User LoggedInUser { get; }
    Collection<SiteWideSettings> SiteWideSettings { get; }
}

public class MyCustomContext : IMyCustomContext
{
    public HttpContext Context
    {
        get { return HttpContext.Current; }
    }


    public User LoggedInUser
    {
        get
        {
            // Logic to return current user based on logged in user
        }
    }

    public Collection<SiteWideSettings> SiteWideSettings
    {
        get
        {
            // Logic to return SiteWideSettings
        }
    }
}

Startup.cs中有一个名为ConfigureServices的方法,你可以在里面添加如下依赖注入:

container.RegisterType<IMyCustomContext, MyCustomContext>(
    TypeLifetime.Singleton);

【讨论】:

  • 你能分享一下,如果写成 AddSingleton,为什么我必须让类密封?你能分享一下该类的最小起订量属性吗?传统的方法是设置方法的起订量。
  • 您不需要将其密封。我只是假设这是您提出的要求,因为这只是您的第一个 code-sn-p 中的类。
【解决方案2】:

值得指出的是,Singleton在这里有双重含义:

  • 单例设计模式确保一个对象只被实例化一次。但它的实现并不理想,因为它依赖于环境状态。
  • IOC 框架使用 Singleton Lifetime,它确保每次都使用相同的对象引用。

简而言之,Singleton Lifetime 有效地消除了实现设计模式的需要,因为 IOC 框架为您确保了支持概念。

意思是,如果我们用 Singleton Lifetime 注册我们的依赖项。

container.RegisterType<ICustomContext, MyCustomContext>(TypeLifetime.Singleton);

我们可以删除单例模式的代码,因为 IOC 容器将接管保护单个实例/引用的责任。

public class MyCustomContext : ICustomContext
{
    public HttpContext Context => HttpContext.Current;

    // Logic to return current user based on logged in user
    public User LoggedInUser => ...
    
    // Logic to return SiteWideSettings
    public Collection<SiteWideSettings> SiteWideSettings => ...
}

我还为我们感兴趣的成员添加了ICustomContext 接口。

public interface ICustomContext
{
    HttpContext Context { get; }
    User LoggedInUser { get; }
    Collection<SiteWideSettings> SiteWideSettings { get; }
}

您能分享一下该类的最小起订量属性吗?

没错,我们只是将问题移到了一级,不是吗?如果需要提取接口,通常需要以递归方式进行。

这也意味着HttpContext 不是接口成员的好人选,这在您考虑时是有道理的。从单元测试的角度来看,我们对验证 ASP.NET 的内部工作不感兴趣。相反,我们想检查我们自己的代码,并且只检查那部分,而不依赖于外部库。为此,您应该只将您需要的HttpContext 成员复制到您的界面上,并删除对HttpContext 的依赖(众所周知,这很难抽象)。

例如:

public interface ICustomContext
{
    IPrincipal User { get; }
    User LoggedInUser { get; }
    Collection<SiteWideSettings> SiteWideSettings { get; }
}

随着属性数量的增加,这将需要一些重构/重构。

对于简单的 DTO,您甚至可以选择不对它们进行抽象/接口,只要您能够轻松地为单元测试创​​建假货。还要记住,只有在有多个实现时才引入接口才有意义。

关于依赖倒置以及 IOC 框架如何工作的另一件事,您通常会让依赖冒泡。推荐的方法是通过构造函数注入,如下面的ICustomContext 单元测试实现所示。

public class TestCustomContext : ICustomContext
{
    public MyCustomContext(IPrincipal user, User loggedInUser, Collection<SiteWideSettings> siteWideSettings)
    {
        User = user;
        LoggedInUser = loggedInUser;
        SiteWideSettings = siteWideSettings;
    }

    IPrincipal User { get; }
    User LoggedInUser { get; }
    Collection<SiteWideSettings> SiteWideSettings { get; }
}

我还有一个问题,MyCustomContext类的LoggedInUserSiteWideSettingsContext等属性应该写成属性还是应该转换成依赖注入的方法?

你可以两者兼得。如果状态是通过构造函数注入来注入的,那么您不妨将其作为属性公开。如果实现类实现了创建/转换状态的行为,您可能希望将行为公开为方法。这一切都取决于实际情况,这里没有金子弹。请记住,在 OO 设计中,接口用于对行为进行建模,其范围尽可能小。


更新

这些属性没有通过构造函数填充。所有这些属性“IPrincipal User { get; } User LoggedInUser { get; } Collection SiteWideSettings { get; }” 在其 getter 中都有主体,它们首先从缓存中获取数据,如果未找到,则调用服务来获取来自 db 的这些属性的数据(所有这些都写入这些属性的获取中)。我应该只将它们保留为属性还是将它们设为方法?

让我分开你的问题。

我应该只将它们保留为属性还是将它们设为方法?

从技术角度来看,这并不重要。属性或自动化属性(如您正在使用的属性)只是完整方法的语法糖。这意味着,它们都被编译成等效的 CIL 指令。

那就只剩下人为因素了。代码的可读性和可维护性。商定的编码风格和实践。这不是我可以为你回答的。就个人而言,我更喜欢处理这类代码流的方法。

他们首先从缓存中获取数据,如果没有找到,则调用服务从数据库中获取这些属性的数据(所有这些都写入这些属性的获取中)。

听起来这个类更像是一个 service 提供者,而不是您域中的实际 model 类。由于还涉及 I/O,我绝对建议您在界面上切换到异步方法。显式(基于任务)签名对阅读您的代码的其他开发人员说明了很多。

我谈到冒泡的依赖关系的部分在这里起着重要作用。缓存和存储库都是MyCustomContext 的依赖项。 IOC 及其固有的依赖倒置原则依赖于依赖的显式声明,如下面的示例所示。注意GetLoggedInUser() 的实现在这里并不重要,而是通过构造函数设置依赖项的方式。所有这些依赖项都需要先在您的 IOC 容器中注册,才能解析ICustomContext

public class MyCustomContext : ICustomContext
{
    private readonly IUsersCache _usersCache;
    private readonly IUsersRepo _usersRepo;

    public MyCustomContext(IUsersCache usersCache, IUsersRepo usersRepo, IPrincipal principal)
    {
        _usersCache = usersCache;
        _usersRepo = usersRepo;
        Principal = principal;
    }

    public IPrincipal Principal { get; }

    public async Task<LoggedInUser> GetLoggedInUser()
    {
        var userId = await GetUserId(Principal);
        var user = _usersCache.GetById(userId);
        if (user == null)
        {
            user = _usersRepo.GetById(userId);
            _usersCache.Add(user);
        }

        return user;
    }

    ...
}

这些属性没有通过构造函数填充。所有这些属性“IPrincipal User { get; } User LoggedInUser { get; } Collection SiteWideSettings { get; }” 在其 getter 中都有正文

我认为IPrincipal 不是这样,因为它与HttpContext 一起由 ASP.NET 在幕后实例化。您需要做的就是告诉 IOC 容器如何解析当前的IPrincipal 并让它发挥它的魔力。

同样,所有依赖于ICustomContext 的类都应该由 IOC 容器注入。

public class MyService : IMyService
{
    private readonly ICustomContext _customContext;

    public MyService(ICustomContext customContext)
    {
        _customContext = customContext;
    }

    public async Task DoWork()
    {
        var currentPrincipal = _customContext.Principal;
        var loggedInUser = await _customContext.GetLoggedInUser();
        ...
    }
}

这里的一个重要部分是单元测试。如果你像这样设计你的类,你可以很容易地创建用于测试的假货。即使不涉及任何测试(我不推荐),像这样解耦类的能力也很好地表明了设计良好的代码库。

【讨论】:

  • 那些属性没有被构造函数填充。所有这些属性“IPrincipal User { get; } User LoggedInUser { get; } Collection SiteWideSettings { get; }” 在其 getter 中都有正文,它们首先从缓存中获取数据,如果未找到则调用服务从 db 获取这些属性的数据(所有这些都写入这些属性的获取中)。我应该只将它们保留为属性还是将它们设为方法?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-04
  • 1970-01-01
  • 2011-11-16
  • 1970-01-01
  • 2015-07-07
相关资源
最近更新 更多