Friday, January 9, 2009

Because Status.Draft smells

Download the Complete Solution

So, at work I had this project where I auto saved a draft of a particular object. I tried to model it after the way gmail saves drafts, but was stuck with the domain model that I had. Wasn't allowed to change that... :( Well the object had an enum property called status, one of which was draft. As soon as I saw it, I started yelling about stick, but wasn't allowed to change it.

Why does it stink? Well, in the case of the object I was working on, there where DB FK constraints on another object that may be created at the same time, and the user might not have filled in the values for that object yet.... Yuck! Arguably, a "draft" is not yet the entity yet anyway. What's an Email without a to address? Is it still an Email? Definitely arguable, but I hope you at least see my point.

Anyway, so I wrote this...

DraftClassDiagram

The idea is you can do something like this...

var myObject = new DraftMePlease 
{Id = 1, Property1 = "Property"};
var myDraft = myObject.GetDraft();
persistanceMechanism.Save(myDraft);
 
 
Here's the first 2 unit tests.

[Test]
        public void draft_serialized_data_gets_populated()
        {
            var draftMeObject = DraftMePlease.GetObject();
            var draft = new Draft<DraftMePlease>(draftMeObject);
            Expect(draft.SerializedData, Is.Not.Null);
 
        }
 

[Test]
        public void serialized_data_returns_xml_that_can_be_deserialized()
        {
            var preserializedObject = DraftMePlease.GetObject();
            var draft = new Draft<DraftMePlease>(preserializedObject);
            var serializer = new XmlSerializer(typeof(DraftMePlease));
            var reader = new StringReader(draft.SerializedData);
            var deserializedDraft = (DraftMePlease) serializer.Deserialize(reader);
            Expect(preserializedObject, Is.EqualTo(deserializedDraft));
        }
 
 
DraftMePlease is just a silly class with 2 properties. The static method returns an instance of a prepopulated object so I can do assertions on it. 
 
The first test just makes sure the SerializedData gets populated. The second one makes sure that the data is actually usable.
 
 
 
Lets make it pass...
 

using System;
using System.IO;
using System.Xml.Serialization;
 
namespace Draft
{
    public class Draft<T>:where T : class, new()
    {
        private string serializedData;
        private readonly T draftObject;
        /// <summary>
        /// Default Constructor
        /// </summary>
        public Draft()
        {
        }
        /// <summary>
        /// Creates an new Draft of the object sent
        /// </summary>
        /// <param name="draftObject"></param>
        public Draft(T draftObject)
        {
            this.draftObject = draftObject;
        }
 
        /// <summary>
        /// The Id of the Owner of this Draft
        /// </summary>
        public int OwnerId
        {
            get; set;
        }
        /// <summary>
        /// This Drafts Id
        /// </summary>
        public int Id
        {
            get; set;
        }
        /// <summary>
        /// Returns the draft as a <see cref="T"/> 
        /// </summary>
        /// <returns></returns>
        public T GetObject()
        {
                   }
        /// <summary>
        /// The Serialized data for the draft.
        /// </summary>
        internal string SerializedData
        {
            get
            {
                if(String.IsNullOrEmpty(serializedData))
                {
                    var serializer = new XmlSerializer(typeof(T));
                    var writer = new StringWriter();
                    serializer.Serialize(writer, draftObject);
                    serializedData = writer.ToString();
                }
                return serializedData;
            }
            set
            {
                serializedData = value;
            }
        }
 
    }
}
 
And BAM... Unit test passes!
 
Simple enough? I chose XmlSerializer since it doesn't require dirtying up your domain with Serializable attributes.
 
Next, lets make GetObject() work...
 
 
 
 

 [Test]
        public void GetObject_returns_populated_object()
        {
            var preserializedObject = DraftMePlease.GetObject();
            var serializer = new XmlSerializer(typeof (DraftMePlease));
            var stringWriter = new StringWriter();
            serializer.Serialize(stringWriter, preserializedObject);
            var draft =
                new Draft<DraftMePlease>
                    {
                        SerializedData = stringWriter.ToString()
                    };
            var deserializedDraft = draft.GetObject();
            Expect(preserializedObject,Is.EqualTo(deserializedDraft));
 
 
        }
 
 
All we're doing is firing up an XmlSerializer in the unit test and making our own "draft" and comparing it to the one the Draft object returns. I made my DraftMePlease object override Equals so I could just do an Equals assertion on it (god I love Resharper!!). 
 
Let's make it pass...
 
 

 /// <summary>
        /// Returns the draft as a <see cref="T"/> 
        /// </summary>
        /// <returns></returns>
        public T GetObject()
        {
            var serializer = new XmlSerializer(typeof (T));
            var reader = new StringReader(SerializedData);
            return (T) serializer.Deserialize(reader);
        }
 
Easy enough.... This is why I love TDD!!!
 
So, I've now got a working object.. But decided I could make it more usable using an extension object. So I wrote this...
 
 

public static class ModelExtensions
    {
        /// <summary>
        /// Returns a Draft of the object
        /// </summary>
        /// <typeparam name="T">The type of the object</typeparam>
        /// <param name="o">the object to get the draft of</param>
        /// <returns></returns>
        public static Draft<T> GetDraft<T>(this T o) where T : class,new()
        {
            
        }
    }
 
Yep, it doesn't work yet, that's because I didn't write the unit test! Of course, code doesn't work if you don't have unit tests! 
 
 

[Test]
        public void GetDraft_returns_a_populated_draft_object()
        {
            var preserializedObject = DraftMePlease.GetObject();
            var draft = preserializedObject.GetDraft();
            var deserilizedDraft = draft.GetObject();
            Expect(preserializedObject,Is.EqualTo(deserilizedDraft));
        }
 
Ok, requirements clear... Now lets go fill in the method...
 
 

public static class ModelExtensions
    {
        /// <summary>
        /// Returns a Draft of the object
        /// </summary>
        /// <typeparam name="T">The type of the object</typeparam>
        /// <param name="o">the object to get the draft of</param>
        /// <returns></returns>
        public static Draft<T> GetDraft<T>(this T o) where T : class,IEntity,new()
        {
            return new Draft<T>(o);
        }
    }
 
Easy enough, huh?
 
Anyway, You can download the code here. Have fun with it. Maybe sometime soon I'll wire it up to NHibernate, or if someone want's to and give it to me, that would be nice :)
 
Have fun!

No comments:

Post a Comment