My work-in-progress semantic model based version of Fluent NHibernate makes heavy use of the visitor pattern. Currently, the visitor implementation is there to serve two purposes:
- Facilitate the construction of a Hbm* representation of the mapping model. Hbm* refers to the set of classes found in the NHibernate.Cfg.MappingSchema namespace that are generated from the NHibernate mapping schema. My version of Fluent NHibernate communicates with NHibernate by serializing the Hbm* representation to xml.
- Enable powerful, user-defined conventions that walk the mapping model and make changes. The NamingConvention class is a very simple example of this – it takes mapping model instances and sets their Name property based on reflected data such as the System.Type or the PropertyInfo.
Lets begin by looking at the implementation of AcceptVisitor for the root of the mapping model – HibernateMapping:
public override void AcceptVisitor(IMappingModelVisitor visitor) { visitor.ProcessHibernateMapping(this); foreach (var classMapping in Classes) visitor.Visit(classMapping); }
This is reasonably straightforward. The HibernateMapping tells the visitor to first process a HibernateMapping instance, and passes itself as the argument. Then it tells the visitor to visit each of the child classes. What the visitor does when its told to visit a ClassMapping is its business, but what it is likely to do is call AcceptVisitor on the ClassMapping:
public override void AcceptVisitor(IMappingModelVisitor visitor) { visitor.ProcessClass(this); if (Id != null) visitor.Visit(Id); if (Discriminator != null) visitor.Visit(Discriminator); foreach (var subclass in Subclasses) visitor.Visit(subclass); base.AcceptVisitor(visitor); }
This is similar to the previous AcceptVisitor implementation, but its worth noting that at the end, it calls base.AcceptVisitor(visitor). This is necessary because ClassMapping inherits from a common base class (JoinedSubclassMapping and SubclassMapping also inherit from this base class). Here is AcceptVisitor on ClassMappingBase:
public override void AcceptVisitor(IMappingModelVisitor visitor) { foreach (var collection in Collections) visitor.Visit(collection); foreach (var property in Properties) visitor.Visit(property); foreach (var reference in References) visitor.Visit(reference); }
Of course – all class mappings, regardless of how they fit into an inheritance hierarchy, can have collections, properties and references (many-to-ones). Its probably not necessary to follow this any further. The important point is that as long as the visitor calls AcceptVisitor when it is told to visit something, then it will make its way along the entire mapping model. To make life easier, I’ve implemented a DefaultMappingModelVisitor class that does precisely this. It has a whole bunch of code that all looks very similar to this:
public override void Visit(PropertyMapping propertyMapping) { propertyMapping.AcceptVisitor(this); } public override void Visit(ManyToOneMapping manyToOneMapping) { manyToOneMapping.AcceptVisitor(this); } public override void Visit(KeyMapping keyMapping) { keyMapping.AcceptVisitor(this); }
.. Many more Visit methods
Now you might be looking at this and wondering why this is necessary. Why can’t we skip this completely, and just have AcceptVisitor implementations that will call AcceptVisitor directly like this:
public override void AcceptVisitor(IMappingModelVisitor visitor) { visitor.ProcessHibernateMapping(this); foreach (var classMapping in Classes) classMapping.AcceptVisitor(visitor); <--- JUST DO THIS?? }
The answer is that the proposed change will work when you want one single visitor instance to visit the entire mapping model. While this works fine for conventions (as you will see in a moment), it does not work so well for building the Hbm representation. I’ll get to that in a second, but lets first take a look at the simpler case of the conventions. Here is a gutted version of the NamingConvention:
public class NamingConvention : DefaultMappingModelVisitor { public Func<MemberInfo, string> DetermineNameFromMember = info => info.Name; public Func<Type, string> DetermineNameFromType = type => type.AssemblyQualifiedName; public override void ProcessOneToMany(OneToManyMapping oneToManyMapping) { if (!oneToManyMapping.Attributes.IsSpecified(x => x.ClassName)) { if (oneToManyMapping.ChildType == null) throw new ConventionException("Cannot apply the naming convention. No type specified.", oneToManyMapping); oneToManyMapping.ClassName = DetermineNameFromType(oneToManyMapping.ChildType); } } }
I’ve removed the majority of its implementation for the sake of brevity. As it currently stands, it will walk the entire mapping model (because it inherits from the aforementioned DefaultMappingModelVisitor), and when it encounters a OneToManyMapping, it will attempt to set its ClassName based on the ChildType property. Its worth noting that the full implementation of the NamingConvention class actually handles naming of many other mappings types, such as ClassMappings, ManyToManyMappings, etc. This means that this visitor completely handles the concern of setting the name of mapping model elements for the entire mapping model. This point is important, because the next example is different. As I mentioned before, this visitor implementation would work fine with the previous simplification of having AcceptVisitor directly call AcceptVisitor on the children. Lets now move on to the process of building a Hbm* representation, and examine why the simplification won’t work so well for this case.
I define an interface for classes that build Hbm:
public interface IHbmWriter<T> { object Write(T mappingModel); }
Here is an example implementor, a hbm writer that will handle ColumnMappings:
public class HbmColumnWriter : NullMappingModelVisitor, IHbmWriter<ColumnMapping> { private HbmColumn _hbm; public object Write(ColumnMapping mappingModel) { _hbm = null; mappingModel.AcceptVisitor(this); return _hbm; } public override void ProcessColumn(ColumnMapping columnMapping) { _hbm = new HbmColumn(); _hbm.name = columnMapping.Name; if(columnMapping.Attributes.IsSpecified(x => x.IsNotNullable)) { _hbm.notnull = columnMapping.IsNotNullable; _hbm.notnullSpecified = true; } if (columnMapping.Attributes.IsSpecified(x => x.Length)) _hbm.length = columnMapping.Length.ToString();
//etc
} }
You’ll notice that this class inherits from NullMappingModelVisitor, which is basically a blank implementation of IMappingModelVisitor. It has all the methods, but none of them do anything. So this visitor ONLY knows how to handle ColumnMappings - if its passed to any other type of mapping, it will do nothing. This is certainly a difference approach to the NamingConvention, which actually knew how to set the name for Classes, OneToMany’s, ManyToMany’s and many other mapping model elements. So why does this difference exist? Basically, the job of generating all the Hbm that NHibernate requires is too big for one class. Creating separate classes for generating the appropriate HBM helps make the job more manageable. These classes can then be composed, like so:
public class HbmIdWriter : NullMappingModelVisitor, IHbmWriter<IdMapping> { private readonly IHbmWriter<ColumnMapping> _columnWriter; private readonly IHbmWriter<IdGeneratorMapping> _generatorWriter; private HbmId _hbm; public HbmIdWriter(IHbmWriter<ColumnMapping> columnWriter, IHbmWriter<IdGeneratorMapping> generatorWriter) { _columnWriter = columnWriter; _generatorWriter = generatorWriter; } public object Write(IdMapping mappingModel) { _hbm = null; mappingModel.AcceptVisitor(this); return _hbm; } public override void ProcessId(IdMapping idMapping) { _hbm = new HbmId(); if(idMapping.Attributes.IsSpecified(x => x.Name)) _hbm.name = idMapping.Name; } public override void Visit(ColumnMapping columnMapping) { var columnHbm = (HbmColumn) _columnWriter.Write(columnMapping); columnHbm.AddTo(ref _hbm.column); } public override void Visit(IdGeneratorMapping generatorMapping) { var generatorHbm = (HbmGenerator) _generatorWriter.Write(generatorMapping); _hbm.generator = generatorHbm; } }
This hbm writer handles IdMappings. IdMappings include one or more columns and a generator, so this writer composes a IHbmWriter<ColumnMapping> and a IHbmWriter<IdGeneratorMapping>. In this way, the task of creating the hbm representation can be managed by a family of visitors that delegate to each other as required.
Finally now, I can return to the previous point of why all the AcceptVisitor implementations call visitor.Visit(child) rather than child.AcceptVisitor(visitor). The former allows the current visitor to see that a different visitor is passed in when calling child.AcceptVisitor(). You can see some of this happening above, in the override for Visit(ColumnMapping) – it asks the IHbmWriter<ColumnMapping> to write the column. The implementation of that method will call columnMapping.AcceptVisitor(this). Thus, the Fluent NHibernate semantic model has an implementation of the visitor pattern that supports both single visitors that visit the entire graph themselves, and families of visitors that collaborate to get a large job done.