Friday, 10 April 2009

Convention over Configuration

In this post I would like to introduce design pattern which is particularly close to my heart - Convention over Configuration. What I like the most about this pattern is that it eliminates lots of monkey code which we have to write from time to time. Firstly let me explain to you what I mean by monkey code.

Monkey code

Typical case of monkey code can be found in old-fasihion NHibernate mappings:

   1:  <hibernate-mapping>
   2:    <class name="Product" table="tblProduct">
   3:      <id name="Id" type="Int32" column="ProductID">
   4:        <generator class="identity" />
   5:      </id>
   6:      <property name="ProductLine" type="String">
   7:        <column name="ProductLine" length="2" />
   8:      </property>
   9:      <property name="Class" type="String">
  10:        <column name="Class" length="2" />
  11:      </property>
  12:      <property name="Size" type="String">
  13:        <column name="Size" length="5" />
  14:      </property>
  15:      <property name="DaysToManufacture" type="Int32">
  16:        <column name="DaysToManufacture" not-null="true" />
  17:      </property>
  18:      <property name="ModifiedDate" type="DateTime">
  19:        <column name="ModifiedDate" not-null="true" />
  20:      </property>
  21:      <property name="ListPrice" type="Int32">
  22:        <column name="ListPrice" not-null="true" sql-type="money" />
  23:      </property>
  24:    </class>
  25:  </hibernate-mapping>

Even though in most cases column name and property name are the same, we still have to specify them. There is nothing really creative in this code. I think it's general problem for all sorts of mappings, it doesn't really matter if they are in a form of XML or a plain-old-CLR-objects - someone has to write this pesky piece of code. If you multiply time needed to map single column by number of columns and then by number of tables then you will get huge waste of time.

Conventions

I like Fluent NHibernate so much because it uses conventions, for instance I can say that all my tables have the same names as my classes and let fluent-nh map everything for me.

Within one application there might be number of aspects to which conventions can be applied. For instance, in fluent-nh it might be:

  • class name vs table name
  • property name vs column name
  • reference property name vs foreign key column name

Another project using conventions is ASP.NET MVC:

  • project structure - there are separate folders for Views, Controllers and Model
  • views placement - by default ASP.NET MVC framework will try to find view for given controller and action in folder like this: /Views/controllername/actionname.aspx.
    Action name equals view name.
As you can see based on ASP.NET MVC example, conventions are not just to eliminate monkey code, they can be also used to establish common standards for project structure, classes/properties naming etc.

Implementation

Lets try to put this high level ideas into concrete ... how Convention over Configuration can be implemented? I will try to present an algorithm based on fluent-nh and real life scenario where we have to map database tables to classes but all tables in database have "tbl" prefix. Of course we don't want to name our classes with this prefix, also we don't want to specify manually for each class that corresponding table's name is "tbl" + class name.

Normally with fluent-nh you would write your own convention implementing IClassConvention interface, then you would use FluentConfiguration class to "fluently" configure your database, add your mappings and also add your custom conventions. Having that you would call BuildSessionFactory() to get instance of properly configured NHibernate's SessionFactory. It's all straightforward but lets look under the hood to see what is going on behind the scene:
  1. In the first step fluent-nh loads classes responsible for "convention discovery". Each class from this group is responsible for single convention, this way it's really simple to add new additional conventions later.
    To find all "discovery" classes you can for instance use reflection to get all classes from a given namespace (flunet-nh's way) or all classes implementing some interface.
  2. Then it instantiates all mapping classes, custom conventions and default conventions. Default conventions will be used if there are no custom conventions. Be default class name will be equal to table name, we want to change it therefore we have to provide our own convention. It's important to remember that default convention shouldn't overwrite custom one.
  3. Next, it executes all "convention discovery" classes. Corresponding methods will get as a parameter all mappings and are responsible for finding and applying specific conventions.
This is high level algorithm which allows you easily add additional conventions. You can have set of default conventions, but also you can provide your custom one. In general it is important to remember that
  1. Any convention (default or custom) shouldn't overwrite explicit configuration!
  2. Default convention shouldn't overwrite custom conventions!

Code samples

All conventions have to implement this simple interface:


   1:  public interface IConvention<T> : IConvention
   2:  {
   3:      /// <summary>
   4:      /// Whether this convention will be applied to the target.
   5:      /// </summary>
   6:      /// <param name="target">Instace that could be supplied</param>
   7:      /// <returns>Apply on this target?</returns>
   8:      bool Accept(T target);
   9:  
  10:      /// <summary>
  11:      /// Apply changes to the target
  12:      /// </summary>
  13:      /// <param name="target">Instance to apply changes to</param>
  14:      void Apply(T target);
  15:  }

Accept() method allows you to have number of conventions of given type and use them based on your custom logic.

This is how "discovery" is implemented:

   1:  public void Apply(IEnumerable<IClassMap> classes)
   2:  {
   3:      var conventions = conventionFinder.Find<IClassConvention>();
   4:  
   5:      foreach (var classMap in classes)
   6:      {
   7:          foreach (var classConvention in conventions)
   8:          {
   9:              if (classConvention.Accept(classMap))
  10:                  classConvention.Apply(classMap);
  11:          }
  12:      }
  13:  }

ConventionFinder returns all objects implementing IClassConvention interface, then it applies this all conventions to all mappings.

Finally, our convention which will add "tbl" prefix for table name:


   1:  public class TableNameConvention : IClassConvention
   2:  {
   3:      public bool Accept(IClassMap target)
   4:      {
   5:          return string.IsNullOrEmpty(target.TableName);
   6:      }
   7:  
   8:      public void Apply(IClassMap target)
   9:      {
  10:          target.WithTable("tbl" + target.EntityType.Name);
  11:      }
  12:  }

And a few interesting pieces of code which I found in fluent-nh sources:


   1:  private void AddDefaultConventions()
   2:  {
   3:      foreach (var foundType in from type in typeof(PersistenceModel).Assembly.GetTypes()
   4:                                where type.Namespace == typeof(TableNameConvention).Namespace && !type.IsAbstract
   5:                                select type)
   6:      {
   7:          ConventionFinder.Add(foundType);
   8:      }
   9:  }

The above method finds all default conventions. Next method instantiates type using default constructor or the one which takes (in this case) ConventionFinder as a parameter:

   1:  private object Instantiate(Type type)
   2:  {
   3:      object instance = null;
   4:  
   5:      foreach (var constructor in type.GetConstructors())
   6:      {
   7:          if (IsFinderConstructor(constructor))
   8:              instance = constructor.Invoke(new[] { this });
   9:          else if (IsParameterlessConstructor(constructor))
  10:              instance = constructor.Invoke(new object[] { });
  11:      }
  12:  
  13:      return instance;
  14:  } 

I hope that this post gives you much better idea about Convention over Configuration in general but also explains how to implement this pattern in practice. I encourage you to give it a try instead of writing monkey code.

If you encountered different cases where CoC fits perfectly I would be very interested to hear about it, leave a comment or send me an email. Or maybe you in general disagree ... leave a comment ... it will be interesting to know your view on this.

1 comment:

Anonymous said...

I'm just at the beginning so only glanced on the article, but to my knowledge you don't need to specify the collumn name... (so no redundancy here)

cowgaR