I’m a big proponent of the whole Convention over configuration idea. I give up some meticulous control over plumbing of my code, and by virtue of adhering to conventions the code just finds its way into the right place. With great power comes great responsibility, as someone once said. You may give up direct control over the process, but you should still somehow validate your code adheres to the conventions, to avoid often subtle bugs.
So what is it about?
Say you have web, MVC application, with controllers, views and such. You say controllers have a layer supertype, common base namespace and common suffix in class name. Great, but what happens if you (or the other guy on your team), fail to conform to these rules? There’s no compiler to raise a warning, since the compiler has no idea about the rules you, (or someone else) came up with. What happens is, most likely your IoC container if you use one, or perhaps routing engine of your application will fail to properly locate the type, and the application will blow into your face during a demo for very important customer.
Take another scenario. Say you’re using AutoMapper, and you came up with convention that entities in namespace Foo.Entities are mapped to DTOs in namespace Foo.Dtos, so that Foo.Entities.Bar gets its mapping to Foo.Dtos.BarDto registered automatically. Again – it won’t pick mapping to FizzbuzzDto when you accidentally put Fizzbuzz entity in Foo.Services. Depending on your test coverage you will either find out sooner or later.
These are usually a hard rules, and both AutoMapper and most IoC frameworks will be able to figure out something is missing and notify you (with an exception). This is a good thing, since you fail fast, and quickly get to fix your rules. You won’t be that lucky all the time though. Say you’re writing an MVVM framework, similar to one Rob built, and create a convention wiring enable state of some controls bound to method Execute, with property CanExecute on your view model (if you have no idea what the heck I’m talking about, go see Rob’s presentation from Mix10). When you fail to conform to this rule, no exception will occur, world will not end. Merely a button will be clickable all the time, instead of only when some validity rule is met. This is a bug though, just one harder to spot, which means usually it won’t be you who finds it – your angry users will.
So I have a problem?
So what to do? You can loosen your conventions. If you had an issue due to namespace mismatch, perhaps don’t require namespaces to match, just type names? Or if you forgot to put a Controller suffix on your controller, just ditch that requirement and it will work just fine, right?
Yes it will (probably), or at least you find yourself breaking some other requirement of your convention and feel the need to shed it as well. This however does not fix the problem – it merely replaces it with another – you will have rotten code.
Problem is not that your convention was too rigid (although sometimes this will indeed be the case as well), you not adhering to the convention was the real problem. Conventions are like compiler rules – follow them or don’t consider your code ‘working’. To get compiler-like behavior you need to take this one step further and along with finding code adhering to your conventions, find the code that does not.
Take the AutoMapper example again. We have designated namespace for our convention – Foo.Dtos. For each type in this namespace we require that a matching type in Foo.Entities namespace exists. You should check if an unmatched dto type exists and send some notification in case there is. Also since you require DTOs and only DTOs to live in that namespace you should express that requirement in code – check for types with Dto suffix in other namespaces, and types without this suffix in Foo.Dtos namespace, and in each case send an appropriate notification as well.
This is a form of executable specification. When a new guy/gal comes to your team and starts working on the code base you don’t have to worry about them breaking the code because they don’t know the conventions you follow. The code will tell them when something is wrong, and what to do to make it right. It will also help you keep your code clean and structured. Improving its readability and in the long run, also decreasing possibility of code rot.
What to do?
I still haven’t touched upon one important thing – how and where do you implement the convention validation rules. There are three places I would put convention validation.
- The bootstrapping code itself. It often makes sense to take advantage of the fact you’re scanning for conventions to perform the conventions bootstrapping and do check for misses as well. Especially when the framework you’re working with does not have any support for conventions built in and you’re doing it all manually.
- Unit tests. Add tests that call AutoMapper’s ‘AssertConfigurationIsValid’ or in case of homegrown framework, reflect over your model and look for deviations from your conventions. It really is very little work upfront compared to massive return it gives you.
- NDepend. NDepend’s CQL rules lend themselves quite nicely to this task. They work in a similar manner to unit tests, but they are external to your code. Especially with very nice Visual Studio integration in version 3, it has very low friction, and provides immediate feedback. In addition they are pretty easy to read, lending themselves to the task of being executable documentation.
Krzysztof, what do you think about this kind of validation? It’s not strictly convention-testing, it’s more about IoC basics.
Thing that frightens me even more – conventions that are based on conventions. It’s like an invisible dependency tree, pure time killer when you need to mentally debug it down to root.
No problems if you know them all though.
when *you* define the conventions, and your code is checked against them that should not be a problem.
Whole Ruby on Rails framework is convention based and it seems to work pretty well, so I wouldn’t worry that much.
Just put meaningful exception messages in your ConventionExceptions
"Type Foo.Controllers.Bar is not a valid controller. Controller types must have ‘Controller’ suffix. Change the name to Foo.Controllers.BarController"