Another time I start from :
“Program to an interface and not to an implementation”
I want have the same approach, I’m using for DAOs, Models, Presenters, Validation, and so on, for my domain.
The domain:
public interface IEntity<TIdentity>: IEquatable<IEntity<TIdentity>>
{
TIdentity Id { get; }
}
public interface IAnimal : IEntity<int>
{
string Description { get; set; }
}
public interface IReptile : IAnimal
{
float BodyTemperature { get; set; }
}
public interface IHuman : IAnimal
{
string Name { get; set; }
string NickName { get; set; }
DateTime Birthdate { get; set; }
}
public interface IFamily<T> : IEntity<int> where T : IAnimal
{
T Father { get; set; }
T Mother { get; set; }
ISet<T> Childs { get; set; }
}
Because I’m going to work with interfaces I will need a sort of factory to have transient-instances of my entities. For this example I’m going to use something simple and more “general purpose”; a class resolver:
public interface IClassResolver : IDisposable
{
T Resolve<T>() where T : class;
T Resolve<T>(string service) where T : class;
}
The responsibility of the IClassResolver implementor is return an instance for a given Type where the Type is an interface (well… in general is an interface). The concrete implementation of a IClassResolver will be injected using some IoC framework but for this post I will use a simple static exposer:
public class DI
{
private static IClassResolver resolver;
private DI() {}
public static IClassResolver Resolver
{
get
{
if (resolver == null)
{
throw new InvalidOperationException("Resolver was not initialized. Use StackResolver.");
}
return resolver;
}
}
public static void StackResolver(IClassResolver dependencyResolver)
{
resolver = dependencyResolver;
}
}
As you can see nothing so complicated.
Now I have all needed to write a test for my domain:
[Test]
public void DomainAbstraction()
{
using (ISession s = sessions.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
var rf = DI.Resolver.Resolve<IReptile>();
rf.Description = "Crocodile";
var rm = DI.Resolver.Resolve<IReptile>();
rm.Description = "Crocodile";
var rc1 = DI.Resolver.Resolve<IReptile>();
rc1.Description = "Crocodile";
var rc2 = DI.Resolver.Resolve<IReptile>();
rc2.Description = "Crocodile";
var rfamily = DI.Resolver.Resolve<IFamily<IReptile>>();
rfamily.Father = rf;
rfamily.Mother = rm;
rfamily.Childs = new HashedSet<IReptile> { rc1, rc2 };
s.Save(rfamily);
tx.Commit();
}
using (ISession s = sessions.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
var hf = DI.Resolver.Resolve<IHuman>();
hf.Description = "Flinstone";
hf.Name = "Fred";
var hm = DI.Resolver.Resolve<IHuman>();
hm.Description = "Flinstone";
hm.Name = "Wilma";
var hc1 = DI.Resolver.Resolve<IHuman>();
hc1.Description = "Flinstone";
hc1.Name = "Pebbles";
var hfamily = DI.Resolver.Resolve<IFamily<IHuman>>();
hfamily.Father = hf;
hfamily.Mother = hm;
hfamily.Childs = new HashedSet<IHuman> { hc1 };
s.Save(hfamily);
tx.Commit();
}
using (ISession s = sessions.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
var hf = s.CreateQuery("from HumanFamily").List<IFamily<IHuman>>();
Assert.That(hf.Count, Is.EqualTo(1));
Assert.That(hf[0].Father.Name, Is.EqualTo("Fred"));
Assert.That(hf[0].Mother.Name, Is.EqualTo("Wilma"));
Assert.That(hf[0].Childs.Count, Is.EqualTo(1));
var rf = s.CreateQuery("from ReptilesFamily").List<IFamily<IReptile>>();
Assert.That(rf.Count, Is.EqualTo(1));
Assert.That(rf[0].Childs.Count, Is.EqualTo(2));
tx.Commit();
}
using (ISession s = sessions.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
s.Delete("from HumanFamily");
s.Delete("from ReptilesFamily");
tx.Commit();
}
}
Note: s.Save(hfamily) <<=== there isn’t a string for the entity-name; now NH are supporting it.
As you can see the users of my domain (the test in this case), are working only using interfaces; there isn’t a reference to a concrete implementation of my domain. The concrete implementation of the domain is trivial and you can see it downloading the code. The main thing you will notice, in the implementation, is the absence of the virtual modifier.
Wiring…
To wire the interface, with its concrete implementation, I want use NHibernate. The mapping is similar to the previous:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="EntityNameInAction.Abstraction.Entities.Impl"
namespace="EntityNameInAction.Abstraction.Entities.Impl.Naturalness"
default-access="backfield">
<class name="MyAnimal" abstract="true"
proxy="EntityNameInAction.Abstraction.Entities.Naturalness.IAnimal, EntityNameInAction.Abstraction.Entities"
entity-name="Animal">
<id name="id" access="field">
<generator class="hilo"/>
</id>
<discriminator column="kind"/>
<property name="Description"/>
</class>
<subclass name="MyHuman"
proxy="EntityNameInAction.Abstraction.Entities.Naturalness.IHuman, EntityNameInAction.Abstraction.Entities"
extends="Animal"
entity-name="Human">
<property name="Name"/>
<property name="NickName"/>
<property name="Birthdate" type="Date"/>
</subclass>
<subclass name="MyReptile"
proxy="EntityNameInAction.Abstraction.Entities.Naturalness.IReptile, EntityNameInAction.Abstraction.Entities"
extends="Animal"
entity-name="Reptile">
<property name="BodyTemperature"/>
</subclass>
<class name="MyFamily`1[[EntityNameInAction.Abstraction.Entities.Naturalness.IReptile, EntityNameInAction.Abstraction.Entities]]"
proxy="EntityNameInAction.Abstraction.Entities.Naturalness.IFamily`1[[EntityNameInAction.Abstraction.Entities.Naturalness.IReptile, EntityNameInAction.Abstraction.Entities]], EntityNameInAction.Abstraction.Entities"
table="Families" discriminator-value="Reptile" where="familyKind = 'Reptile'"
entity-name="ReptilesFamily">
<id name="id" access="field">
<generator class="hilo"/>
</id>
<discriminator column="familyKind"/>
<many-to-one name="Father" cascade="all" entity-name="Reptile"/>
<many-to-one name="Mother" cascade="all" entity-name="Reptile"/>
<set name="Childs" generic="true" cascade="all">
<key column="familyId" />
<one-to-many entity-name="Reptile"/>
</set>
</class>
<class name="MyFamily`1[[EntityNameInAction.Abstraction.Entities.Naturalness.IHuman, EntityNameInAction.Abstraction.Entities]]"
proxy="EntityNameInAction.Abstraction.Entities.Naturalness.IFamily`1[[EntityNameInAction.Abstraction.Entities.Naturalness.IHuman, EntityNameInAction.Abstraction.Entities]], EntityNameInAction.Abstraction.Entities"
table="Families" discriminator-value="Human" where="familyKind = 'Human'"
entity-name="HumanFamily">
<id name="id" access="field">
<generator class="hilo"/>
</id>
<discriminator column="familyKind"/>
<many-to-one name="Father" cascade="all" entity-name="Human"/>
<many-to-one name="Mother" cascade="all" entity-name="Human"/>
<set name="Childs" generic="true" cascade="all">
<key column="familyId" />
<one-to-many entity-name="Human"/>
</set>
</class>
</hibernate-mapping>
Mapping highlight
- class/subclass name: are my concrete classes (implementors of domain)
- proxy : is the interface (the domain); using it as proxy I can avoid virtual methods in the implementation because the underlining Dynamic-Proxy will inherit from the interface. Using interface I have many others vantages but is to long explain each (only one for example: I can cast a proxy-instance to an interface)
- entity-name : is the name I will use for persistence and represent another abstraction-level. For persistence stuff I can use a “conceptual-name” of the entity without take care about its representation in C#. As you can see the entity-name are playing on each association/aggregation/extends; not the concrete class nor the interface.
- As in this post the domain is represented in two tables.
Class Resolver
In the implementation of IClassResolver I’m going to use the NHibernate’s mapping to wire the interface of the domain (ex: IHuman) to its concrete class (ex: MyHuman) trough the entity-name. Is it not clear ? ok perhaps the code will be more clear
public class NhEntityClassResolver : IClassResolver
{
private readonly Dictionary<Type, string> serviceToEntityName = new Dictionary<Type, string>();
public NhEntityClassResolver(ISessionFactoryImplementor factory)
{
if(factory == null)
{
throw new ArgumentNullException("factory");
}
Factory = factory;
InitializeTypedPersisters();
}
private void InitializeTypedPersisters()
{
foreach (var entityName in Factory.GetAllClassMetadata().Keys)
{
serviceToEntityName
.Add(Factory.GetEntityPersister(entityName)
.GetConcreteProxyClass(EntityMode.Poco), entityName);
}
}
public ISessionFactoryImplementor Factory { get; private set; }
#region Implementation of IDisposable
public void Dispose()
{
}
#endregion
#region Implementation of IClassResolver
public T Resolve<T>() where T : class
{
string entityName;
if(serviceToEntityName.TryGetValue(typeof(T), out entityName))
{
return Resolve<T>(entityName);
}
return null;
}
public T Resolve<T>(string service) where T: class
{
return Factory.GetEntityPersister(service).Instantiate(null, EntityMode.Poco) as T;
}
#endregion
}
The ISessionFactoryImplementor is one of the interfaces implemented by the NH SessionFactory. The method Resolve<T>(string) are using the parameter service as the entity-name. The hard-work is done by the method InitializeTypedPersisters; what I’m doing there is map each interface with its entity-name… nothing more.
Conclusions
“Program to an interface and not to an implementation” is really wonderful.
Do you really have some doubt about how NHibernate implements “Persistence ignorance” ?
Code available here.
Nice solution and very good article!!! Keep feeding us!!!
ReplyDeleteWhy doesn't nhibernate allow to define mapping directly for particular interface? As example we have next mapping definition:
ReplyDelete<class name="MyDomain.IAnimal, MyAssembly" table="ANIMALS" abstract="true">
<property name="Description"...
And this code:
class MyAnimal : IAnimal { ... }
IAnimal animal = new MyAnimal();
session.Load(animal, 12); // select ... from ANIMALS where ID = 12;
class AnotherAnimal : IAnimal { … }
session.Load(new AnotherAnimal(), 14); // select … from ANIMALS where ID = 14;
I want to say that it would be nice to have ability to define mapping abstractions.
In this scenario we have a constant mapping definition but free to use different classes which implements IAnimal interface.
Sorry for my english.
and what happen for HQL/Criteria ?
ReplyDeletefrom IAnimal
which will be the concrete class NH must create ?
offtopic: comments notification absence is so unusable.. )
ReplyDeleteIn this case I think you need create query like this „from MyAnimal“ or „from AnotherAnimal“ and nhibernate resolve type to IAnimal and create „select … from ANIMALS“ SQL query.
Attribute abstract="true" specify that you can't use IAnimal directly as EntityType. By the way this attribute („abstract“) already exists in mapping definition language so this can be attribute with another more appropriate name :)
I'm sorry, I think I found „subscribe for comments“ link :)
ReplyDeleteWhat I don't understand is how this:
ReplyDeletepublic interface IEntity etc.
{
TIdentity Id { get; }
}
works. There is no setter. When NHibernate (2.0.0 GA) tries to populate a proxy defined as an interface in any of my attempts (accessed via a property) it says, in effect, "There is no setter".
I know there is no setter, but the example from Fabio doesn't have one either. Is this a mistake in the example? How is this done?
Anonymous: As you can see identifier mapped to the FIELD named «id», so IEntity interface is your logical feature and (as I understand) don't affect to the mapping definition.
ReplyDeleteHi,
ReplyDeletevery interesting things, but how can I use class mapping attributes like "extends" if the NH 2.1.0 doesn't support it? let me know how, and then, is there some updated documentation about NH 2.1.0??
For support request please use one of NHibernate forums.
ReplyDeleteBTW NH still support "extends" tag.
So you are saing that this feature represented by the attribute "extends" is supported by NH2.1.0 but not yet in the schema??
ReplyDeleteNo; I'm saying that nobody had removed "extends" at it stay there from NH0.x.y and the trunk have a lot of tests using "extends" in the mapping, so I don't know what you are talking about.
ReplyDeleteI'm really sorry I was in error..sorry
ReplyDeleteNo problem.
ReplyDelete