Finding the right level of coupling

Up On the Tight Rope by Smiles Are Free.

https://www.flickr.com/photos/tammys_world/1785770650/

This is a follow up from my previous post on Composite extensions for Win Forms.

Early on as we started the development of Prism, we had a few decisions to make around our coupling to WPF. We knew that Prism undoubtedly was being targeted for WPF. On the other hand we knew that some aspects of the work were not WPF specific, and might be applicable for other platforms.

After many discussions we arrived at the following guiding principles.

  • Don't unnecessarily couple anything to WPF.
  • Leverage WPF to the fullest extend possible.
  • Don't unnecessarily couple to other parts of Prism itself.
  • Don't unnecessarily couple to a specific IoC container.

The principles may seem contradictory, but they are not. What they meant for us was that if a particular problem we encountered was best solved by WPF, then by all  means we used it. That instead of coming up with a generic abstraction that is non-WPF specific. On the other hand if there were other problems whose solution was not by it's nature WPF specific then we did not make it have any dependencies on WPF. These were guiding principles with some exceptions, the Bootstrapper being one of them . We could have decoupled it from WPF but decided not too. However nothing in the CAL depended on the Bootstrapper and it was fairly easy to implement so we felt we could get  away with it and reduce the number of types we introduced.

Below are some of  the problems we encountered and how we handled them.

Problem: Assembly level coupling to a particular platform technology

At a high level before delving into specific challenges there was an issue of assembly coupling. What this meant is that if a non-UI specific piece of functionality was mixed in an assembly that was UI specific, then that meant that anyone who used that assembly, had to reference the UI assemblies that were reference. For example if the module loader sits in the same assembly where there are direct references to WPF, then if I reference that assembly from a WinForms app, I pull along those WPF references. Further more having the two in the same assembly, made it much more likely that the pieces within would end up coupled to something that was platform specific.

Solution: Assembly separation and layering.

The solution here simply to break out the Composite Application Library into two assemblies (and corresponding namespaces) namely "Microsoft.Practices.Composite.dll" and "Microsoft.Practices.Composite.Wpf.dll". We then carefully placed each service / component in Prism into it's respective place. In some cases there was layering between the two. Many of the core services in Prism live in Microsoft.Practices.Composite such as the Module Loader and the container facade. Other services use a layered approach, such as the Event Aggregator which has a core set of infrastructure in Composite, along with WPF specific functionality that layers on top of it in Composite.Wpf. The Region Manager functionality all lives in Composite.Wpf, however all of its interfaces are defined in Composite.

Problem: Implementing Module Loading

In the case of the Module loader, there was nothing about it that seemed WPF specific. Modules / Module loading was something that we had implemented over and over again in p&p at least three different times. Did we need a fourth? We felt not. When we looked at the module loading mechanisms we had done, we realized that there were two points of coupling. First, there were the parameters the loader required the module to accept. In the case of CAB, the module required a WorkItem, while in the case of WCSF it required a CompositionContainer. In either case the parameters provided to the loader were a gateway to set of other services. Furthermore the module was a base class which also tied in some more specific bits. What to do?

Solution: Favoring Composition over Inheritance.

The solution? Applying a simple principle. Composition vs Inheritance. We removed passing in a service locator to the module. Then we changed the module from being a base class to an interface. Thus with these changes the module become agnostic to platform and framework. This solved one part of the problem, but how would the module get access it's services if there was no longer presence of something similar to a WorkItem? To address this. we brought an IoC container into the mix to handle providing dependencies to the module via it's constructor.  We then implemented the module loader service to utilize the container to resolve modules. As the module is resolved, it's dependencies are injected. Rather than binding the loader to a specific IoC container implementation, we had the loader utilize a pluggable IContainerFacade (the precursor to IServiceLocator) implementation to talk to the container.

Below is a snippet of the module loader which shows this functionality.

 /// <summary>
 /// Initialize the specified list of modules.
 /// </summary>
 /// <param name="moduleInfos">The list of modules to initialize.</param>
 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
 public void Initialize(ModuleInfo[] moduleInfos)
 {
     List<ModuleInfo> modules = GetModulesLoadOrder(moduleInfos);
  
     IEnumerable<ModuleInfo> newModules = LoadAssembliesAndTypes(modules);
  
     foreach (ModuleInfo moduleInfo in newModules)
     {
         Type type = initializedModules[moduleInfo.ModuleType];
  
         try
         {
             IModule module = CreateModule(type);
             module.Initialize();
         }
         catch (Exception e)
         {
             HandleModuleLoadError(moduleInfo, type.Assembly.FullName, e);
         }
     }
 }
  
 /// <summary>
 /// Uses the container to resolve a new <see cref="IModule"/> by specifying its <see cref="Type"/>.
 /// </summary>
 /// <param name="type">The type to resolve. This type must implement <see cref="IModule"/>.</param>
 /// <returns>A new instance of <paramref name="type"/>.</returns>
 protected virtual IModule CreateModule(Type type)
 {
     return (IModule)containerFacade.Resolve(type);
 }

Problem: Implementing Event Aggregation

Event Aggregation was an interesting case. Parts of the aggregator had nothing particularly platform specific. The invocation of the event handler was on the other hand very specific. Events fired by the aggregator have different threading needs with regards to how the subscriber is invoked. In the case of WPF using the Dispatcher was the easiest way to go. However, if you are in Win Forms or on the web, different rules apply.

Solution: Applying separation of concerns, layering, introducing an abstract event class

Our EventAggregator implementation can essentially be broken into several parts. There is the subscription / retrieval of the events which the EventAggregator service provides, and then the mechanism for dispatch.  The key to providing reuse in this case was keeping the two decoupled. The EventAggregator service itself only knows about registering and retrieving events, it does not know anything about the invocation. For the invocation side we introduced an abstract EventBase class. We discussed using an interface, but there were some specific expectations we had around how events were implemented for example ensuring that subscriptions are properly cleaned up. The EventBase relies on an IEventSubscription class for individual subscriptions. This allows flexibility not just around invocation but around the registration itself which we use in our implementation. I am sure one could argue that we could have done even further refactoring, however we got to the point that allowed us to achieve our goals.

Note: In this case having a base did not seem to block any further extensibility requirements of customers, as it was so low level.  In other cases it does, such as in the module implementation mentioned before. The moral of this story is there is no silver bullet, and it depends, it depends, it depends.

Once we had the base in place, we made the EventAggregator service depend on that base rather than a specific implementation. To address the WPF specific needs we used layering and included a CompositeWPFEvent which uses the Dispatcher. We also introduced a host of other capabilities as well in the CompositeWPFEvent like weak delegate dispatching and such, invoking the publisher thread, etc. Introducing it at this level meant not coupling other platform implementations to use this functionality. 

Below is the code for the Subscribe method which as you can see delegates to the InternalSubscribe method (from EventBase) which takes an IEventSubscription.

 public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate<TPayload> filter)
 {
     IDelegateReference actionReference = new DelegateReference(action, keepSubscriberReferenceAlive);
     IDelegateReference filterReference = new DelegateReference(filter, keepSubscriberReferenceAlive);
     EventSubscription<TPayload> subscription;
     switch (threadOption)
     {
         case ThreadOption.PublisherThread:
             subscription = new EventSubscription<TPayload>(actionReference, filterReference);
             break;
         case ThreadOption.BackgroundThread:
             subscription = new BackgroundEventSubscription<TPayload>(actionReference, filterReference);
             break;
         case ThreadOption.UIThread:
             subscription = new DispatcherEventSubscription<TPayload>(actionReference, filterReference, UIDispatcher);
             break;
         default:
             subscription = new EventSubscription<TPayload>(actionReference, filterReference);
             break;
     }
  
  
     return base.InternalSubscribe(subscription);
 }

Problem: Region Manager

Regions are the mechanism that Prism uses for defining layouts to plug views into. As we looked at WPF, we saw some very specific benefits WPF provided in implementing regions. For example using attached properties was a great way to associate a particular portion of the UI as a region, and allow a declarative mechanism for registering it. Additionally ItemsControls along with WPFs DataTemplate capability were a perfect way of controlling how views were laid out. On the other hand the notion of a region itself and a registry off regions seemed plausable for other UI platform technologies though the implementation would greatly differ. Also it seemed reasonable that extenders of CAL might want to replace parts or the whole of our region manager implementation.

Solution: Favoring Composition over Inheritance, Layering.

The solution here was to create a set of interfaces for the Region Manager and Region Adapter. These interfaces were placed in the Microsoft.Practices.Composite which is UI Agnostic. We then created a host of WPF specific classes which layered on top of this infrastructure which lives in the Microsoft.Practices.Composite.Wpf. Applications that utilize the CAL, depend on the Region Manager interfaces rather than any specific implementations.

Below is a snapshot of the code for some of the region related interfaces.

 public interface IRegionManager
  {
      IDictionary<string, IRegion> Regions { get; }
      void AttachNewRegion(object regionTarget, string regionName);
      IRegionManager CreateRegionManager();
  }
  
 public interface IRegion
  {
      IViewsCollection Views { get; }
      IViewsCollection ActiveViews { get; }
      IRegionManager Add(object view);
      IRegionManager Add(object view, string viewName);
      IRegionManager Add(object view, string viewName, bool createRegionManagerScope);
      void Remove(object view);
      void Activate(object view);
      void Deactivate(object view);
      object GetView(string viewName);
      IRegionManager RegionManager { get; set; }
  }
  
 public interface IViewsCollection : IEnumerable<object>, INotifyCollectionChanged
 {
     bool Contains(object value);
 }

So what did we learn?

Above and beyond Prism itself, we learned some important lessons about finding that right level of coupling.  It's a bit like walking a tight-rope, with each case having to carefully evaluated:

  • If you couple too tightly, you can greatly limit extensibility, testability and general usage scenarios, for example not being able to use in a brown-field (existing) application, which is often a critique leveled at CAB.
  • If you are too decoupled you end up introducing a lot of abstractions which greatly increases complexity and decreases maintainability of the system.  You also often end up in a least-common-denominator situation where by decoupling from a specific technology, prevents you from taking full advantage of any specific one. This specific point comes up all the time whenever there are discussions around (the myth of) portable UI.

In other words, there's no silver bullet. In some cases it makes sense to be more tightly coupled, in others it makes sense to be more decoupled. Finding the right balance between the two is key. It's not easy, but absolutely necessary for a maintainable system.