Saturday, January 30, 2010

No you really don't need that singleton (Db40 with StructureMap)

OK, really , StructureMap ROCKS.

So, we've managed to slip in StructureMap at work, and I decided to play a little bit at home to get better using it. Not that it's tough at all, I just wanted to get a little more experience under my belt so I could spend time at work doing work, not mucking with my IOC.

So, I've already blogged about the beauty of Db40. I'm thinking about starting some silly little side project, and I wanted to use db40 as an embedded db. So, db40 grabs a handle on the file and locks it. You must have some kinda singleton thing going on, since only one instance of IObjectServer can hit the file at a time.

The singleton pattern is the devils spawn. I despise it! What to do???First of all, I wrote a quick littler Db40Server object that just wraps up creating the object. It is NOT singleton.

using Db4objects.Db4o;

namespace Ellemy.Data
{
public class Db4oServer
{
private readonly string _pathToDbFile;
private readonly int _port;

public Db4oServer(string pathToDbFile,int port,bool autoStrart)
{
_pathToDbFile = pathToDbFile;
_port = port;
if(autoStrart)
Start();
}

/// <summary>
///
The server
/// </summary>
public IObjectServer Server{get; private set;}
/// <summary>
///
Starts the Server
/// </summary>
public virtual void Start()
{
Server = Db4oFactory.OpenServer(_pathToDbFile, _port);
}
/// <summary>
///
Stops the Server
/// </summary>
public virtual void Stop()
{
Server.Close();
}
public IObjectContainer GetClient()
{
return Server.OpenClient();
}
}
}



Simple enough right? Usually someone would ugly this up by adding singleton implementation right to the object (probably have to couple it up to some app settings to to get those ctor dependencies supplied as well. I didn't do that. I just made the functionality I needed. If we where using the Singleton pattern here, we'd do something like this.
Db40Server.Instance.GetClient()
but I didn't do that... I let StructureMap do that work.
Here's what I did.




namespace Dojo.Business.Bootstrapping
{
public class DojoBusinessRegistry:Registry
{
public DojoBusinessRegistry()
{
Scan(registry =>
{
registry.AssemblyContainingType(typeof (IPerson));
registry.WithDefaultConventions();
});

ForSingletonOf<Db4oServer>().Use<Db4oServer>()
.Ctor<String>("pathToDbFile").Is("Tests.bin") //.EqualToAppSetting("Db4oServerFile")
.Ctor<int>("port").Is(0)//.EqualToAppSetting("Db4oServerPort")
.Ctor<bool>().Is(true)
;

For<IObjectContainer>().Use(context => context.GetInstance<Db4oServer>().GetClient());
For<IObjectServer>().Use(context => context.GetInstance<Db4oServer>().Server);
}

}



What's going on here? First of all, the Scan stuff, just does uses naming conventions to figure out how to resolve dependencies.

No issues there.


But check out the ForSingletonOf line. It tells SM that anytime someone asks for a Db40Server, return the same instance. All the ugly singleton stuff is gone (or in the guts of SM somewhere). Having in SM guts is fine by me, because I can still test the heck outta my code, and inject dependencies as I normally would.
But does it work?


First I wrote this test.



[TestFixture]
public class Registry_Tests
{
[TestFixtureSetUp]
public void StartTest()
{
var container = new Container(registry =>
{
registry.AddRegistry(new DojoBusinessRegistry());

});
ServiceLocator.SetLocatorProvider(() => new StructureMapServiceLocator(container));
}
[Test]
public void Config_is_valid()
{
ObjectFactory.AssertConfigurationIsValid();
}



 



That passed fine. Now not because I don't think JM got it right, but because I'm not sure I know what the hell I'm doing, I wrote this test.




[Test]
public void Db4oServer_is_a_singleton_because_Structure_Map_kicks_ass()
{
var client1 = (IObjectContainer)ServiceLocator.Current.GetInstance(typeof (IObjectContainer));

var client2 = (IObjectContainer)ServiceLocator.Current.GetInstance(typeof(IObjectContainer));

var server = ServiceLocator.Current.GetInstance(typeof (IObjectServer));
var server2 = ServiceLocator.Current.GetInstance(typeof (IObjectServer));

server.ShouldEqual(server2);
}



Note that there's no Assertion on the client1, client2 stuff. The reason I did that was because if we got a new instance of Db40Server, we'd get an exception because the file would be locked by the first instance of Db4oServer when the 2nd one tried to open the file.



Then, just because I like wasting time, I checked that 2 instances of resolved IObjectServer are the same instance.



Anyway, in my humble opinion, this totally rocks... I can quit using that horrendous pattern but still control how instances are created. Thanks a lot Miller!



Comments welcome!

4 comments:

  1. Luiz Alberto ZaiatsFebruary 3, 2010 at 9:13 AM

    Hey! I really appreciated your point of view! I work with DB4O for about 2 years and the big problem i´ve found is when i use an IIS instance with more than 1 worker per Application. The file is locked by one thread and i get some errors sometimes... I think there is no way to solve this problem unless i create a simple .exe server that locks the file and open the DB4O server at some port and then use ASP.NET to connect in my server instance through sockets ;) Am i correct?

    ReplyDelete
  2. Really, all you'd need to do is on the Start method of Db40Server, just do a GrantAccess(username,password).

    Then (as long as your IIS app was running), you could connect with other apps using Db4oFactory.OpenClient(host,port,username,password)

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Nice post. Some information can also be found at

    http://tiny.cc/gr8xy

    ReplyDelete