Someone decided to publicly criticize my example code in my previous post by saying that it was “bad.” While I don’t agree with the nature of the criticism (my blog *does* have comments, you know) I totally agree with the message. It was bad code. I would never place that in a production application.
The intent of the post was to demonstrate some ajax techniques, not to talk about how to write testable, decoupled controllers. I suppose that example code should come with a bit of a disclaimer that says “hey, we are doing this here for simplicity’s sake, but obviously you’d abstract this out.” Ideally I would have commented out the entire flickr section, because it’s totally not important in the context of the example.
In light of this topic, here is how a perfectly testable PhotosController might be written:
public class PhotosController : Controller { private readonly IPhotoRepository _photoRepository; public PhotosController(IPhotoRepository photoRepository) { _photoRepository = photoRepository; } [ControllerAction] public void Search() { RenderView("Search", new Photo[] { }); } [ControllerAction] public void Find(string query, bool? ajax) { var photos = _photoRepository.SearchPhotos(query); ViewData["query"] = query; ViewData["photos"] = photos; if(ajax == true) { RenderView("_images", photos); } else { RenderView("results"); } } }
Here you can clearly see our dependency on IPhotoRepository. This guy can be anything we want, from a Flickr implementation, google images implementation, a file system directory, or even (gasp) a TEST implementation for writing unit tests!
Here is the IPhotoRepository, that we are using in the example:
public interface IPhotoRepository { Photo[] SearchPhotos(string query);asdf }
asdf
This returns a Photo object (that I defined), which means whatever implementation you choose, you need to map to this object. This is a good thing because it removed my dependency on "FlickrPhoto" in the view itself.
Our FlickrPhotoRepository now looks like this:
public class FlickrPhotoRepository : IPhotoRepository { private readonly IConfigSource _ConfigSource; public FlickrPhotoRepository(IConfigSource configSource) { _ConfigSource = configSource; } public Photo[] SearchPhotos(string query) { var flickr = new Flickr(_ConfigSource.GetSetting("flickr.api.key")); var results = flickr.PhotosSearch(query, TagMode.AnyTag, query, 40, 1); var photos = new List<Photo>(); foreach (var item in results.PhotoCollection) { photos.Add(new Photo(item.SquareThumbnailUrl, item.Title, item.PhotoId)); } return photos.ToArray(); } }
Wait a minute! This class now has it's own dependency on IConfigSource? Craziness! In our example code from before, I had accessed the Configuration singleton directly, which is punishable by hand-removal in Albonia. Thank goodness I don't live there. Now this class is decoupled from the actual configuration implementation and it could really be coming from a text file or database (whatever you want).
Wiring all this up is easy. You could do it via XML, but I chose to do it directly in code: private void InitializeWindsor() { _container = new WindsorContainer(); _container.AddComponent("config-source", typeof(IConfigSource), typeof(AppDomainConfigSource)); foreach(Type type in Assembly.GetExecutingAssembly().GetTypes()) { if(type.IsSubclassOf(typeof(Controller))) { _container.AddComponent(type.Name, type); } } }
private void InitializeWindsor() { _container = new WindsorContainer(); _container.AddComponent("config-source", typeof(IConfigSource), typeof(AppDomainConfigSource)); foreach(Type type in Assembly.GetExecutingAssembly().GetTypes()) { if(type.IsSubclassOf(typeof(Controller))) { _container.AddComponent(type.Name, type); } } }
Also note that I'm using WindsorControllerFactory from MVCContrib, so now my controller can be created (and each of it's dependencies ( and their dependencies (and their Dependencies)))... I think you get the idea.
And there you have it. A perfectly decoupled and testable modification for my previous example code. As you were….
I'm Ben Scheirman. I am a .NET software developer with a strong interest in agility. I work as a Principal Consultant with Sogeti.
Read more here.
email me
Ads by The Lounge
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.