With all the talk on weblogs or technical conversations within my own organization about DI it's difficult to ignore it as little more than the latest "new black" pattern. I've given it some considerable thought and until quite recently didn't really comprehend the overall niftiness of the DI approach. As with anything else it took a moment of "a-HA" to really grasp the power of DI; I was developing a custom CruiseControl.Net build plugin and realized that the plugins are injected dynamically at construction time. If you debug one of these plugins, you'll notice that the plugin constructors don't match a common parameter structure. The one commonality throughout all the plugins I was investigating as examples for my own education seemed to be in the parameter types - they were all interfaces, implementations of which were usually stored in the CCNet server application domain.
Services, basically. My interest in service orientation perked my interest in DI; the two concepts appeared mutually beneficial. So I opened the laptop and started coding away on my own implementation of a DI framework, with tests (since it's virtually impossible to talk about DI without talking about tests, too). This blog post consists of an investigation into my own implementation. It doesn't offer up DI as a "holier-than-thou" approach nor a dismissable coding trend. Rather, it is my first attempt to prove to myself that I am getting this DI stuff and to implement it in my own words. *cracks knuckles*
First Things First - Support Laziness
As with any framework, I anticipate that the easier it is to use the more chance I'll have of talking someone else into giving this a shot or a glance. So I knew that I wanted my approach to be very simple to use, quick to implement, and hopefully, make the approach of using DI more interesting. My first question to every pattern is "does it make my coding process easier and more flexible?" I know that:
- I'm going to implement DI here, so my dependent classes will "just pop up" because the "stuff" they need to "live" will be provided to them by some service layer.
- So I know I will need to implement ServiceContainer in some fashion, and the services I put into it will implement interfaces, since the Service approach implies the use of interfaces to define contracts.
My discomfort with the ServiceContainer approach is that, someone actually has to "Add" the service implementations to the service container. Hence, they have to:
- Know how to do that.
- Do it the way you expect they're going to do it .
- Write their code in such a way that they account for my service container implementation.
That point, most times, is where I get pretty aggravated when I'm using someone else's framework. That is a requirement that I must not impose on my audience, but one that I can't get started without. So a compromise is in order, and I'll use the idea of simple metadata to notify me of interfaces the developer intends need to be added to the service layer at run-time.
1 /// <summary>
2 /// Defines an interface as one that should be created and hosted by the
3 /// service host during application run-time.
4 /// </summary>
5 [AttributeUsage(AttributeTargets.Interface, AllowMultiple=false)]
6 public class DependencyServiceInterfaceAttribute : Attribute
7 {
8 }
Using the DSI attribute I can mark any interface that I intend to be added to a service container instance within the application domain. This makes it really easy to use, as the code below indicates.
76 [DependencyServiceInterface]
77 public interface IMockServiceA
78 {
79 void DoMockServiceWork();
80 }
81
82 [DependencyServiceInterface]
83 public interface IMockServiceB
84 {
85 void DoMoreWork();
86 }
87
88 public class MockServiceA : IMockServiceA
89 {
90 #region IMockService Members
91
92 public void DoMockServiceWork()
93 {
94 System.Diagnostics.Debug.WriteLine("Doing Mock Service A's Job");
95 }
96
97 #endregion
98 }
99
100 public class MockServiceB : IMockServiceB
101 {
102 #region IMockServiceB Members
103
104 public void DoMoreWork()
105 {
106 System.Diagnostics.Debug.WriteLine("Doing Mock Service B's Job");
107 }
108
109 #endregion
110 }
I create a new interfaces and mark them with the DSI attribute. Then, I implement each interface with a custom class containing some basic functionality. Notice that I don't have to do anything to the classes themselves; the DI layer we'll begin to investigate next will do that work for us.
Comprehensive - Sure, Why Not?!
If you can see where I'm going with this you're most likely scratching your head and saying "no way, not everything..." Looking through each assembly for implementations of interfaces marked with metadata isn't the most performant approach to doing type-loading but for now it will serve it's purpose. Think of it this way - I'm making exhaustively sure I'm not going to miss any service implementation that I might need later by a dependent class. If I suspect that any interface I've got implementations of will ever be needed by any dependent class that might be required later, I can just slap the DSI attribute onto the interface and off we go.
Just to take a quick look at the code that'd perform this exhaustive search it's right here. The DependencyServiceContainer does just what you suspected - it looks through everything in the application domain to find any implementations of any interfaces that have been marked with the DSI attribute. Whenever it finds such an implementation, an instance of it is created and added via the base method ServiceContainer.AddService.
8 public class DependencyServiceContainer : ServiceContainer
9 {
10 public static DependencyServiceContainer Instance
11 {
12 get { return _instance; }
13 }
14
15 static DependencyServiceContainer _instance;
16 static DependencyServiceContainer()
17 {
18 _instance = new DependencyServiceContainer();
19 }
20
21 internal DependencyServiceContainer()
22 {
23 Preload();
24 }
25
26 private int _svcCount;
27 public int ServiceCount
28 {
29 get { return _svcCount; }
30 }
31
32 void Preload()
33 {
34 foreach (Assembly assm in AppDomain.CurrentDomain.GetAssemblies())
35 {
36 SearchAssemblyForDSIAttributes(assm);
37 }
38 }
39
40 void SearchAssemblyForDSIAttributes(Assembly assm)
41 {
42 foreach (Type tp in assm.GetTypes())
43 {
44 if (!tp.IsInterface)
45 {
46 SearchTypeForInterfaces(tp);
47 }
48 }
49 }
50
51 void SearchTypeForInterfaces(Type t)
52 {
53 foreach (Type intrfc in t.GetInterfaces())
54 {
55 if (IsInterfaceDSI(intrfc))
56 {
57 AddService(
58 intrfc,
59 Activator.CreateInstance(t)
60 );
61
62 _svcCount++;
63 }
64 }
65 }
66
67 internal static bool IsInterfaceDependencyInjectable(Type intrfc)
68 {
69 return (
70 (intrfc.GetCustomAttributes(typeof(DependencyServiceInterfaceAttribute), false).Length > 0)
71 &&
72 (intrfc.IsInterface)
73 );
74 }
75
76 bool IsInterfaceDSI(Type intrfc)
77 {
78 return DependencyServiceContainer.IsInterfaceDependencyInjectable(intrfc);
79 }
80 }
Activation of Dependent Objects - the Point
Now that the DI framework has a service container into which services required by dependent classes have been added the logic to create dependent objects must be created. Basically, we're going to use reflection to inspect dependent classes. During reflection each constructor signature will be observed. When a constructor is found that has parameters of interface types that are all being held within the DependencyServiceContainer, the constructor will be called and the resulting object returned.
8 public class DependentClassActivator
9 {
10 static DependentClassActivator _instance;
11 static DependentClassActivator()
12 {
13 _instance = new DependentClassActivator();
14 }
15
16 public static DependentClassActivator Instance
17 {
18 get { return _instance; }
19 }
20
21 public T CreateInstance<T>() where T : class
22 {
23 Type tp = typeof(T);
24
25 foreach (ConstructorInfo ctor in tp.GetConstructors())
26 {
27 if (Observe(ctor))
28 {
29 return InvokeConstructor<T>(ctor);
30 }
31 }
32
33 return null;
34 }
35
36 #region Private Helper Methods
37
38 bool Observe(ConstructorInfo ctor)
39 {
40 foreach (ParameterInfo prm in ctor.GetParameters())
41 {
42 if (!Observe(prm)) return false;
43 }
44
45 return true;
46 }
47
48 bool Observe(ParameterInfo prm)
49 {
50 return (
51 DependencyServiceContainer.IsInterfaceDependencyInjectable(prm.ParameterType) &&
52 GetTypeFromServiceContainer(prm.ParameterType)
53 );
54 }
55
56 bool GetTypeFromServiceContainer(Type intrfc)
57 {
58 return (DependencyServiceContainer.Instance.GetService(intrfc) != null);
59 }
60
61 T InvokeConstructor<T>(ConstructorInfo ctor) where T : class
62 {
63 object[] prms = GetConstructorParametersFromServiceContainer(ctor);
64 return ctor.Invoke(prms) as T;
65 }
66
67 object[] GetConstructorParametersFromServiceContainer(ConstructorInfo ctor)
68 {
69 List<object> prms = new List<object>();
70
71 foreach (ParameterInfo prm in ctor.GetParameters())
72 {
73 prms.Add(DependencyServiceContainer.Instance.GetService(prm.ParameterType));
74 }
75
76 return prms.ToArray();
77 }
78
79 #endregion
80 }
Here's the basic run-down. The DependentClassActivator creates looks at all of a class's constructors. Specifically, at each constructor's parameter. When it finds a constructor that has parameters it sees in the DependencyServiceContainer, it calls that constructor. So you don't have to use your class's constructors any longer. Instead, you tell the new DependentClassActivator to give you an instance, instead. Something like this:
27 MockServiceA a =
DependentClassActivator.Instance.CreateInstance<MockServiceA>();
To use a metaphor, it's like walking into a kitchen in which everything you ever need to make anything is already there. You need a spatula, you got it, you need a mixer, you got it. And so on.
How Do You Test It?
Possibly the most important aspect of all this is the ability to test it. In fact, code written using this DI framework is rather simplistic to test. To explain how you'd use this approach we'll consider the ever-relevant banking scenario. Below you'll see the test code for all the functionality described earlier. You'll see some interfaces that have been marked with the DSI attribute, some implementations to create and use, and tie it all up with a mock banking execution example.
10 #region Bank Service Interfaces and Implementations
11
12 public class Account
13 {
14 private int _accountId;
15 private decimal _bal;
16
17 public decimal Balance
18 {
19 get { return _bal; }
20 set { _bal = value; }
21 }
22
23 public int AccountId
24 {
25 get { return _accountId; }
26 set { _accountId = value; }
27 }
28 }
29
30 [DependencyServiceInterface]
31 public interface IAccountLookupService
32 {
33 Account FindAccount(int accountId);
34 }
35
36 [DependencyServiceInterface]
37 public interface IWithdrawalService
38 {
39 bool Withdraw(Account account, decimal amount);
40 }
41
42 [DependencyServiceInterface]
43 public interface IDepositService
44 {
45 bool Deposit(Account account, decimal amount);
46 }
47
48 public class AccountLookup : IAccountLookupService
49 {
50 #region IAccountLookupService Members
51
52 public Account FindAccount(int accountId)
53 {
54 if (accountId != 1234) return null;
55
56 Account mockAccount = new Account();
57 mockAccount.AccountId = 1234;
58 mockAccount.Balance = 100;
59 return mockAccount;
60 }
61
62 #endregion
63 }
64
65 public class AccountWithdrawer : IWithdrawalService
66 {
67 #region IWithdrawalService Members
68
69 public bool Withdraw(Account account, decimal amount)
70 {
71 if (amount > account.Balance) return false;
72 account.Balance -= amount;
73 return true;
74 }
75
76 #endregion
77 }
78
79 public class AccountDepositer : IDepositService
80 {
81 #region IDepositService Members
82
83 public bool Deposit(Account account, decimal amount)
84 {
85 account.Balance += amount;
86 return true;
87 }
88
89 #endregion
90 }
91
92 public class Bank
93 {
94 IAccountLookupService lookupService;
95 IWithdrawalService withdrawalService;
96 IDepositService depositService;
97
98 public Bank(IAccountLookupService lookupService,
99 IWithdrawalService withdrawalService,
100 IDepositService depositService)
101 {
102 this.lookupService = lookupService;
103 this.withdrawalService = withdrawalService;
104 this.depositService = depositService;
105 }
106
107 public Account GetAccount(int id)
108 {
109 return this.lookupService.FindAccount(id);
110 }
111
112 public bool Withdraw(Account account, decimal amount)
113 {
114 return this.withdrawalService.Withdraw(account, amount);
115 }
116
117 public bool Deposit(Account account, decimal amount)
118 {
119 return this.depositService.Deposit(account, amount);
120 }
121 }
122
123 #endregion
124
125 #region Tests
126
127 [TestFixture]
128 public class BankAccountTests
129 {
130 [SetUp]
131 public void Setup()
132 {
133 }
134
135 [TearDown]
136 public void TearDown()
137 {
138 }
139
140 [Test]
141 public void CanBankClassBeCreated()
142 {
143 Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();
144 Assert.IsNotNull(bank);
145 }
146
147 [Test]
148 public void CanBankHandBackAccount()
149 {
150 Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();
151 Account account = bank.GetAccount(1234);
152 Assert.IsNotNull(account );
153 account = bank.GetAccount(1000);
154 Assert.IsNull(account );
155 }
156
157 [Test]
158 public void CanBankAccountWithdrawMoney()
159 {
160 Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();
161 Account account = bank.GetAccount(1234);
162 Assert.IsNotNull(account);
163 decimal bal = account.Balance;
164 decimal amt = 42;
165
166 bool result = bank.Withdraw(account , amt);
167 Assert.IsTrue(result);
168 Assert.AreEqual((bal - amt), account .Balance);
169
170 result = bank.Withdraw(account , 99999999);
171 Assert.IsFalse(result);
172 }
173
174 [Test]
175 public void CanBankAccountDepositMoney()
176 {
177 Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();
178 Account account = bank.GetAccount(1234);
179 Assert.IsNotNull(account );
180 decimal bal = account .Balance;
181 decimal amt = 42;
182
183 bool result = bank.Deposit(account , amt);
184 Assert.IsTrue(result);
185 Assert.AreEqual((bal + amt), account .Balance);
186 }
187 }
188
189 #endregion
I welcome any comments on this approach. Is this DI or have I completely missed the boat on this whole concept. Hopefully this look at one appraoch DI has been as enlightening as it has been for me. Happy coding!