在使用依赖注入的过程当中,除了应用设计模式注意代码的变化隔离之外,另外一个重要的内容就是生命周期控制。
每次获取都是新的实例
前文中用到的方式都是这样的效果。在容器中每次获取同一个接口的实现,每次获取到的都是不同的实例。读者可以翻阅一下先前的示例代码回顾一下。
单例模式
单例模式也是一种常见的设计模式,这种设计模式。主要是为了解决某些特定需求时不希望特定的实例过多,而采用单个实例的设计模式。
在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 { 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 { 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 { decimal GetBalance(string id);
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
去除,观察运行效果的不同。
使用相同的数据库事务
转账从现有的代码结构而言,需要开启数据库事务才能够确保在数据入库时是无误的。从三层结构的角度来说,通常需要调用多个具有修改数据库数据功能的DAL方法时,将会开启事务从而确保这些DAL方法的执行是正确的。
为了实现这个特性,首先准备一些基础的类。
using System; using System.Data;
namespace Use_Dependency_Injection_With_Lifetime_Scope_Control { public interface IExecuteSqlDbConnection : IDbConnection { void ExecuteSql(string sql, object[] ps, IDbTransaction dbTransaction = null); }
public class ConsoleDbConnection : IExecuteSqlDbConnection { public delegate ConsoleDbConnection Factory();
public void Dispose() { Console.WriteLine("数据库连接:释放"); }
public IDbTransaction BeginTransaction() { return new ConsoleOutDbTransaction(this, IsolationLevel.Unspecified); }
public IDbTransaction BeginTransaction(IsolationLevel il) { return new ConsoleOutDbTransaction(this, il); }
public void Close() { Console.WriteLine("数据库连接:关闭"); }
public void ChangeDatabase(string databaseName) { throw new NotSupportedException(); }
public IDbCommand CreateCommand() { throw new NotSupportedException(); }
public void Open() { throw new NotSupportedException(); }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get { throw new NotSupportedException(); } }
public string Database { get { throw new NotSupportedException(); } }
public ConnectionState State { get { throw new NotSupportedException(); } }
public void ExecuteSql(string sql, object[] ps, IDbTransaction dbTransaction = null) { if (dbTransaction == null) { Console.WriteLine($"无事务执行:{string.Format(sql, ps)}"); } else { Console.WriteLine($"有事务执行:{string.Format(sql, ps)}"); } } }
public class ConsoleOutDbTransaction : IDbTransaction { public ConsoleOutDbTransaction(IDbConnection connection, IsolationLevel isolationLevel) { Connection = connection; IsolationLevel = isolationLevel; }
public void Dispose() { Console.WriteLine("事务:释放"); }
public void Commit() { Console.WriteLine("事务:提交"); }
public void Rollback() { Console.WriteLine("事务:回滚"); }
public IDbConnection Connection { get; } public IsolationLevel IsolationLevel { get; } } }
|
具备了数据库链接和事务的基础类后,假设我们不采用生命周期控制的方案。那么一种实现方案如下
using Autofac; using System; using System.Collections.Generic; using System.Data;
namespace Use_Dependency_Injection_With_Lifetime_Scope_Control { public static class Demo4 { public static void Run() { var cb = new ContainerBuilder(); cb.RegisterType<AccountBll>().As<IAccountBll>(); cb.RegisterType<AccountDal>().As<IAccountDal>(); cb.RegisterType<DbFactory>().As<IDbFactory>(); cb.RegisterType<ConsoleDbConnection>().AsSelf(); var container = cb.Build();
using (var beginLifetimeScope = container.BeginLifetimeScope()) { var accountBll = beginLifetimeScope.Resolve<IAccountBll>(); accountBll.Transfer("yueluo", "newbe", 333); } }
public interface IDbFactory { IExecuteSqlDbConnection CreateDbConnection(); }
public class DbFactory : IDbFactory { private readonly ConsoleDbConnection.Factory _factory;
public DbFactory( ConsoleDbConnection.Factory factory) { this._factory = factory; }
public IExecuteSqlDbConnection CreateDbConnection() { return _factory(); } }
public interface IAccountBll { void Transfer(string fromAccountId, string toAccountId, decimal amount); }
public class AccountBll : IAccountBll { private readonly IDbFactory _dbFactory; private readonly IAccountDal _accountDal;
public AccountBll( IDbFactory dbFactory, IAccountDal accountDal) { _dbFactory = dbFactory; _accountDal = accountDal; }
public void Transfer(string fromAccountId, string toAccountId, decimal amount) { using (var dbConnection = _dbFactory.CreateDbConnection()) { using (var transaction = dbConnection.BeginTransaction()) { try { var fromAmount = _accountDal.GetBalance(fromAccountId); var toAmount = _accountDal.GetBalance(toAccountId); fromAmount -= amount; toAmount += amount; _accountDal.UpdateBalance(fromAccountId, fromAmount, dbConnection, transaction); _accountDal.UpdateBalance(toAccountId, toAmount, dbConnection, transaction); transaction.Commit(); } catch (Exception) { transaction.Rollback(); throw; } } } } }
public interface IAccountDal { decimal GetBalance(string id);
void UpdateBalance(string id, decimal balance, IExecuteSqlDbConnection dbConnection = null, IDbTransaction dbTransaction = null); }
public class AccountDal : IAccountDal { private readonly IDbFactory _dbFactory;
public AccountDal( IDbFactory dbFactory) { _dbFactory = dbFactory; }
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, IExecuteSqlDbConnection dbConnection = null, IDbTransaction dbTransaction = null) { if (dbConnection == null) { dbConnection = _dbFactory.CreateDbConnection(); dbConnection.ExecuteSql("更新语句:更新 {0} 余额为 {1}", new object[] { id, balance }); _accounts[id] = balance;
} else { dbConnection.ExecuteSql("更新语句:更新 {0} 余额为 {1}", new object[] { id, balance }, dbTransaction); _accounts[id] = balance; }
} } } }
|
简要分析,上例代码中关键点:
IAccountDal.UpdateBalance
支持传入数据库链接和事务对象,这样在IAccountBll
既可以开启事务确保方法在一个事务内执行,也可以不开启事务,进行分事务执行。
这样做的缺点也比较明显。DAL层实现比较麻烦。
假如参照上文中“日志”的处理方案,将数据库链接和事务作为生命周期内单例来控制,实现起来将更加方便。
using Autofac; using System; using System.Collections.Generic; using System.Data;
namespace Use_Dependency_Injection_With_Lifetime_Scope_Control { public static class Demo5 { public static void Run() { var cb = new ContainerBuilder(); cb.RegisterType<AccountBll>().As<IAccountBll>(); cb.RegisterType<AccountDal>().As<IAccountDal>(); cb.RegisterType<DbFactory>().As<IDbFactory>() .InstancePerLifetimeScope(); cb.RegisterType<ConsoleDbConnection>().AsSelf(); var container = cb.Build();
using (var beginLifetimeScope = container.BeginLifetimeScope()) { var accountBll = beginLifetimeScope.Resolve<IAccountBll>(); accountBll.Transfer("yueluo", "newbe", 333); } }
public interface IDbFactory { IExecuteSqlDbConnection CreateDbConnection(); }
public class DbFactory : IDbFactory { private readonly ConsoleDbConnection.Factory _factory;
public DbFactory( ConsoleDbConnection.Factory factory) { this._factory = factory; }
private IExecuteSqlDbConnection _connection; public IExecuteSqlDbConnection CreateDbConnection() { return _connection ?? (_connection = new TransactionOnceDbConnection(_factory())); } }
public class TransactionOnceDbConnection : IExecuteSqlDbConnection { private readonly IExecuteSqlDbConnection _innerConnection; private IDbTransaction _innerDbTransaction; public TransactionOnceDbConnection( IExecuteSqlDbConnection innerConnection) { _innerConnection = innerConnection; }
public void Dispose() { _innerConnection.Dispose(); }
public IDbTransaction BeginTransaction() { if (_innerDbTransaction != null) { return _innerDbTransaction; } return _innerDbTransaction = _innerConnection.BeginTransaction(); }
public IDbTransaction BeginTransaction(IsolationLevel il) { if (_innerDbTransaction != null) { return _innerDbTransaction; } return _innerDbTransaction = _innerConnection.BeginTransaction(il); }
public void Close() { _innerConnection.Close(); }
public void ChangeDatabase(string databaseName) { _innerConnection.ChangeDatabase(databaseName); }
public IDbCommand CreateCommand() { return _innerConnection.CreateCommand(); }
public void Open() { _innerConnection.Open(); }
public string ConnectionString { get => _innerConnection.ConnectionString; set => _innerConnection.ConnectionString = value; }
public int ConnectionTimeout => _innerConnection.ConnectionTimeout;
public string Database => _innerConnection.Database;
public ConnectionState State => _innerConnection.State; public void ExecuteSql(string sql, object[] ps, IDbTransaction dbTransaction = null) { _innerConnection.ExecuteSql(sql, ps, _innerDbTransaction ?? dbTransaction); } }
public interface IAccountBll { void Transfer(string fromAccountId, string toAccountId, decimal amount); }
public class AccountBll : IAccountBll { private readonly IDbFactory _dbFactory; private readonly IAccountDal _accountDal;
public AccountBll( IDbFactory dbFactory, IAccountDal accountDal) { _dbFactory = dbFactory; _accountDal = accountDal; }
public void Transfer(string fromAccountId, string toAccountId, decimal amount) { using (var dbConnection = _dbFactory.CreateDbConnection()) { using (var transaction = dbConnection.BeginTransaction()) { try { var fromAmount = _accountDal.GetBalance(fromAccountId); var toAmount = _accountDal.GetBalance(toAccountId); fromAmount -= amount; toAmount += amount; _accountDal.UpdateBalance(fromAccountId, fromAmount); _accountDal.UpdateBalance(toAccountId, toAmount); transaction.Commit(); } catch (Exception) { transaction.Rollback(); throw; } } } } }
public interface IAccountDal { decimal GetBalance(string id);
void UpdateBalance(string id, decimal balance); }
public class AccountDal : IAccountDal { private readonly IDbFactory _dbFactory;
public AccountDal( IDbFactory dbFactory) { this._dbFactory = dbFactory; }
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) { var dbConnection = _dbFactory.CreateDbConnection(); dbConnection.ExecuteSql("更新语句:更新 {0} 余额为 {1}", new object[] { id, balance }); _accounts[id] = balance; } } } }
|
简要分析,上例代码关键点:
- 通过装饰模式实现了
TransactionOnceDbConnection
,支持一次开启事务之后,后续操作都使用相同事务。 - 修改了
DbFactory
,实现一次开启链接之后,就是用相同链接的特性。 - 将
IDbFactory
标记为生命周期内单例。 - 在使用
IAccountBll
时,开启了一个生命周期。
这样改造之后,DAL实现时,就不需要关系事务到底是否开启没有,只需要直接执行相关操作即可。
总结
在使用依赖注入的时候,生命周期控制是一个相当重要的课题。读者需要在实践中注意分析。
以上示例代码都是基于较为简单的业务场景与基础代码实现,实际操作中不一定是如此,读者需要在实践中注意分析。
本文由于采用了Autofac作为主要的依赖注入框架,因此生命周期控制方式也采用了框架相关的函数。实际上,绝大多数框都提供了以上提及的生命周期控制方式。在实践中,读者可以找寻相关框架的文档,了解如何应用框架进行生命周期控制。
关于Autofac更加深入的生命周期控制:参考链接。
至此,该系列文章也已完结,希望读者能够从中获益。
本文示例代码地址
教程链接