In a effort to maintain the testability of my code, I’ve started exposing my application settings from an abstraction layer. The settings are then passed into consumer object (classes) as needed using interface injection (aka dependency injection).
Consider the fictitious example below. The DiscountCalculator class contains the function FinalOrderAmount. This function returns total cost of an order based on the number of order items. If the number of items in the order exceeds a specific number, a discount is applied and the discounted value is returned, otherwise the original value is returned. The number of items that trigger the discount and the amount to discount the cost by, are held in an object that implements IContainDiscountSettings.
1: public interface IContainDiscountSettings
2: {
3: int Discount { get; set; }
4: int NumberOfItemsToTriggerDiscount { get; set; }
5: }
6:
7: public class DiscountCalculator
8: {
9: private IContainDiscountSettings _settings;
10:
11: public DiscountCalculator(IContainDiscountSettings settings)
12: {
13: _settings = settings;
14: }
15:
16: public double FinalOrderAmount(int itemCount, double cost)
17: {
18: if (itemCount >= _settings.NumberOfItemsToTriggerDiscount)
19: {
20: return (cost / 100) * (100 - _settings.Discount);
21: }
22: return cost;
23: }
24: }
By injecting an instance of IContainDiscountSettings into the constructor, the DiscountCalculator class becomes loosely coupled to the IContainDiscountSettings instance, enabling it to be unit tested in isolation. The test below checks that the FinalOrderAmount function applies a discount when the number of items reaches equals the trigger value held in the settings object. In this instance, we’ve injected a test stub implementing IContainDiscountSettings. Using a stub in this way, allows us to set different values for each test case written.
1: [TestFixture]
2: public class DiscountCalculatorTest
3: {
4:
5: [Test]
6: public void DiscountAppliedWhenTriggerAmountMet()
7: {
8:
9: // Arrange
10: const int triggerItemCount = 5;
11: const double itemCost = 30.00;
12:
13: IContainDiscountSettings settings = new DiscountSettingsStub()
14: {
15: Discount = 15,
16: NumberOfItemsToTriggerDiscount = triggerItemCount
17: };
18:
19: var subject = new DiscountCalculator(settings);
20:
21: // Act
22: double orderAmount = subject.FinalOrderAmount(triggerItemCount, itemCost);
23:
24: // Assert
25: Assert.Less(orderAmount, itemCost,
26: "Order cost should be less than cost of all items due to dicount being triggered");
27:
28: }
29:
30: }
31:
32: internal class DiscountSettingsStub : IContainDiscountSettings
33: {
34:
35: #region IContainDiscountSettings Members
36:
37: public int Discount
38: {
39: get;
40: set;
41: }
42:
43: public int NumberOfItemsToTriggerDiscount
44: {
45: get;
46: set;
47: }
48:
49: #endregion
50: }
Deriving from the ConfigurationSection
OK, so I’ve finally heeded the calls of the cool kids on twitter and started to utilise custom configuration sections in the app.config file. I don’t necessarily feel like one of the cool kids but feel I’ve got one less thing for me to get brow beaten over!
Here’s how we can an hold our DiscountSettings values in our app.config file without using the “filthy” appSettings element.
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <configSections>
4: <section name="DiscountSettings" type="MyWorkingApp.DiscountSettings,MyWorkingApp"/>
5: </configSections>
6: <DiscountSettings CustomerDiscount="25" TriggerDiscountItems="20" />
7: </configuration>
As you can see, I’ve added a section element into the configSections element to declare the config section, then added the DiscountSettings element to hold all my values as attributes. I must admit, it does make things a lot more manageable than tossing everything in the appSettings section.
We can read the values from our app using the class below…
1: public class DiscountSettings : ConfigurationSection
2: {
3:
4: [ConfigurationProperty("CustomerDiscount",IsRequired=true)]
5: public int Discount
6: {
7: get
8: {
9: return (int)this["CustomerDiscount"];
10: }
11:
12: set
13: {
14: this["CustomerDiscount"] = value;
15: }
16: }
17:
18: [ConfigurationProperty("TriggerDiscountItems",IsRequired=true)]
19: public int NumberOfItemsToTriggerDiscount
20: {
21: get
22: {
23: return (int)this["TriggerDiscountItems"];
24: }
25:
26: set
27: {
28: this["TriggerDiscountItems"] = value;
29: }
30: }
31: }
… which derives from the ConfigurationSection class of the .NET stack and although, there is a little more ceremony than reading in from the appSettings section, there are some benefits: Decorating each required property or the class with the “IsRequired=True” property on the ConfigurationProperty attribute, ensures that an exception is thrown if the setting is missing in the config file as soon as the class is initialised, rather than than when you try to retrieve the value and use it. You can also set default values if you don’t want to specify the value in the config file but that seems a bit dirty to me.
We can declare an instance of our DiscountSettings object as so…
var settings = ConfigurationManager.GetSection("DiscountSettings")as DiscountSettings;
…and by configuring it to implement IContainDiscountSettings, an instance of it can be injected into our DiscountCalculator as needed:
public class DiscountSettings : ConfigurationSection, IContainDiscountSettings
Wrap Up
Using the Interface Injection method described above, we can easily switch our application settings management solution as the need arises. By writing a class that implements the necessary settings management interface, we could easily read our settings from a database, registry or even a network stream. It also enables us to effectively test consumer object in isolation.
Any feedback would be welcome if there’s a better way of achieving this :-)
Cool, just been doing exactly the same thing, but using the Configuration Section Designer to build the config section (complete with intellisense support via a schema).
ReplyDeletehttp://csd.codeplex.com/