views:

66

answers:

3

Base class is Task. There are several derived classes such as PhoneCall, Fax, Email.... The framework is .NET 3.5 and the language is C#

In our application, we want to create some automatic tasks based on certain rules for a customer. For eg. if the customer has been signed up for 30 days, a task will get created by the rules engine.

The owner of the task should then be able to convert this task into a PhoneCall, Fax..... based on the scenario. Also, another requirement will be to convert a PhoneCall to Fax or Email or vice versa.

1) Should there be a converted class that should facilitate this conversion or each business object should allow methods to perform conversion?

2) If there are any design patterns or guidance someone can provide, that will be great.

Pratik

+2  A: 

Inheritance is not necessarily the best way to model problems where instances of types can change over time.

You may want to consider using composition instead. Something like:

class Task
{
    private TaskDetail m_Detail;

    public TaskDetail Detail { get { return m_Detail; } }
}

abstract class TaskDetail { ... }

class PhoneCallDetail : TaskDetail { ... }
class FaxDetail : TaskDetail { ... }
class EmailDetail : TaskDetail { ... }

Tasks would not change when their task details shift from one type to another. You would also need to implement some utility code to convert between the different task types, as appropriate.

So example use might look like:

Task theTask = new Task( ... );
theTask.ConvertToEmail(); // internally establishes this as an email task

EmailDetail detail = (EmailDetail)theTask.Detail;
detail.EmailAddress = "[email protected]";

theTask.ConvertToFax();   // may transfer or lose some detail...
FaxDetail faxDetail = (FaxDetail)theTask.Detail;
faxDetail.FaxDate = DateTime.Now;

// and so on.

The primary disadvantage to the approach above is that consumers of the Task class must use runtime checks to determine the type of detail associated with the task before operating on it; which also then requires casting of the detail property everywhere:

Task someTask = ...;
if( someTask.Detail is EmailDetail )
{ 
    EmailDetail detail = (EmailDetail)someTask.Detail;
    /* operate on email detail ... */
}
else if( someTask.Detail is FaxDetail )
{
    FaxDetail detail = (FaxDetail)someTask.Detail;
    /* operate on fax detail ... */
}

As the number of different subtypes grows, this approach becomes harder to maintain and evolve. If the number of subtypes is small, and likely to be stable over time, then it may be a reasonable choice.

In general, it's difficult to model situations like these - and you often have to compromise based on what persistence provider you use, how many different detail types they are, and what use cases you intend to support involving conversions from one detail type to another.

Another design approach that is often employed in such cases is Key-Value-Coding. This approch uses a dictionary of keys/values to model the various data elements of different kinds of details. This allows details to be very flexible, at the cost of less compile-time safety. I try to avoid this approach when possible, but sometimes it does model certain problem domains better.

It's actually possible to combine key-value coding with a more strongly typed approach. This allows details to expose their properties (usually for read-only purposes) without requiring the caller to perform runtime checks or casts:

abstract class TaskDetail
{
    public abstract object this[string key] { get; }
}

public class FaxDetail : TaskDetail
{
    public string FaxNumber { get; set; }
    public DateTime DateSent  { get; set; }

    public override object this[string key]
    {
        get
        {
            switch( key )
            {
                case "FaxNumber": return FaxNumber;
                case "DateSent":  return DateSent;
                default:          return null;
            }
        }
    }
}

public class EmailDetail : TaskDetail
{
    public string EmailAddress { get; set; }
    public DateTime DateSent { get; set; }

    public override object this[string key]
    {
       get
       {
           switch( key )
           {
               case "EmailAddress": return EmailAddress;
               case "DateSent":     return DateSent;
               default:             return null;
           } 
       }
    }
}

// now we can operate against TaskDetails using a KVC approach:
Task someTask;
object dateSent = someTask.Detail["DateSent"]; // both fax/email have a DateSent
if( dateSent != null )
   // ...
LBushkin
A: 

1) to create such tasks you may use factory method

2) I don't think it would be good idea that owner has to convert task to subclasses. may be that Strategy or State petterns would help there.

Arseny
A: 

In your particular situation, types aren't the way to go because you have not defined the behavior of a PhoneCall, a Fax or an Email beyond that of Task.

If the only thing you will do is store a phone number in a PhoneCall object, you've already broken encapsulation. The PhoneNumber property should be on the Customer object, as should e-mail address, mailing address, fax number and so on.

You can happily use a TaskType enum as a property of Task up until the point that Task changes its behavior based on the current task type. This is the point you will need to worry about using type conversion the way LBushkin has described.

I can see you in a situation where you are using different presentation logic for each task. In this case you would use the Decorator pattern.

somori