Description: dependency injection2e.jpg

From Dependency Injection, Principles, Practices, and Patterns by Steven van Deursen and Mark Seemann

This article explores the Ambient Context DI anti-pattern: what it is, how to identify it, and why it’s so dangerous.


Take 37% off Dependency Injection, Principles, Practices, and Patterns. Just enter code fccseemann into the discount code box at checkout at manning.com.


The Ambient Context anti-pattern is related to Service Locator. Where a Service Locator allows global access to an unrestricted set of Dependencies, an Ambient Context makes a single strongly typed Dependency available through a static accessor.

Definition

An Ambient Context supplies application code outside the Composition Root with global access to a Volatile Dependency or its behavior by the use of static class members.

A Volatile Dependency is a Dependency that involves side effects that can be undesirable at times. This may include modules that don’t yet exist or that have adverse requirements on its runtime environment. These are the Dependencies that are addressed by DI and hidden behind Abstractions.

The following listing shows the Ambient Context anti-pattern in action.

Listing 1  Using the ambient Context anti-pattern (BAD CODE!)

  
 public string GetWelcomeMessage()
 {
     ITimeProvider provider = TimeProvider.Current;      
     DateTime now = provider.Now;
  
     string partOfDay = now.Hour < 6 ? "night" : "day";
  
     return string.Format("Good {0}.", partOfDay);
   }
  

The Current static property represents the Ambient Context, which allows access to an ITimeProvider instance. This hides the ITimeProvider Dependency and complicates testing.

In this example, ITimeProvider presents an Abstraction which allows retrieving the system’s current time. Because you might want to influence how time’s perceived by the application (for instance, for testing), you don’t want to call DateTime.Now directly. Instead of letting consumers call DateTime.Now directly, a good solution is to hide access to DateTime.Now behind an Abstraction. It’s all too tempting to allow consumers to access the default implementation through a static property or method. In listing 1, the Current property allows access to the default ITimeProvider implementation.

Ambient Context is similar in structure to the Singleton pattern.[2] Both allow access to a Dependency by the use of static class members. The difference is that Ambient Context allows its Dependency to be changed, whereas the Singleton pattern ensures that its singular instance never changes.

The access to the system’s current time is a common need. Let’s dive a little bit deeper into the ITimeProvider example.

Example: Accessing time through Ambient Context

Many reasons exist where one would need to exercise some control over time. Many applications have business logic that depends on time or the progression of it. In the previous example, you saw a simple case where we displayed a welcome message based on the current time.

Because the need to work with time is such a widespread requirement, developers often feel the urge to simplify access to such a Volatile Dependency by using an Ambient Context. The following listing shows an example ITimeProvider Abstraction.

Listing 2 An ITimeProvider Abstraction

  
 public interface ITimeProvider
 {
     DateTime Now { get; }        
 }
  

Allows consumers to acquire the system’s current time

The following listing shows a simplistic implementation of the TimeProvider class for this ITimeProvider Abstraction.

Listing 3 A TimeProvider ambient Context implementation (BAD CODE!)

  
 public static class TimeProvider                              
 {
     private static ITimeProvider current =                    
         new DefaultTimeProvider();
  
     public static ITimeProvider Current                       
     {
         get { return current; }
         set { current = value; }
     }
  
     private class DefaultTimeProvider : ITimeProvider         
     {
         public DateTime Now { get { return DateTime.Now; } }
     }
   }
  

A static class that allows global access to a configured ITimeProvider implementation

Initialization of a Local Default that uses the real system clock

Static property that allows global read/write access to the ITimeProvider Volatile Dependency

Default implementation that uses the real system clock

Using the TimeProvider implementation, you can unit test the previously defined GetWelcomeMessage method. The following listing shows such test.

Listing 4 A unit test depending on an ambient Context (BAD CODE!)

  
  [Fact]
 public void SaysGoodDayDuringDayTime()
 {
     // Arrange
     DateTime dayTime = DateTime.Parse("2017-05-14 6:00");
  
     var stub = new TimeProviderStub { Now = dayTime };
  
     TimeProvider.Current = stub;                             
  
     var sut = new WelcomeMessageGenerator();                 
  
     // Act
     string actualMessage = sut.GetWelcomeMessage();          
  
     // Assert
     Assert.Equal(expected: "Good day.", actual: actualMessage);
     }
  

Replaces the default implementation with a Stub that always returns the specified dayTime.

WelcomeMessageGenerator’s API is dishonest because its constructor hides the fact that ITimeProvider is a required Dependency.

There’s an implicit relationship between TimeProvider.Current and GetWelcomeMessage.

This is one variation of the Ambient Context anti-pattern. Other common variations you might encounter are these:

  • An Ambient Context which allows consumers to make use of the behavior of a globally configured Dependency. With the previous example in mind, the TimeProvider could supply consumers with a static GetCurrentTime method that hides the used Dependency by calling it internally.
  • An Ambient Context that merges the static accessor with the interface into a single Abstraction. In respect to the previous example, that would mean that you have a single TimeProvider base class that contains both the Now instance property and the static Current property.
  • An Ambient Context where delegates are used instead of a custom-defined Abstraction. Instead of having a fairly descriptive ITimeProvider interface, you could achieve the same using a Func<DateTime> delegate.

Ambient Context can come in many shapes and implementations. Again, the caution regarding Ambient Context is that it provides either direct or indirect access to a Volatile Dependency by means of some static class member.

Many other examples of Ambient Context exist, but this example is common and widespread enough that we’ve seen them countless times in companies we’ve consulted with. Let’s  discuss why it’s a problem and how to deal with it.

Analysis of ambient Context

Ambient Context is usually encountered when developers have a Cross-Cutting Concern as a Volatile Dependency, which is used ubiquitously. This ubiquitous nature makes developers think it justifies moving away from Constructor Injection. It allows them to hide Dependencies and avoids the necessity of adding the Dependency to many constructors in their application.

The problems with Ambient Context are related to the problems with Service Locator. Here are the main issues:

  • The Dependency is hidden.
  • Testing becomes more difficult.
  • It becomes hard to change the Dependency based on its context.
  • There’s Temporal Coupling between the initialization of the Dependency and its usage.

When you hide a Dependency by allowing global access to it through Ambient Context, it becomes easier to hide the fact that a class has too many Dependencies. This is related to the Constructor Over-injection code smell and it’s typically an indication that you’re violating the Single Responsibility Principle.

When a class has many Dependencies, it’s an indication that it’s doing more than it should. It’s theoretically possible to have a class with many Dependencies, while still having one reason to change. The larger the class the less likely it is to abide by this guidance. The use of Ambient Context hides the fact that classes might have become too complex, and need to be refactored.

Ambient Context also makes testing more difficult because it presents a global state. When a test changes the global state, as you saw in listing 4, it might influence other tests. This is the case when tests run in parallel, but even sequentially executed tests can be affected when a test forgets to revert its changes as part of its teardown. Although these test-related issues can be mitigated, it means building a specially crafted Ambient Context and either global or test-specific teardown logic. This adds complexity, whereas the alternative doesn’t.

The use of an Ambient Context makes it hard to provide different consumers with different implementations of the Dependency. For instance, say you need part of your system to work with a moment in time that’s fixed at the start of the current request, whereas other, possibly long-running operations, should get a Dependency that’s live-updated. When using an Ambient Context, the consumer must typically provide the Ambient Context with additional information to allow different implementations to be returned. This needlessly complicates the consumer.

The use of an Ambient Context causes the usage of its Dependency coupled on a temporal level, a code smell known as Temporal Coupling. Unless you initialize the Ambient Context in the Composition Root, the application fails when the class starts using the Dependency for the first time. We rather want our applications to fail fast instead.

Although Ambient Context isn’t as destructive as Service Locator, because it only hides a single Dependency opposed to an arbitrary number of Dependencies, it has no place in a well-designed code base. Better alternatives exist, most obviously Constructor Injection.


That’s all for this article. If you want to learn more about Ambient Context, check it out on liveBook here.

You can also see other articles on common DI-related topics:
Writing Maintainable, Loosely-Coupled Code
Understanding Property Injection
Understanding Method Injection
Understanding the Composition Root
Understanding Constructor Injection
Abuse of Abstract Factories
The Service Locator Anti-Pattern
The Transient Lifestyle
The Scoped Lifestyle
The Singleton Lifestyle

 

[2] Erich Gamma et al., Design Patterns, 132.