值得指出的是,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类的LoggedInUser、SiteWideSettings和Context等属性应该写成属性还是应该转换成依赖注入的方法?
你可以两者兼得。如果状态是通过构造函数注入来注入的,那么您不妨将其作为属性公开。如果实现类实现了创建/转换状态的行为,您可能希望将行为公开为方法。这一切都取决于实际情况,这里没有金子弹。请记住,在 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();
...
}
}
这里的一个重要部分是单元测试。如果你像这样设计你的类,你可以很容易地创建用于测试的假货。即使不涉及任何测试(我不推荐),像这样解耦类的能力也很好地表明了设计良好的代码库。