Mitigating Incomplete Dependencies

09 September 2014

As a consultant, I frequently encounter projects where assumed dependencies are not in place before the beginning of the project. Sometimes, these items might not even be available for months after I am expected to finish and leave. This is especial true of anything related to data (e.g. data services, data access libraries, databases, and sample data). But, it could also be related to non-functional requirements such as authentication and logging frameworks. Despite this, even when estimates were based on these items being ready and the customer knowingly signed off on those assumptions, customers have a hard time understanding the impact to the project schedule and will often argue not to move the target release date. I am not advocating that your team should give in to these unreasonable demands. In fact, I would argue the opposite. However, whether you are waiting on dependencies that should have been completed or had planned to work in parallel from the start, I do recommend architecting your application in such a way as to minimize the damage caused by incomplete dependencies.

Non-Functional Requirements and Dependencies

Non-functional requirements are usually pretty easy to address by defining interfaces and models, creating fake/temporary implementations, and then injecting these dependencies into your application code via a DI framework. Later, you can simply change the configuration in one place (i.e. the DI framework's bootstrapper) and magically you'll be using the real dependency (assuming the dependency actually works).

Consider logging functionality. Your customer or company might not be able to decide between Microsoft's logging, Log4Net, or something home grown. (Yes, these debates do happen in the corporate world, and they do linger on wasting time and money.) But, that doesn't mean that the project has to grind to a halt while they hold up their side of the arrangement. Look at most any logging framework and they all have roughly the same methods and message levels (e.g. trace, debug, info, warning, error, and fatal). The same is true for other non-functional dependencies such as authentication and authorization. Why not define your own interfaces based on the way you want to consume them. Then, give your interfaces to the customer or another team to create the final implementation. Treat these interface as a contract between you and whomever is developing your dependency. Many open source projects such as NancyFx, ServiceStack, and Rhino Service Bus follow this pattern to be flexible in any environment and your project should probably do the same whether you think you need to be flexible or not.

If you can, specify the expectations for your "contract" via tests. However, keep in mind that it may be difficult to do so for non-functional dependencies. Tests for something like logging will almost certainly imply an implementation, and these implementation details should be left to the developer fulfilling that contract. Tests for user management, on the other hand, where there is an external cause and effect (e.g. if I lock an account, that user should not be allowed to authenticate) can and should be explained through tests. The point is to communicate your needs and expectations, and not to be a "control freak." Therefore, if you cannot communicate through tests, be sure to heavily comment your expectations in your "contract" interface. Just be sure to remove these comments after the new dependency is integrated. These types of comments become stale very quickly and may add confusion later in the product's lifetime. If you need to refer back to them, you can always reference the file history in your source control system.

Faking Incomplete Dependencies

While you are waiting on someone else to implement your "contracts," you can create a lightweight implementation that simply wraps your favorite framework or write a fake implementation. The important thing is to make it your own. Do not tie yourself to frameworks that you may not end up using in the end. You will likely end up with an adapter around some common framework. But, this approach means reduced integration time late in the project or backfilling functionality late in the process (i.e. monkey work). Both of which are unpleasant and error prone. Additionally, it loosens up the coupling in your code and makes it easy for things to change even if your customer is not a problem.

For fake implementations of dependencies in .NET, Linq-to-Objects makes things extremely easy. For example, a simple fake authentication/authorization provider might look something like the following:

 1 //This model can be internal
 2 //as long as it is not exposed in your interfaces
 3 public class User
 4 {
 5     public string UserName { get; set; }
 6 
 7     //Note: Clear text is fine for fake/temporary users.
 8     public string Password { get; set; }
 9 
10     public string[] Roles { get; set; }
11 }
12 
13 public interface IAuthentication
14 {
15     bool IsValidUser(string userName, string password);
16 }
17 
18 public interface IAuthorization
19 {
20     bool IsInRole(string userName, string role);
21 }
22 
23 public class FakeAccessControl : IAuthentication, IAuthorization
24 {
25     private readonly List<User> users;
26 
27     public FakeAccessControl(User[] users)
28     {
29         this.users = new List<User>();
30         this.users.AddRange(users);
31     }
32 
33     public List<User> Users { get { return users; } }
34 
35     public bool IsValidUser(string userName, string password)
36     {
37         return users
38             .Any(x => x.UserName == userName 
39                 && x.Password == password);
40     }
41 
42     public bool IsInRole(string userName, string role)
43     {
44         var user = users
45             .FirstOrDefault(x => x.UserName == userName);
46         if (user == null || user.Roles == null)
47             return false;
48 
49         return user.Roles.Any(x => x == role);
50     }
51 }

Application Specific Dependencies

While logging, authentication, and authorization have pretty straightforward expected behaviors, application specific dependencies such as data access layers can be more unclear and need additional considerations. But, this does not mean that you cannot use the same approach. For these situations, models and interfaces should be based on the expected user experience and not necessarily your preferences. Also, it is vital that you produce acceptance tests around these models and interfaces and provide them as an additional part of the contract between you and your customer. Without creating acceptance tests, the models and interfaces are likely to be more vague than you realize. This will cause the developer(s) doing the implementation to make assumptions that are probably wrong. By proving tests, you can lead the implementers to produce exactly what you expect and push responsibilities to them when things go wrong while integrating. More importantly, it also gives you additional benefits:

  • a reduction in the integration time needed for the project
  • miscommunication is often minimized
  • the ability to demo (with fakes) early and often
  • the potential to identify problems early

Faking Data Dependencies

Just like non-functional requirements, fake implementations of application specific dependencies are easy in .NET when using Linq-to-Objects.

 1 // Obviously, this is grossly over simplified.
 2 // But, the concepts still apply... 
 3 
 4 public class Customer
 5 {
 6     public int Id { get; set; }
 7     public string Name { get; set; }
 8 }
 9 
10 public class Order
11 {
12     private readonly List<string> items = new List<string>();
13 
14     public int Id { get; set; }
15     public Customer Customer { get; set; }
16     public IList<string> Items { get { return items; } }
17 }
18 
19 public interface IOrderRepository
20 {
21     Order Get(int id);
22     void Save(Order model);
23     IEnumerable<Order> GetByCustomerId(int id);
24 }
25 
26 public class FakeOrderRepository : IOrderRepository
27 {
28     private readonly List<Order> orders = new List<Order>();
29 
30     public Order Get(int id)
31     {
32         return orders.FirstOrDefault(x => x.Id == id);
33     }
34 
35     public void Save(Order order)
36     {
37         if (order.Id == 0)
38         {
39             order.Id = orders.Count + 1;
40             orders.Add(order);
41         }
42     }
43 
44     public IEnumerable<Order> GetByCustomerId(int id)
45     {
46         return orders.Where(x => x.Customer.Id == id);
47     }
48 }

Specifying Dependency Behavior

To produce acceptance tests around these models / interfaces and make them understandable to the customer, I lean heavily on BDD frameworks such as SpecFlow. For the data dependency above, the specification might look something like the following:

 1 Feature: Order Repository
 2  
 3 Scenario: Save Order
 4 Given a new order for customer 'abc' with an id 123
 5 And an order item of 'widget 1'
 6 When the order is saved
 7 Then the id on the order should be greater than 0
 8  
 9 Scenario: Save and retrieve order by Id
10 Given a new order for customer 'xyz' with an id 789
11 And an order item of 'widget 2'
12 And the order is saved
13 When the new order is retrieved from the repository
14 Then the new order should be found
15 And the customer should be 'xyz'
16 And order items should contain 'widget 2'
17  
18 Scenario: Save and retrieve order by CustomerId
19 Given a new order for customer 'xyz' with an id 789
20 And an order item of 'widget 3'
21 And the order is saved
22 When the customer orders are retrieved from the repository via customer id of 789
23 Then 1 customer order should be found
24 And the customer order customer should be 'xyz'
25 
26  
27 #Add your own additional specifications

The step definitions for these specifications might look something like the following:

  1 [Binding]
  2 public class MitigatingDependenciesSteps
  3 {
  4     [Given(@"a new order for customer '(.*)' with an id (.*)")]
  5     public void GivenANewOrderForCustomer(string customer, int id)
  6     {
  7         var order = new Order { Customer = new Customer { Id = id, Name = customer } };
  8         ScenarioContext.Current.Add("order", order);
  9     }
 10 
 11     [Given(@"an order item of '(.*)'")]
 12     public void GivenAnOrderItemOf(string item)
 13     {
 14         var order = (Order)ScenarioContext.Current["order"];
 15         order.Items.Add(item);
 16     }
 17 
 18     [When(@"the order is saved")]
 19     [Given(@"the order is saved")]
 20     public void WhenTheOrderIsSaved()
 21     {
 22         var order = (Order)ScenarioContext.Current["order"];
 23         var repo = GetOrderRepo(ScenarioContext.Current);
 24         repo.Save(order);
 25 
 26         ScenarioContext.Current.Add("orderId", order.Id);
 27     }
 28 
 29     [When(@"the new order is retrieved from the repository")]
 30     public void WhenTheNewOrderIsRetrievedFromTheRepository()
 31     {
 32         var orderId = (int)ScenarioContext.Current["orderId"];
 33         var repo = GetOrderRepo(ScenarioContext.Current);
 34         var order = repo.Get(orderId);
 35 
 36         ScenarioContext.Current.Add("repo-order", order);
 37     }
 38 
 39     [When(@"the customer orders are retrieved from the repository via customer id of (.*)")]
 40     public void WhenTheNewOrderIsRetrievedFromTheRepositoryViaCustomerIdOf(int customerId)
 41     {
 42         var repo = GetOrderRepo(ScenarioContext.Current);
 43         var orders = repo.GetByCustomerId(customerId);
 44 
 45         ScenarioContext.Current.Add("cust-orders", orders);
 46     }
 47 
 48 
 49 
 50     [Then(@"the id on the order should be greater than (.*)")]
 51     public void ThenTheIdOnTheOrderShouldBeGreaterThan(int val)
 52     {
 53         var order = (Order)ScenarioContext.Current["order"];
 54         Assert.Greater(order.Id, val);
 55     }
 56 
 57     [Then(@"the new order should be found")]
 58     public void ThenTheNewOrderShouldBeFound()
 59     {
 60         var order = ScenarioContext.Current["repo-order"] as Order;
 61         Assert.IsNotNull(order);
 62     }
 63 
 64     [Then(@"the customer should be '(.*)'")]
 65     public void ThenTheCustomerShouldBe(string customer)
 66     {
 67         var order = (Order)ScenarioContext.Current["repo-order"];
 68         Assert.AreEqual(customer, order.Customer.Name);
 69     }
 70 
 71     [Then(@"order items should contain '(.*)'")]
 72     public void ThenOrderItemsShouldContain(string item)
 73     {
 74         var order = (Order)ScenarioContext.Current["repo-order"];
 75         var hasItem = order.Items.Any(x => x == item);
 76         Assert.IsTrue(hasItem);
 77     }
 78 
 79     [Then(@"(.*) customer order should be found")]
 80     public void ThenCustomerOrderShouldBeFound(int count)
 81     {
 82         var orders = (IEnumerable<Order>)ScenarioContext.Current["cust-orders"];
 83         Assert.AreEqual(count, orders.Count());
 84     }
 85 
 86     [Then(@"the customer order customer should be '(.*)'")]
 87     public void ThenTheCustomerOrderCustomerShouldBe(string customer)
 88     {
 89         var orders = (IEnumerable<Order>)ScenarioContext.Current["cust-orders"];
 90         var match = orders.All(x => x.Customer.Name == customer);
 91         Assert.IsTrue(match);
 92     }
 93 
 94 
 95     private IOrderRepository GetOrderRepo(ScenarioContext ctx)
 96     {
 97         const string key = "order-repo";
 98         if (ctx.ContainsKey(key))
 99         {
100             return (IOrderRepository)ctx[key];
101         }
102 
103         var repo = new FakeOrderRepository();
104         ctx.Add(key, repo);
105         return repo;
106     }
107 }

Other Suggestions

  • Keep fake implementations separated from your interfaces and specifications by putting them into a separate assembly.
  • Keep dependency interfaces and models in a separate assembly from both your application code and the dependency implementation. This is especially true for WCF data contracts.
  • When you need to update your "contracts" and specifications, talk to the person developing that piece and publish the changes to them as soon as possible.
  • If you end up not needing something in your "contract," remove it. There is no good reason to have someone build something for you when you know you will end up not needing it. This is true even when the work is already started.
  • Don't throw your fake implementations away. Use your fakes during demos! Demos are notorious for going wrong in front of an audience even after being practiced repeatedly. Why gamble that you'll have connectivity to a database or a REST service during your demo?

Conclusions

As developers (and humans for that matter), we cannot always act alone. We inevitably need to depend on others for at least some of the things we need. Because of this, much of what we do as developers involves communication. The biggest obstacle for communication is assumptions. By explicitly defining your needs (i.e. dependencies), you not only help others to help you, but you can avoid late integration problems and maintain your ability to demo early and often. This type of abstraction is valuable for not only fast-tracking your release, but it also leaves you open for change in the future.