9
\$\begingroup\$

I'm going over the interview questions from "Cracking the Coding Interview" and one of the chapters (Chapter 7) deals with Object Oriented Design (OOD). The requirements for one of the problems are as follows:

Imagine you have a call center with three levels of employees: fresher, technical lead (TL), product manager (PM). There can be multiple employees, but only one TL or PM. An incoming telephone call must be allocated to a fresher who is free. If a fresher can’t handle the call, he or she must escalate the call to technical lead. If the TL is not free or not able to handle it, then the call should be escalated to PM. Design the classes and data structures for this problem. Implement a method getCallHandler().

I've written a solution in C#, it was not compiled (suggested by the author of the book) and it is currently not multithreaded. My solution is a bit long, but it should be really simple to understand.

Based on my understanding of the problem, I've established a "chain of command" where each employee knows who his/her superior is. Calls get handled in the following manner:

  • If a fresher is given a call which he/she cannot handle, then the fresher escalates the call to his/her superior: a tech lead.
  • If there are no free freshers, then the tech lead automatically gets the call.
  • If the tech lead is busy or cannot service the call, then he/she escalates the call to the product manager.
  • If the product manager (PM) is busy, then the call gets queued on the PM's queue and dequeued once their current call is serviced.

NOTE: multithreading will be added in a later revision, so please ignore any multithreading issues.

enum EStatus{ HANDLING, ESCALATED, QUEUED }
enum ERank{ FRESHER, TECH_LEAD, PROD_MANAGER }

abstract class Employee
{
    public ERank Rank{get; private set;}

    public Employee(ERank rank)
    {
        Rank = rank;
    }

    public abstract EStatus ServiceCall(Call call);
}

class Fresher:Employee
{
    private TechLead _superior;

    public Fresher(TechLead superior):base(ERank.FRESHER)
    {
        _superior = superior;
    }

    public override EStatus ServiceCall(Call call)
    {
        if(CanService(call))
        {
            call.Service(this);
            return EStatus.HANDLING;
        }
        else
        {
            _superior.ServiceCall(call);
            return EStatus.ESCALATED;
        }
    }

    private bool CanService(Call call)
    {
        // some logic to determine if the call can be serviced 
    }
}

class TechLead:Employee
{
    private Call _activeCall;
    private ProductManager _superior;

    public TechLead(ProductManager superior):base(ERank.TECH_LEAD)
    {
        _activeCall = null;
        _superior = superior;
    }

    public override EStatus ServiceCall(Call call)
    {
        if(_activeCall == null && CanService(call))
        {
            HandleCall(call);
            return EStatus.HANDLING;
        }
        else
        {
            _superior.ServiceCall(call);
            return EStatus.ESCALATED;
        }
    }

    private bool CanService(Call call)
    {
        // some logic to determine if the call can be serviced 
    }

    private HandleCall(Call call)
    {
        _activeCall = call;
        call.OnCallServiced += new Call.CallServicedDelegate(OnCallServiced);
        call.Service(this);
    }

    private void OnCallServiced(Call call)
    {
        if(call!=_activeCall)
        {
            // TODO: this call was never accepted
        }
        else
        {
            _activeCall = null;
        }
    }
}

class ProductManager:Employee
{
    private Call _activeCall;
    private Queue<Call> _callQueue;

    public ProductManager():base(ERank.PROD_MANAGER)
    {
        _activeCall = null;
        _callQueue = new Queue<Call>();
    }

    public override EStatus ServiceCall(Call call)
    {
        if(_activeCall == null)
        {
            HandleCall(call);
            return EStatus.HANDLING;
        }
        else
        {
            _callQueue.Enqueue(call);
            return EStatus.QUEUED;
        }
    }

    private HandleCall(Call call)
    {
        _activeCall = call;
        call.OnCallServiced += new Call.CallServicedDelegate(OnCallServiced);
        call.Service(this);
    }

    private void OnCallServiced(Call call)
    {
        if(call!=_activeCall)
        {
            // TODO: this call was never accepted
        }
        else
        {
            if(_callQueue.Count==0)
            {
                _activeCall = null;
            }
            else
            {
                HandleCall(_callQueue.Dequeue());
            }
        }
    }
}

class Call
{
    public delegate void CallServicedDelegate(Call call);
    public event CallServicedDelegate OnCallServiced;

    public Employee CallHandler{get; private set;}

    public Call()
    {
        CallHandler = null;
    }

    public void Service(Employee callHandler)
    {
        CallHandler = callHandler;

        // Invoke the notification when the call is complete
        OnCallServiced(this);
    }

    public void Disconnect()
    {
        // Disconnect the call
    }
}

class CallCenter
{
    private bool _running;
    private BlockingQueue<Call> _callQueue;
    private Queue<Employee> _freeFreshers;
    private List<Employee> _busyFreshers;
    private readonly TechLead _techLead;

    public CallCenter(int numEmployees)
    {
        // create the tech lead and a supervising product manager
        _techLead = TechLead(new ProductManager());

        _callQueue = new BlockingQueue<Call>();
        _freeFreshers = new Queue<Employee>();
        _busyFreshers = new List<Employee>();

        for(int i = 0; i < numEmployees; i++)
        {
            _freeFreshers.Enqueue(new Fresher(_techLead));
        }
    }

    public void AcceptCall(Call call)
    {
        _callQueue.Enqueue(call);
    }

    // Assuming that threading will be handled
    public void StartCallService()
    {
        _running = true;

        while(_running)
        {
            Call call = _callQueue.Dequeue();

            // Subscribe for the on call serviced delegate
            call.OnCallServiced += new Call.CallServicedDelegate(OnCallServiced);

            if(_freeFreshers.Count>0)
            {
                // Block until a fresher is available
                 Employee e = _freeFreshers.Dequeue();

                 // Assign the call to a free fresher
                switch(e.ServiceCall(call))
                {
                    case EStatus.HANDLING:
                        _busyFreshers.Add(e);
                        break;
                    case EStatus.ESCALATED:
                        _freeFreshers.Enqueue(e);
                        break;
                    case EStatus.QUEUED:
                    default:
                        break;
                }
            }
            else
            {
                _techLead.ServiceCall(call);
            }
        }
    }

    // Assuming that threading will be handled
    public void StopCallService()
    {
        _running = false;
    }

    private void OnCallServiced(Call call)
    {
        switch(call.CallHandler.Rank)
        {
            case ERank.FRESHER:
                _busyFreshers.Remove(call.CallHandler);
                _freeFreshers.Enqueue(call.CallHandler);
                break;
            case ERank.TECH_LEAD:
            case ERank.PROD_MANAGER:
            default:
                // nothing to do if it's a tech lead or prod manager
                break;
        }
    }
}

I was hoping to get some feedback since this question came from a book, there are many possible OOD solutions and I don't have an actual interviewer to comment on the solution. In addition, I do have the author's solution and I can post it if necessary.

Does this seem like a decent solution to the problem as stated? Is there something that I should definitely improve?

\$\endgroup\$
  • \$\begingroup\$ I don't see a getCallHandler... :) \$\endgroup\$ – Michael K Feb 10 '11 at 14:15
  • \$\begingroup\$ @Michael, it's a public property of Call: it's called CallHandler instead of GetCallHandler. \$\endgroup\$ – Kiril Feb 10 '11 at 18:39
9
\$\begingroup\$

In short, in my opinion, I think you've done too much.

It seems like your design has gone beyond a design for the initial interview question and into an exercise of your own.

However, I don't have the book you're reading, so you may be going off more than just that snippet in your question.

Based on the principle:

Do the simplest thing that could possibly work.

The basics I pulled from that question were:

  • We need a way to represent the three different kinds of employees.
  • We need a way of representing a call.
  • We need a way of choosing an employee to handle a call (the getCallHandler method).

i.e. I think the question is just after an implementation of GetCallHandler and some basic definitions of the types involved.

I came up with the following:

public class Call
{
    // TODO: Implement Call.
}

public enum EmployeePosition
{
    Fresher,
    TechnicalLead,
    ProductManager
}

public class CallCentre
{
    private IList<IEmployee> employees;

    public IEmployee GetCallHandler(Call call)
    {
        return employees.Where(e => e.CanHandle(call))
                        .OrderBy(e => e.Position)
                        .FirstOrDefault();
    }

    public CallCentre(IList<IEmployee> employees)
    {
        if (employees == null)
        {
            throw new ArgumentNullException("employees");
        }

        this.employees = employees;
    }
}

public interface IEmployee
{
    EmployeePosition Position { get; }
    Boolean CanHandle(Call call);
}

public class Fresher : IEmployee
{
    public EmployeePosition Position { get { return EmployeePosition.Fresher; } }

    public Boolean CanHandle(Call call)
    {
        // TODO: Logic for handling a call.
    }
}

// Similar implementations of IEmployee for TechnicalLead and ProductManager.

That would be the starting point based on what the question says. After this would usually be a period of interaction between interviewee and interviewer(s) in which assumptions could be addressed and the design discussed/updated with more detail, such as employees being in a busy state etc.

I think your design is a step beyond that.

The combination of class and enumeration for the different employee types is something we both went for. However, I chose an interface over a base class. I don't think your base class provides a lot of value - just code to assign a Rank property. My proposal has the benefit that someone's not going to break implementation for some employees inadvertantly by altering the base class. The interface contains no implmentation. Using an interface also allows the employee classes to use a different base class in the future if needs be.

One point for further discussion might be where it states:

If a fresher can’t handle the call, he or she must escalate the call to technical lead.

This may suggest that they want a fresher object to communicate directly with their superior like in your design (as opposed to my implementation, where employees don't need to know each other even exist - similar to many workplaces!). This could be clarified in the interview. However, I prefer my design because of the looser coupling between employees.

As far as multithreading goes, I'd expect quite a bit of discussion on

  • how it might be implemented
  • potential issues with different threading models
  • potential bottlenecks
  • typical call volume

before it got added to any code. But like I say, it does seem like your design is more than just an answer to the interview question.

\$\endgroup\$
  • \$\begingroup\$ On the point of "further discussion": it is only my interpretation that the fresher must escalate the call to a tech lead... so while I agree with the general idea that employees should not know about each-other, they should at least know who their supervisor is. I think I have some valid justification for my design, but Employee not being an interface is a very good point! \$\endgroup\$ – Kiril Feb 10 '11 at 22:31
  • \$\begingroup\$ Alex, I've also provided the author's solution as an answer for my question, I hope it's helpful to establish a frame of reference. \$\endgroup\$ – Kiril Feb 10 '11 at 22:33
  • \$\begingroup\$ @Alex, and the multithreading thing is not part of the question or the solution provided by the author, but I think it's inevitable with this type of system. I feel pretty comfortable with multithreading and my solution is pretty long as is, so I didn't bother to go any further. \$\endgroup\$ – Kiril Feb 10 '11 at 22:42
  • \$\begingroup\$ Finally, I think that going beyond the scope of the interview question is a bit excessive on my part, so that's definitely a huge issue that I need to think about! \$\endgroup\$ – Kiril Feb 10 '11 at 23:02
  • \$\begingroup\$ @Lirik - I think in summary, interview questions like this are intended to be leading. You can't ask a book questions or for clarification. I've done as much as I can above without that feedback. However, I understand why you've gone the extra mile here. Your writing the code you'd write after you'd discussed the requirements further. Just be aware that you're making assumptions - be careful of making up your own requirements in an actual interview. \$\endgroup\$ – Alex Humphrey Feb 11 '11 at 12:14
4
\$\begingroup\$

Sorry if there is a lack of pleasantries, but I am going to get down to it.

Source at time of review:

enum EStatus{ HANDLING, ESCALATED, QUEUED }
enum ERank{ FRESHER, TECH_LEAD, PROD_MANAGER }

abstract class Employee
{
    public ERank Rank{get; private set;}

    public Employee(ERank rank)
    {
        Rank = rank;
    }

    public abstract EStatus ServiceCall(Call call);
}

class Fresher:Employee
{
    private TechLead _superior;

    public Fresher(TechLead superior):base(ERank.FRESHER)
    {
        _superior = superior;
    }

    public override EStatus ServiceCall(Call call)
    {
        if(CanService(call))
        {
            call.Service(this);
            return EStatus.HANDLING;
        }
        else
        {
            _superior.ServiceCall(call);
            return EStatus.ESCALATED;
        }
    }

    private bool CanService(Call call)
    {
        // some logic to determine if the call can be serviced 
    }
}

class TechLead:Employee
{
    private Call _activeCall;
    private ProductManager _superior;

    public TechLead(ProductManager superior):base(ERank.TECH_LEAD)
    {
        _activeCall = null;
        _superior = superior;
    }

    public override EStatus ServiceCall(Call call)
    {
        if(_activeCall == null && CanService(call))
        {
            HandleCall(call);
            return EStatus.HANDLING;
        }
        else
        {
            _superior.ServiceCall(call);
            return EStatus.ESCALATED;
        }
    }

    private bool CanService(Call call)
    {
        // some logic to determine if the call can be serviced 
    }

    private HandleCall(Call call)
    {
        _activeCall = call;
        call.OnCallServiced += new Call.CallServicedDelegate(OnCallServiced);
        call.Service(this);
    }

    private void OnCallServiced(Call call)
    {
        if(call!=_activeCall)
        {
            // TODO: this call was never accepted
        }
        else
        {
            _activeCall = null;
        }
    }
}

class ProductManager:Employee
{
    private Call _activeCall;
    private Queue<Call> _callQueue;

    public ProductManager():base(ERank.PROD_MANAGER)
    {
        _activeCall = null;
        _callQueue = new Queue<Call>();
    }

    public override EStatus ServiceCall(Call call)
    {
        if(_activeCall == null)
        {
            HandleCall(call);
            return EStatus.HANDLING;
        }
        else
        {
            _callQueue.Enqueue(call);
            return EStatus.QUEUED;
        }
    }

    private HandleCall(Call call)
    {
        _activeCall = call;
        call.OnCallServiced += new Call.CallServicedDelegate(OnCallServiced);
        call.Service(this);
    }

    private void OnCallServiced(Call call)
    {
        if(call!=_activeCall)
        {
            // TODO: this call was never accepted
        }
        else
        {
            if(_callQueue.Count==0)
            {
                _activeCall = null;
            }
            else
            {
                HandleCall(_callQueue.Dequeue());
            }
        }
    }
}

class Call
{
    public delegate void CallServicedDelegate(Call call);
    public event CallServicedDelegate OnCallServiced;

    public Employee CallHandler{get; private set;}

    public Call()
    {
        CallHandler = null;
    }

    public void Service(Employee callHandler)
    {
        CallHandler = callHandler;

        // Invoke the notification when the call is complete
        OnCallServiced(this);
    }

    public void Disconnect()
    {
        // Disconnect the call
    }
}

class CallCenter
{
    private bool _running;
    private BlockingQueue<Call> _callQueue;
    private Queue<Employee> _freeFreshers;
    private List<Employee> _busyFreshers;
    private readonly TechLead _techLead;

    public CallCenter(int numEmployees)
    {
        // create the tech lead and a supervising product manager
        _techLead = TechLead(new ProductManager());

        _callQueue = new BlockingQueue<Call>();
        _freeFreshers = new Queue<Employee>();
        _busyFreshers = new List<Employee>();

        for(int i = 0; i < numEmployees; i++)
        {
            _freeFreshers.Enqueue(new Fresher(_techLead));
        }
    }

    public void AcceptCall(Call call)
    {
        _callQueue.Enqueue(call);
    }

    // Assuming that threading will be handled
    public void StartCallService()
    {
        _running = true;

        while(_running)
        {
            Call call = _callQueue.Dequeue();

            // Subscribe for the on call serviced delegate
            call.OnCallServiced += new Call.CallServicedDelegate(OnCallServiced);

            if(_freeFreshers.Count>0)
            {
                // Block until a fresher is available
                 Employee e = _freeFreshers.Dequeue();

                 // Assign the call to a free fresher
                switch(e.ServiceCall(call))
                {
                    case EStatus.HANDLING:
                        _busyFreshers.Add(e);
                        break;
                    case EStatus.ESCALATED:
                        _freeFreshers.Enqueue(e);
                        break;
                    case EStatus.QUEUED:
                    default:
                        break;
                }
            }
            else
            {
                _techLead.ServiceCall(call);
            }
        }
    }

    // Assuming that threading will be handled
    public void StopCallService()
    {
        _running = false;
    }

    private void OnCallServiced(Call call)
    {
        switch(call.CallHandler.Rank)
        {
            case ERank.FRESHER:
                _busyFreshers.Remove(call.CallHandler);
                _freeFreshers.Enqueue(call.CallHandler);
                break;
            case ERank.TECH_LEAD:
            case ERank.PROD_MANAGER:
            default:
                // nothing to do if it's a tech lead or prod manager
                break;
        }
    }
}

Observation on chain of command.

First thing to address is that "chain of command" behaves in a way other then what is specified in the problem. The problem states:

An incoming telephone call must be allocated to a fresher who is free.

The defined "chain of command" behavior...

If there are no free freshers, then the tech lead automatically gets the call.

...stands in opposition to that. Additionally, in the way the solution is implemented it then follows that if the tech lead is busy then the call gets bumped to product manager. If the product manager is busy then the call is placed in his personal call queue, to wait until he is free.

This behavior has the consequence of filling the PMs personal call queue with unscreened calls whenever all freshers and the tech is busy. This makes the PM responsible for screening all of those calls, even if at some point a fresher were to become free.

Suggested improvement.

Implement the base caller class in such a way that it have to it assigned a queue^ of calls to watch. This would allow for 2 things:

  1. All freshers could watch the same queue^ of new calls. When a new call is received, it then could be added to a common fresher queue^.
  2. The tech lead and project manager could each be assigned their own queues^ where escalated calls could wait for them to become available.

This allows for all new calls to wait for a fresher to be free, and for the TL and PM to only have calls queued to them that they need answer.

^Queues could be lists. Read on...

Observation on queuing behaviors.

In the implementation I counted 3 queues and a bonus list.

  1. _callQueue in CallCenter. Intended to contain new calls.
  2. _freeFreshers in CallCenter. Holds freshers without calls.
  3. _callQueue in ProductManager. Holds PMs calls or unserviced calls (see chain of command).
  4. _busyFreshers* in CallCenter. This is not really a queue but it holds freshers from the _freeFreshers queue when they are not free.

Q1:

In design, this queue holds the incoming calls. In practice, Q1 keeps the while(_running) loop in the StartCallService method from eating up as much cpu as possible. As soon as a call is added to Q1 it is promptly dequeued and handled by (A)a free caller, (B) the tech lead, (C) the project manager, or (D) thrown in the PMs queue. Nothing ever stays in Q1.

Q2 & Q4:

Q2 and Q4 are simply keeping track of the freshers busy state. Q4 only exists so that busy freshers are not lost? This also enforces a set behavior of choosing which free fresher takes a call.

Q3:

This queue behaves like a queue. Callers wait here for the attention of the project manager.

Suggested improvement.

I am struggling with what to write here because, minus a few design flaws in other places, these queues are perfectly functional. But, what really should be asked is, is the function of a queue really wanted?

In the case of Q2 & Q4, which are holding the free state and dictating who gets the next call. It makes more sense to add a IsBusy property to employee. That way CallCenter can simply have a _freshers list. To determine if a fresher is busy you just ask. To get a free fresher you are no longer stuck with whoever pops out of Q2. You may simply ask for the employee that fits the any criteria you like.

In the same way Q1 could be replaced by a list. When a new call comes in it is assigned to the best free fresher. If no fresher is available it is added to the unanswered calls list. When a fresher finishes a call a check to the unanswered calls list is made. This allows for the elimination of the while(_running) loop, and I love eliminating loops, really love, really.

Observation on use of enums

The defined enums...

enum EStatus{ HANDLING, ESCALATED, QUEUED }
enum ERank{ FRESHER, TECH_LEAD, PROD_MANAGER }

...really end up being just duplicated information. ERank can be determined by class, and EStatus can be determined by which queue or employee has the call.

Suggested improvement.

Just get rid of them.

OH, getCallHandler()

At this point this seems like a silly (and somewhat mysteriously defined) thing to implement. Maybe another user why this is a sensible thing to implement?

But, a requirement is a requirement. And, your Call.CallHandler, property has the right idea. I would just make it a method called getCallHandler() to play by the letter of the law, and null seems like a reasonable value to return if no one has handled the call yet.

As always, I too welcome comments or criticisms. Cheers!

\$\endgroup\$
  • \$\begingroup\$ I wish I had enough points to comment on some of the other solutions. :) There is some great input here. \$\endgroup\$ – jphofmann Feb 10 '11 at 23:45
  • \$\begingroup\$ VERY thorough review and it leaves me with very little to counter. I think the biggest issue here is that I've actually propagated the call up the chain-of-command if there is no free fresher. A call should only be propagated up if the fresher can't handle the call. I'm going to add another answer in which I update my solution based on your comments (of course I will not accept it). It seems that you've pointed out the most crucial issues here, thanks for the help! \$\endgroup\$ – Kiril Feb 10 '11 at 23:58
  • \$\begingroup\$ by the way, if you want to get 100 free points, just sign up for one of the other stack exchange sites and link your accounts. This will allow you to comment. \$\endgroup\$ – Kiril Feb 10 '11 at 23:59
  • \$\begingroup\$ @Lirik Sadly, I have linked the other stack exchange accounts I have and none of them must have enough points to award me a free 100. \$\endgroup\$ – jphofmann Feb 11 '11 at 0:31
  • \$\begingroup\$ there are a couple of things that I disagree with: rank allows us to specify what is the minimum rank level that is necessary to service a given call; after looking further into the queue system, it looks like the author's solution makes the most sense. Other than that, I've pretty much done everything that you suggested in your answer :) Thanks! \$\endgroup\$ – Kiril Feb 11 '11 at 2:04
0
\$\begingroup\$

It looks like it may be a good idea to provide the solution from the book just to establish a frame of reference:

public class CallHandler 
{
    static final int LEVELS = 3; // we have 3 levels of employees
    static final int NUM_FRESHERS = 5; // we have 5 freshers
    ArrayList<Employee>[] employeeLevels = new ArrayList[LEVELS];

    // queues for each call’s rank
    Queue<Call>[] callQueues = new LinkedList[LEVELS];

    public CallHandler() { ... }

    Employee getCallHandler(Call call) 
    {
        for (int level = call.rank; level < LEVELS - 1; level++) 
        {
            ArrayList<Employee> employeeLevel = employeeLevels[level];
            for (Employee emp : employeeLevel) 
            {
                if (emp.free) 
                {
                    return emp;
                }
            }
        }
        return null;
    }

    // routes the call to an available employee, or adds to a queue

    void dispatchCall(Call call) 
    {
        // try to route the call to an employee with minimal rank
        Employee emp = getCallHandler(call);
        if (emp != null) 
        {
            emp.ReceiveCall(call);
        } else {
            // place the call into queue according to its rank
            callQueues[call.rank].add(call);
        }
    }

    void getNextCall(Employee e) {...} // look for call for e’s rank
}

class Call 
{
    int rank = 0; // minimal rank of employee who can handle this call
    public void reply(String message) { ... }
    public void disconnect() { ... }
}

class Employee 
{
    CallHandler callHandler;
    int rank; // 0- fresher, 1 - technical lead, 2 - product manager
    boolean free;
    Employee(int rank) { this.rank = rank; }
    void ReceiveCall(Call call) { ... }
    void CallHandled(Call call) { ... } // call is complete
    void CannotHandle(Call call) 
    { // escalate call
        call.rank = rank + 1;
        callHandler.dispatchCall(call);
        free = true;
        callHandler.getNextCall(this); // look for waiting call
    }
}

class Fresher extends Employee 
{
    public Fresher() { super(0); }
}

class TechLead extends Employee 
{
    public TechLead() { super(1); }
}

class ProductManager extends Employee 
{
    public ProductManager() { super(2); }
}

The solution of the book is similar similar in design and extent... there is no additional information besides what is provided by the quote above. My evaluation is that:

  1. I did not actually make a method called GetCallHandler(), I converted it to a property of the Call.
  2. There might have been a specific reason why a GetCallHandler() method is needed, i.e. necessary for another component of the system.

I do have a justification for my design compared to the Author's design:

  1. It's more efficient to maintain a queue of free employees than to actually go through a list of employees and check which one is free every time a call comes through.
  2. The behavior in response to a call is defined by particular implementation of Employee, rather than an Employee "expert" (i.e. each employee knows what they're supposed to do, rather than the system telling each employee what to do).
  3. If a GetCallHandler method is needed in another system, then the question would have been more specific as to the signature of the method.
  4. Judging on the solution, GetCallHandler is only called by the CallHandler class itself, so that supports #3. Nobody outside of CallHandler seems to use it.
\$\endgroup\$
  • \$\begingroup\$ in addition, I'd like to add that my and the author's implementations of GetCallHandler would probably be particularly unfair on the first few fresher employees - the 5th fresher would only answer a call if the first four were busy! I think the first fresher would get sick of their job pretty quickly :) If you queue them like you did, they'll be allocated calls in a round robin fashion. \$\endgroup\$ – Alex Humphrey Feb 11 '11 at 12:25
  • \$\begingroup\$ @Alex yes, the author's approach is a bit biased towards the first freshers, but it does get a little better when the call volume increases. I really appreciate your input on all of this :), thanks! \$\endgroup\$ – Kiril Feb 11 '11 at 19:37
0
\$\begingroup\$

Here is a running revision of my solution (based on all the answers):

enum ERank{ FRESHER = 0, TECH_LEAD = 1, PROD_MANAGER = 2 }

abstract class Employee
{
    // Properties
    public ERank Rank{get; private set;}
    public bool Busy{get; priavte set;}

    // Fields
    protected static readonly CallService _callService = new CallService(SOME_GLOBAL_VALUE);

    // Constructors
    public Employee(ERank rank)
    {
        Rank = rank;
        Busy = false;
    }

    public void ServiceCall(Call call)
    {
        if(CanService(call))
        {
            Busy = true;
            call.Service(this);
        }
        else
        {
            // Only escalate if there is a superior rank available
            if(!Busy && (Rank != ERank.PROD_MANAGER))
            {
                call.Escalate();
            }

            // Put the call back onto the queue
            _callService.AcceptCall(call);
        }
    }

    public void OnCallServiced(Call call)
    {
        if(this != call.CallHandler)
        {
            // TODO: this call was never accepted
            // somebody is innapropriately calling this method
        }
        else
        {
            Call nextCall = _callService.GetNextCall();
            if(nextCall == null)
            {
                Busy = false;
            }
        }
    }

    protected abstract bool CanService(Call call);
}

class Fresher:Employee
{
    public Fresher():base(ERank.FRESHER){}
    private override bool CanService(Call call){ /*...*/ }
}

class TechLead:Employee
{    
    public TechLead():base(ERank.TECH_LEAD){}
    private override bool CanService(Call call){ /*...*/ }
}

class ProductManager:Employee
{    
    public ProductManager():base(ERank.PROD_MANAGER){}
    private override bool CanService(Call call){ /*...*/ }
}

class Call
{    
    public Employee CallHandler{get; private set;}
    public ERank AcceptableRank{get; private set;}

    public Call()
    {
        CallHandler = null;
        AcceptableRank = ERank.FRESHER;
    }

    public void Service(Employee callHandler)
    {
        CallHandler = callHandler;

        // notify the call handler when the call is serviced
        CallHandler.OnCallServiced(this);
    }

    public void Escalate()
    {
        if((in)AcceptableRank < (int)ERank.PROD_MANAGER)
        {
            AcceptableRank = ERank(((int)AcceptableRank)+1);
        }
    }
}

class CallCenter
{
    private Employee[][] _employees;
    private Queue<Call>[] _rankedCallQueue;

    public CallCenter(int numEmployees)
    {
        _prodManager = new ProductManager();
        _techLead = new TechLead();

        _rankedCallQueue = new Queue<Call>[((int)ERank.PROD_MANAGER)+1];
        _employees = new Employee[((int)ERank.PROD_MANAGER)+1][];

        for(int rank = 0; rank <= (int)ERank.PROD_MANAGER; rank++)
        {
            _rankedCallQueue[rank] = new Queue<Call>();
            switch((ERank)rank)
            {
                case ERank.Fresher:
                    _employees[rank] = new Employee[numEmployees];
                    for(int i = 0; i < numEmployees; i++)
                    {
                        _employees[rank][i] = new Fresher();
                    }
                    break;
                case ERank.TECH_LEAD:
                    _emlpoyees[rank] = new Employee[]{new TechLead()};
                    break;
                case ERank.PROD_MANAGER:
                    _employees[rank] = new Employee[]{new ProductManager()};
                    break;
                default:
                    break;
            }
        }
    }

    public Employee GetCallHandler(Call call)
    {
        int rank = (int)call.AcceptableRank;
        for(int i = 0; i < _employees[rank].Length; i++)
        {
            if(!_employees[rank][i].Busy)
            {
                return _employees[rank][i];
            }
        }
        return null;
    }

    public void AcceptCall(Call call)
    {
        Employee employe = GetCallHandler(call);
        if(employee == null)
        {
            _rankedQueue[(int)call.AcceptableRank].Enqueue(call);
        }
        else
        {
            employee.ServiceCall(call);
        }
    }

    public Call GetNextCall(ERank rank)
    {
        if(_callQueue[(int)rank].Count == 0)
        {
            return null;
        }
        else
        {
            return _callQueue[(int)rank].Dequeue();
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.