最常見的依賴注入(Dependency Injection)方式,就是從建構式上面,將依賴的服務一一注入,但是實務上多多少少會有一部分的 Instances,在服務被釋放之前都沒有被用到,雖然一般來說,產生 Instance 的成本不大,不過我還是想試一下,能不能將依賴注入這件事移到執行的目標方法裡面,在方法裡面有用到的服務才注入,所以就有了「延遲依賴注入(Lazy Dependency Injection)
」這個題目。
我的實驗環境是這樣的,假定我有一個 MyService
的類別,實作 IMyService
這個介面,介面有定義一個 GetMyData()
方法,而 MyData
則是依賴 IMyDataAccess
取得,IMyDataAccess 則是有一個 MyDataAccess
的實作。
public interface IMyService
{
MyData GetMyData();
}
public interface IMyDataAccess
{
MyData QuerySingle(int id);
}
public class MyService : IMyService
{
private readonly IMyDataAccess myDataAccess;
public MyService(IMyDataAccess myDataAccess)
{
this.myDataAccess = myDataAccess;
}
public MyData GetMyData()
{
return this.myDataAccess.QuerySingle(1);
}
}
public class MyDataAccess : IMyDataAccess
{
public MyData QuerySingle(int id)
{
return new MyData { Id = id, Name = "Johnny" };
}
}
public class MyData
{
public int Id { get; set; }
public string Name { get; set; }
}
一般從建構式注入服務,就差不多像上面這個樣子,底下我就開始著手把它改成延遲在方法裡面注入。
AOP(Aspect-Oriented Programming)
我想到的方式就是用 AOP
去做,我這邊使用的是 AspectInjector 這套 AOP 框架,建立一個 Aspect,在每一個方法執行之前,把方法裡面需要的服務一一注入,為此我需要一個 DependencyInjectionAspectAttribute
來實作注入的邏輯,還有 DependencyInjectAttribute
來取得方法裡面需要注入哪些服務?以及一個簡易的 ServiceCollection
,也避免產生重覆的 Instance。
[Aspect(Scope.PerInstance)]
[Injection(typeof(DependencyInjectionAspectAttribute))]
public class DependencyInjectionAspectAttribute : Attribute
{
[Advice(Kind.Before, Targets = Target.Method)]
public void Before([Argument(Source.Metadata)] MethodBase method, [Argument(Source.Instance)] object instance)
{
var privateFields = instance.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
var serviceCollection = privateFields.Single(fi => fi.FieldType == typeof(ServiceCollection)).GetValue(instance) as ServiceCollection;
foreach (var type in method.GetCustomAttribute<DependencyInjectAttribute>().Types)
{
var field = privateFields.Single(fi => fi.FieldType == type);
var implType = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.First(p => type.IsAssignableFrom(p) && !p.IsInterface);
var implInstance = Activator.CreateInstance(implType);
serviceCollection.Add(type, implInstance);
field.SetValue(instance, implInstance);
}
}
}
public class DependencyInjectAttribute : Attribute
{
public DependencyInjectAttribute(params Type[] types)
{
this.Types = types;
}
public Type[] Types { get; }
}
public class ServiceCollection
{
private readonly ConcurrentDictionary<Type, object> services;
public ServiceCollection()
{
this.services = new ConcurrentDictionary<Type, object>();
}
public void Add(Type type, object instance)
{
this.services.TryAdd(type, instance);
}
public object Get(Type type)
{
return this.services.TryGetValue(type, out var instance) ? instance : default;
}
}
因為只是 POC(Proof of concept),所以暫不考量太多細節,產生 Instance 的過程也是用最簡便的 Reflection 來做,再來就是把做好的 Attribute 放到目標方法上就大功告成了。
public class MyService : IMyService
{
private readonly ServiceCollection serviceCollection = new ServiceCollection();
private IMyDataAccess myDataAccess;
[DependencyInjectionAspect]
[DependencyInject(typeof(IMyDataAccess))]
public MyData GetMyData()
{
return this.myDataAccess.QuerySingle(1);
}
}
後續如果需要增加依賴的服務,也只要把服務實作好,加到 DependencyInjectAttribute 上就可以了。
public class MyService : IMyService
{
private readonly ServiceCollection serviceCollection = new ServiceCollection();
private IMyDataAccess myDataAccess;
private IMyIdDataAccess myIdDataAccess;
[DependencyInjectionAspect]
[DependencyInject(typeof(IMyDataAccess), typeof(IMyIdDataAccess))]
public MyData GetMyData()
{
var myId = this.myIdDataAccess.GetMyId();
return this.myDataAccess.QuerySingle(myId);
}
}
引入 DI Framework
如果我們有用 DI Framework,可以把 ServiceCollection 用 DI Framework 產生的 Container 取代掉,這邊我用 Autofac 來做,並且建立 LifetimeScope,避免重覆產生 Instance。
public class MyService : IMyService
{
private readonly ILifetimeScope lifetimeScope = AutofacConfig.Container.BeginLifetimeScope();
private IMyDataAccess myDataAccess;
private IMyIdDataAccess myIdDataAccess;
...
}
[Aspect(Scope.PerInstance)]
[Injection(typeof(DependencyInjectionAspectAttribute))]
public class DependencyInjectionAspectAttribute : Attribute
{
[Advice(Kind.Before, Targets = Target.Method)]
public void Before([Argument(Source.Metadata)] MethodBase method, [Argument(Source.Instance)] object instance)
{
var privateFields = instance.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
var lifetimeScope = privateFields.Single(fi => fi.FieldType == typeof(ILifetimeScope)).GetValue(instance) as ILifetimeScope;
foreach (var type in method.GetCustomAttribute<DependencyInjectAttribute>().Types)
{
var field = privateFields.Single(fi => fi.FieldType == type);
field.SetValue(instance, lifetimeScope.Resolve(type));
}
}
}
public class AutofacConfig
{
public static IContainer Container { get; private set; }
public static void Register()
{
var builder = new ContainerBuilder();
builder.RegisterType<MyDataAccess>().As<IMyDataAccess>().InstancePerLifetimeScope();
builder.RegisterType<MyIdDataAccess>().As<IMyIdDataAccess>().InstancePerLifetimeScope();
builder.RegisterType<MyService>().As<IMyService>();
Container = builder.Build();
}
}
POC 就先到這邊,看起來延遲依賴注入(Lazy Dependency Injection)是可行的,後續還有很多可以調整的部分,像是不要用 Reflection 來存取私有欄位,改用 IL Code 來做。