Thursday, February 26, 2009

Specification Manager

So, I'm very fond of the Specification Pattern. I typically find myself writing one Specification for each MVP view that may or may not create new Specifications. Seems like I'm repeating my self a bit, and there HAD to be a better way to do it.

So I got to thinking, we just need a Manager that you can send an instance of a view, and get all the specifications in the assembly that the view does not satisfy.

Simple enough, so I wrote this.

    8  [TestFixture]

    9     public class ViewSpecificationManagerTestFixture

   10     {

   11         [Test]

   12         public void manager_returns_failed_views()

   13         {

   14             var mocker = new MockRepository();

   15             var view = mocker.DynamicMock<ITestView>();

   16             var manager = new ViewSpecificationManager();

   17             var failedSpecs = manager.GetFailedSpecifications(view);

   18             Assert.That(failedSpecs.Count == 1);

   19             Assert.That(failedSpecs[0].GetType() == typeof(TestSpecificationThatAlwaysFails));

   20         }

   21     }

 

Yeah, that's the unit test... Here's the working class...

 

 

    7  /// <summary>

    8     /// Manager to handle Views and thier Specs

    9     /// </summary>

   10     public class ViewSpecificationManager

   11     {

   12         private readonly SpecificationFactory factory;

   13         public ViewSpecificationManager():this(new SpecificationFactory())

   14         {

   15         }

   16 

   17         internal ViewSpecificationManager(SpecificationFactory factory)

   18         {

   19             this.factory = factory;

   20         }

   21         /// <summary>

   22         /// Returns all Specifications not satisfied by the view

   23         /// </summary>

   24         /// <typeparam name="T"></typeparam>

   25         /// <param name="view"></param>

   26         /// <returns></returns>

   27         public virtual Collection<Specification<T>> GetFailedSpecifications<T>(T view)

   28         {

   29             var failedSpecs = new List<Specification<T>>();

   30             foreach (var specification in factory.GetSpecificationsFor(view))

   31             {

   32                 if(!specification.IsSatisfiedBy(view))

   33                 {

   34                     failedSpecs.Add(specification);

   35                 }

   36             }

   37             return new Collection<Specification<T>>(failedSpecs);

   38         }

   39     }

So basically, all this class does is get all the specs from the SpecificationFactory and check thier IsSatisfiedBy method and returns the ones that fail. Pretty simple, right?

I didn't want the Manager to understand ANYTHING except how to check all the specifications for the view it's given, NOT how to actually figure out what specifications are there, that would violate SRP.

The SpecificationFactorys single responsibility is to get all the Specifications that match up to the view.  Here are it's unit tests...

Actually, here are some dummy classes I created for the unit test consumption.

    5 public class ITestView { }

    6     public class TestSpecificationThatAlwaysPasses : Specification<ITestView>

    7     {

    8         public override bool IsSatisfiedBy(ITestView obj)

    9         {

   10             return true;

   11         }

   12     }

   13     public class TestSpecificationThatAlwaysFails : Specification<ITestView>

   14     {

   15         public override bool IsSatisfiedBy(ITestView obj)

   16         {

   17             return false;

   18         }

   19     }

Now, here's the unit tests...

   12 [Test]

   13         public void factory_returns_newed_up_specs()

   14         {

   15             var mocker = new MockRepository();

   16             var view = mocker.DynamicMock<ITestView>();

   17             var factory = new SpecificationFactory();

   18             var specs = factory.GetSpecificationsFor(view);

   19             Assert.That(specs.Count == 2);

   20             Assert.That(specs[0],Is.Not.Null);

   21             Assert.That(specs[1],Is.Not.Null);

   22         }

 

Simple enough, right... implementation just news them up....

    8 public class SpecificationFactory

    9     {

   10         private readonly ViewSpecificationResolver resolver;

   11 

   12         public SpecificationFactory():this(new ViewSpecificationResolver())

   13         {

   14         }

   15 

   16         internal SpecificationFactory(ViewSpecificationResolver resolver )

   17         {

   18             this.resolver = resolver;

   19         }

   20         /// <summary>

   21         /// Returns all <see cref="Specification{T}"/> for the object sent

   22         /// </summary>

   23         /// <typeparam name="T"></typeparam>

   24         /// <param name="view"></param>

   25         /// <returns></returns>

   26         public virtual Collection<Specification<T>> GetSpecificationsFor<T>(T view)

   27         {

   28             var specifications = new List<Specification<T>>();

   29             foreach(var type in resolver.GetSpecificationsFor<T>())

   30             {

   31                 specifications.Add((Specification<T>)Activator.CreateInstance(type));

   32             }

   33             return new Collection<Specification<T>>(specifications);

   34         }

   35 

   36 

   37     }

 

So yeah, the SpecificationFactory DOES NOT know what types to get, just to new up the types that the ViewSpecificationResolver object tells it to.

 

Here is it's unit test.

 

   13 [Test]

   14         public void resolver_finds_specifications_for_view()

   15         {

   16             var resolver = new ViewSpecificationResolver();

   17             var results = resolver.GetSpecificationsFor<ITestView>();

   18             Assert.That(results.Count,Is.EqualTo(2));

   19             Assert.That(results[0] == typeof(TestSpecificationThatAlwaysPasses) || results[0] == typeof(TestSpecificationThatAlwaysFails));

   20             Assert.That(results[1] == typeof(TestSpecificationThatAlwaysPasses) || results[1] == typeof(TestSpecificationThatAlwaysFails));

   21 

   22         }

 

Here's the actual class...

 

    8  public class ViewSpecificationResolver

    9     {

   10         public virtual Collection<Type> GetSpecificationsFor<T>()

   11         {

   12             var types = typeof (T).Assembly.GetTypes();

   13             var specifications = new List<Type>();

   14             foreach (var type in types)

   15             {

   16                 if(type.BaseType == typeof(Specification<T>))

   17                 {

   18                     specifications.Add(type);

   19                 }

   20             }

   21             return new Collection<Type>(specifications);

   22         }

   23     }

 

 

There's all the guts, now, from any Presenter, you can new up the ViewSpecificationManager and Call GetFailedSpecificationsFor(view).. Like SO...

 

   26           specificationManager = new ViewSpecificationManager();

   27                     var failedSpecifications = specificationManager.GetFailedSpecifications(view);

 

So that's pretty good... I like it, since the Presenter no longer cares about specifications for the view. I just thought it would be nicer to go ahead and create a Presenter base class that does this for you...

 

Here's what I did.

 

    6  /// <summary>

    7     /// Class that Presenters should inherit

    8     /// </summary>

    9     /// <typeparam name="TView"></typeparam>

   10     public abstract class Presenter<TView>

   11     {

   12         protected readonly TView view;

   13         private ViewSpecificationManager specificationManager;

   14 

   15         protected Presenter(TView view)

   16         {

   17             this.view = view;

   18         }

   19 

   20         internal virtual ViewSpecificationManager ViewSpecificationManager

   21         {

   22             get

   23             {

   24                 if (specificationManager == null)

   25                 {

   26                     specificationManager = new ViewSpecificationManager();

   27                 }

   28                 return specificationManager;

   29             }

   30         }

   31         /// <summary>

   32         /// Returns the <see cref="Specification{T}"/> that the <see cref="TView"/> does not satisfy.

   33         /// </summary>

   34         /// <returns></returns>

   35         protected Collection<Specification<TView>> GetFailedSpecifications()

   36         {

   37             return ViewSpecificationManager.GetFailedSpecifications(view);

   38         }

   39 

   40     }

 

There ya go.. now from any Presenter you can simple call GetFailedSpecifications() and get the list of all failed specifications.  THen just call SetFailedSpecsMessage from the MvpPage base class...

 

Here it is...

 

 

 

    9 public abstract class MvpPage:Page

   10     {

   11         /// <summary>

   12         /// The Presenter for this page

   13         /// </summary>

   14         private object presenter;

   15         protected override void OnLoad(System.EventArgs e)

   16         {

   17             base.OnLoad(e);

   18             presenter = PresenterFactory.GetPresenterFor(this);

   19         }

   20         /// <summary>

   21         /// Sets the controls text to the Resources for the failed Specs

   22         /// </summary>

   23         /// <typeparam name="TView"></typeparam>

   24         /// <param name="specifications"></param>

   25         /// <param name="control"></param>

   26         protected void SetFailedSpecsMessage<TView>(Collection<Specification<TView>> specifications,ITextControl control)

   27         {

   28             foreach (var specification in specifications)

   29             {

   30                 var message = GetLocalResourceObject(specification.GetType().Name);

   31                 control.Text += String.Format("<span>{0}</span>", message);

   32             }

   33         }

   34     }

 

 

Have fun with it, and feel free to tell me what sucks about it (or what doesn't!) :)

No comments:

Post a Comment