Reusable Domain Models. Interfaces and delegation.

XAF Team Blog
10 October 2006

This is the second post on this topic. We are trying to find a way to create reusable domain model libraries. We did not choose any particular route, I'm just thinking out loud. So here is one way of solving the problem - using interfaces and delegation for implementation.

Let me describe all the problems related to reusable domain models with one simple example:

In my project (code name Saturn), I need to implement tasks system. Tasks can be assigned to engineers and be related to some projects. And all domain classes in my project must be inherited from SaturnBase class. As a developer I don't really want to dig into task-management systems as well as person-related aspects of Engineer. At the same time I want to be able to change the default task behavior when I need to.

I would like to have a library of objects that implements all or most of the task and person management functionality for me.

So, to summarize the requirements:

  1. Task and Person implementations should be inherited from SaturnBase.
  2. Task should have associations with the Engineer and Project classes.
  3. Reusing Task and Person should add the necessary business logic to my classes.
  4. Task management code should be able to work with my task implementation.
  5. Everything should be explicit. If I use Task in my application I should have full control over it.

Now I'm wearing a library developer hat. How can I satisfy these requirements? The easiest one is #4. I have to write some code that deals with tasks and persons and in the same time I cannot work with certain classes. So I should use the ITask and IPerson interfaces:

    public interface IPerson {
        string FirstName { get; set; }
        string LastName { get; set; }
        DateTime BirthDate { get; set; }
        string DisplayName { get; }
    }

    public interface ITask {
        void MarkCompleted();
        string Subject { get; set; }
        string Description { get; set; }
        DateTime DueDate { get; set; }
        DateTime StartDate { get; set; }
        DateTime DateCompleted { get; }
        TaskStatus Status { get; set; }
        Int32 PercentCompleted { get; set; }
    }

With these interfaces I will be able to edit and show Task and Person objects, but I cannot retrieve them or create new. At some point I should know certain implementation class name. This information can be provided to my module instance in the configuration - I see no problem with that.

To satisfy requirement #3 I could inherit Task from some TaskBase that has the necessary logic implemented, but this will break requirement #1. So if I cannot use the inheritence I will try aggregation. I can implement ITask in some helper class and delegate all ITask calls to it:

    public class Task : SaturnBase, ITask {
        public Task(Session session) : base(session) { }

        #region ITask library implementation
        private ITask taskImpl = new TaskImpl();
        public void MarkCompleted() {
            taskImpl.MarkCompleted();
        }
        public string Subject {
            get { return taskImpl.Subject; }
            set { taskImpl.Subject = value; }
        }
        public string Description {
            get { return taskImpl.Description; }
            set { taskImpl.Description = value; }
        }
        public DateTime DueDate {
            get { return taskImpl.DueDate; }
            set { taskImpl.DueDate = value; }
        }
        public DateTime StartDate {
            get { return taskImpl.StartDate; }
            set { taskImpl.StartDate = value; }
        }
        public DateTime DateCompleted {
            get { return taskImpl.DateCompleted; }
        }
        public TaskStatus Status {
            get { return taskImpl.Status; }
            set { taskImpl.Status = value; }
        }
        public int PercentCompleted {
            get { return taskImpl.PercentCompleted; }
            set { taskImpl.PercentCompleted = value; }
        }

        #endregion
    }

The same with IPerson:

    public class Engineer : SaturnBase, IPerson {
        public Engineer(Session session) : base(session) { }

        #region IPerson library implementation
        private IPerson personImpl = new PersonImpl();
        public string FirstName {
            get { return personImpl.FirstName; }
            set { personImpl.FirstName = value; }
        }
        public string LastName {
            get { return personImpl.LastName; }
            set { personImpl.LastName = value; }
        }
        public DateTime BirthDate {
            get { return personImpl.BirthDate; }
            set { personImpl.BirthDate = value; }
        }
        public string DisplayName {
            get { return personImpl.DisplayName; }
        }
        #endregion
     }

This code is a dumb code, so ideally it should be generated automatically. With DXCore and partial classes it is possible to hide it from the active development reducing the cost of maintainence.

Now requirement #2. Task should have an association with Project and Engineer. The easiest solution is to leave this out of the library implementation:

    public class Task : SaturnBase, ITask {
        public Task(Session session) : base(session) { }

        #region ITask library implementation
	...
        #endregion
        [Association("Task-Engineer")] 
        public Engineer AssignedTo {
            get { return GetCollection("Engineer"); }
        }
        [Association("Task-Project")] 
        public Project AssignedTo {
            get { return GetCollection("Project"); }
        }
    }

But many domain models consist of more than one classes. Employment models describe the relationship between organization, organization units, person. Let's assume that our Task model library provides some functionality for other end of task association - I will call it TaskTarget:

    public interface ITask {
	...
        ITaskTarget AssignedTo { get; set; }
        ...
    }
    public interface ITaskTarget {
        IList Tasks { get; }
        bool HaveTasks(); 
    }

Now I see a serious problem with my library ITaskTarget implementation. It is in the nature of associations in persistent object models in ORM systems. In our example the collection of tasks is not something that can be stored in the TaskTargetImpl helper class - in fact this should be a result of sql query that selects all records in the Tasks table (where the actual class is mapped) where the Project foreign key is equal to the current Project key field. In short, it means for me that I cannot have the Tasks collection getter implemented in some helper class - only final persistent objects are able to build the collection for their association.

So - the developer will have to specify the necessary associations in his final classes and let the helper implementation know about it:

    public class TaskTargetImpl : ITaskTarget {
        ITaskTarget owner;
        public TaskTargetImpl(ITaskTarget owner) {
            this.owner = owner;
        }
        public IList Tasks {
            get { return owner.Tasks; }
        }

        public bool HaveTasks() {
            return Tasks.Count > 0;
        }
    }


    public class Engineer : SaturnBase, IPerson, ITaskTarget {
        public Engineer(Session session) : base(session) {
            taskTargetImpl = new TaskTargetImpl(this);
        }
        #region IPerson library implementation
	...
        #region ITaskTarget library implementation
        private TaskTargetImpl taskTargetImpl;
        [NonPersistent]
        IList ITaskTarget.Tasks {
            get { return new ListConverter(this.Tasks); }
        }
        [Association("Task-Engineer")]
        XPCollection Tasks {
            get { return GetCollection("Tasks"); }
        }
        public bool HaveTasks() {
            return taskTargetImpl.HaveTasks();
        }
        #endregion
    }

ListConverter here is a class that can make List from List (where Task is ITask) - the compiler cannot do this automatically. Note that TaskTargetImpl uses a real Tasks collection, not his own, so the owner class can change the default implementation but HaveTasks will still work correctly.

The described approach satisfies all listed requirements, but it does not look easy to me. I'm thinking about some DXCore plugin that can make addding these plumbing structures easier and that checks that all parts of puzzle are in place.

Also one thing that is left out in my samples is attributes. Real Task implementation needs additional attributes to tell XPO field lengths or that some properties are non-persistent or calculated. Since in my library there are no real classes, then the only place where I can write these attributes is the interface definition itself. Something like this:

    public interface ITask {
        void MarkCompleted();
        string Subject { get; set; }
        [Size(SizeAttribute.Unlimited)]
        string Description { get; set; }
        DateTime DueDate { get; set; }
        DateTime StartDate { get; set; }
        [Persistent]
        DateTime DateCompleted { get; }
        [Association("ITask-ITaskTarget")]
        ITaskTarget AssignedTo { get; set; }
        TaskStatus Status { get; set; }
        Int32 PercentCompleted { get; set; }
    }

Implementation generator can copy these attributes to its implementation methods. Or I can ask XPO developer to tweak it to understand attributes for the interfaces that persistent classes implement. Note that the Association attribute cannot be just copied as is, its name should be replaced with a real persistent classes combination.

Another thing I want to mention is that the approach described here is not really polymorphic. In other words, while several classes can implement ITask, there is no way here to work with them at the same time. For example I cannot easily get a collection of objects from the database that implements ITask as one list. To do so, XPO should be modified to be fully interface-aware and be able to generate sql union queries or create some denormalized tables for interfaces. But I think that in these rare cases when the developer needs different ITask implementations in the same list - he will be able to use the inheritence and implement ITask in the base class.

All this analysis hasn't led to a clear conclusion so far. We are still considering the best way to go and we want to invite everybody to a discussion. If you have other ideas, please feel free to let us know about them.

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
Tags
No Comments

Please login or register to post comments.