Sunday, 12 December 2010

Turning DTOs into a domain layer

Following on from this blog: Do WCF Data Contracts have relevance outside of WCF where I talk about using WCF Data Contracts outside of service calls. This is part of a strategy for re-using DTOs for more than just a transport layer and building them for use across all layers.

Obviously you will instantly think about domain object behaviours, Object Oriented Design etc. In my experience you want to separate out the behaviour of a class in different layers. For example you will want to bind your View to a Model in the UI or use persistent objects in the Data layer, additionally you will want to have business behaviours in the domain layer. What if you could share the underlying shape of the data across all of these tiers.

In this article I will talk about reusing DTOs as a domain model. The obvious benefit is that you don't need to maintain 2 sets of objects and a mapping layer between them.

The main premise of this model is to have the DTO model representing the data shape of the domain object and adding behaviours through the use of extension methods to add this behaviour. For example you might have a Person DTO:


    [DataContract]
    public class Person
    {
        [DataMember(IsRequired=true)]
        public string Title { get; set; }

        [DataMember(IsRequired = true)]
        public string FirstName { get; set; }
       
        [DataMember(IsRequired = true)]
        public string LastName { get; set; }

        [DataMember(IsRequired = true)]
        public DateTime DateOfBirth { get; set; }

        [DataMember(IsRequired = true)]
        public Address Address { get; set; }

        [DataMember(IsRequired = true)]
        public PersonSex Sex { get; set; }

        [ContractInvariantMethod]
        private void EnforceInvariant()
        {
            Contract.Invariant(this.DateOfBirth < DateTime.Now);
            Contract.Invariant(!string.IsNullOrWhiteSpace(this.FirstName));
            Contract.Invariant(!string.IsNullOrWhiteSpace(this.LastName));
            Contract.Invariant(!string.IsNullOrWhiteSpace(this.LastName));
            Contract.Invariant(this.Address != null);

            Contract.Invariant(Enum.IsDefined(typeof(PersonSex), this.Sex));
        }
    }

Note that this uses the code contract model that I describe in Combing Code Contracts with DataContracts for better contract expressiveness and quality

To build a domain layer on top of this you can add a set of extension methods like this to add some business logic to the class:

    public static class PersonBusinessLayerExtensions
    {
        public static bool IsOver18(this Person person)
        {
            return (person.DateOfBirth <= DateTime.Now.AddYears(18));
        }

        public static bool LivesInUK(this Person person)
        {
            Contract.Requires(person.Address != null);

            return (string.Compare(person.Address.Country, "UK", StringComparison.OrdinalIgnoreCase) == 0);
        }

        public static bool CanVoteInUKElection(this Person person)
        {
            return (IsOver18(person) && LivesInUK(person));
        }
    }



You can see from this that we've added a bunch of methods which query some semantic value on top of the raw data, but you could easily add some modification of the state.

Remember that you can combine this with the Transaction Script pattern for longer business logic transactions spanning DTOs or other calls.

One of the elements that makes it powerful is that you can add polymorphic behaviour in different layers and protect the business logic from the presentation or database layers or change the behaviour. This is enforced by deploying the appropriate assembly in the layer.


    public static class PersonUIExtensions
    {
        public static string GetFullName(this Person person)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", person.Title, person.FirstName, person.LastName);
        }
    }


A potential disadvantage with this model is polymorphism of behaviours through abstract and virtual methods on the domain classes. However this can still be simulated via inheritance of the DTOs and extension methods on the base classes with extended behaviours on derived classes. This will require different names, but that can be a good thing, as sometimes polymorphism can be abused by overloading the behaviours in ways which aren't described by the method name.

Your thoughts on this approach?

No comments:

Post a comment