生命周期控制
在使用依赖注入的过程当中,除了应用设计模式注意代码的变化隔离之外,另外一个重要的内容就是生命周期控制。
每次获取都是新的实例
前文中用到的方式都是这样的效果。在容器中每次获取同一个接口的实现,每次获取到的都是不同的实例。读者可以翻阅一下先前的示例代码回顾一下。
单例模式
单例模式也是一种常见的设计模式,这种设计模式。主要是为了解决某些特定需求时不希望特定的实例过多,而采用单个实例的设计模式。
在C#之中,最为容易理解的一种单例模式的应用便是静态成员,这点显而易见,以下获取系统时间的代码。便是一种单例模式。
using System;
using System.Threading;
namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo1
{
public static void Run()
{
Console.WriteLine($"第一次获取时间:{DateTime.Now}");
Thread.Sleep(1000);
Console.WriteLine($"第二次获取时间:{DateTime.Now}");
Thread.Sleep(1000);
Console.WriteLine($"第三次获取时间:{DateTime.Now}");
}
}
}
每隔一秒钟获取一次系统时间。DateTime.Now是DateTime类型提供的静态属性。在C#语言之中这可以被看做一种单例模式。
但是,存在一个问题,那就是单元测试的可行性。简单来说,这段代码的运行结果会随着时间的变化而变化,每次运行的结果都不相同,这样通常来说是不可测的。因此,应用依赖注入进行一下改造。
using Autofac;
using System;
using System.Threading;
namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo2
{
public static void Run()
{
var cb = new ContainerBuilder();
cb.RegisterType<StaticClockByOneTime>()
.As<IClock>()
.SingleInstance();
var container = cb.Build();
var clock = container.Resolve<IClock>();
Console.WriteLine($"第一次获取时间:{clock.Now}");
Thread.Sleep(1000);
clock = container.Resolve<IClock>();
Console.WriteLine($"第二次获取时间:{clock.Now}");
Thread.Sleep(1000);
clock = container.Resolve<IClock>();
Console.WriteLine($"第三次获取时间:{clock.Now}");
}
public interface IClock
{
/// <summary>
/// 获取当前系统时间
/// </summary>
DateTime Now { get; }
}
public class StaticClockByOneTime : IClock
{
private DateTime _firstTime = DateTime.MinValue;
public DateTime Now
{
get
{
if (_firstTime == DateTime.MinValue)
{
_firstTime = DateTime.Now;
}
return _firstTime;
}
}
}
}
}
简要分析。通过改造之后引入了新的接口获取当前系统时间。由于接口的存在,我们可以替换接口的实现。
此处使用了一个有趣的实现StaticClockByOneTime。简单来说,这个实例如果获取过一次时间之后,时间就不会变化。
为这个特性作支撑的,便是SingleInstance这个方法。此方法将StaticClockByOneTime注册时标记为了“单例”。因此,从容器中获取IClock实例时始终得到的是同一个实例。就这样,便即实现了单例,又实现了可以自主控制时间的需求。
读者可以将上文代码中的
SingleInstance代码去掉来体验单例和非单例运行结果的区别。
生命周期内单例
上文的单例是一种全局性的单例配置。只要容器建立起来,在容器内就是完全单例的。但在实际的应用场景中可能需要在某个特定生命周期内的单例,也可以成为局部单例。
业务需求
以下实例代码都将完成如下定义的一个业务场景:从A账号转账给B账号,转账数额为C,则A账号减少数额C,B账号增加数额C。
有关联的输出日志
转账影响了两个账号余额,现在考虑输出两条余额更新的日志,并且在日志中需要包含相同的转账流水号。
using Autofac;
using System;
using System.Collections.Generic;
namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo3
{
public static void Run()
{
var cb = new ContainerBuilder();
cb.RegisterType<AccountBll>().As<IAccountBll>();
cb.RegisterType<AccountDal>().As<IAccountDal>();
cb.RegisterType<ConsoleLogger>().As<ILogger>()
.InstancePerLifetimeScope();
var container = cb.Build();
using (var beginLifetimeScope = container.BeginLifetimeScope())
{
var accountBll = beginLifetimeScope.Resolve<IAccountBll>();
accountBll.Transfer("yueluo", "newbe", 333);
accountBll.Transfer("yueluo", "newbe", 333);
}
}
public interface ILogger
{
void BeginScope(string scopeTag);
void Log(string message);
}
public class ConsoleLogger : ILogger
{
private string _currenctScopeTag;
public void BeginScope(string scopeTag)
{
_currenctScopeTag = scopeTag;
}
public void Log(string message)
{
Console.WriteLine(string.IsNullOrEmpty(_currenctScopeTag)
? $"输出日志:{message}"
: $"输出日志:{message}[scope:{_currenctScopeTag}]");
}
}
public interface IAccountBll
{
/// <summary>
/// 转账
/// </summary>
/// <param name="fromAccountId">来源账号Id</param>
/// <param name="toAccountId">目标账号Id</param>
/// <param name="amount">转账数额</param>
void Transfer(string fromAccountId, string toAccountId, decimal amount);
}
public class AccountBll : IAccountBll
{
private readonly ILogger _logger;
private readonly IAccountDal _accountDal;
public AccountBll(
ILogger logger,
IAccountDal accountDal)
{
_logger = logger;
_accountDal = accountDal;
}
public void Transfer(string fromAccountId, string toAccountId, decimal amount)
{
_logger.BeginScope(Guid.NewGuid().ToString());
var fromAmount = _accountDal.GetBalance(fromAccountId);
var toAmount = _accountDal.GetBalance(toAccountId);
fromAmount -= amount;
toAmount += amount;
_accountDal.UpdateBalance(fromAccountId, fromAmount);
_accountDal.UpdateBalance(toAccountId, toAmount);
}
}
public interface IAccountDal
{
/// <summary>
/// 获取账 户的余额
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
decimal GetBalance(string id);
/// <summary>
/// 更新账户的余额
/// </summary>
/// <param name="id"></param>
/// <param name="balance"></param>
void UpdateBalance(string id, decimal balance);
}
public class AccountDal : IAccountDal
{
private readonly ILogger _logger;
public AccountDal(
ILogger logger)
{
_logger = logger;
}
private readonly Dictionary<string, decimal> _accounts = new Dictionary<string, decimal>
{
{"newbe",1000},
{"yueluo",666},
};
public decimal GetBalance(string id)
{
return _accounts.TryGetValue(id, out var balance) ? balance : 0;
}
public void UpdateBalance(string id, decimal balance)
{
_logger.Log($"更新了 {id} 的余额为 {balance}");
_accounts[id] = balance;
}
}
}
}
简要分析。以上代码的关键点:
- 在注册
ILogger时,注册为了生命周期内单例。 - 在获取
IAccountBll时,开启了一个生命周期,那么在这个生命周期内获取的ILogger实例都是同一个。 - 在
IAccountBll内使用ILogger记录了转账流水号。
读者可以尝试将InstancePerLifetimeScope去除,观察运行效果的不同。