这就是我的要求(摘自本书:破解编码面试)


想象一下,您有一个呼叫中心,该呼叫中心有三级员工:新员工,技术主管(TL)和产品经理(PM)。可以有多个员工,但只有一个TL或PM。来电必须分配给免费的新生。如果新生无法处理该电话,则他或她必须将电话升级为技术主管。如果TL不是免费的或无法处理,则应将呼叫升级为PM。设计此问题的类和数据结构。实现方法getCallHandler()


这是我的实现:

public interface CallAllocator {
    public Employee getCallHandler() throws NoEmployeeInTheHouseException;
    void setTL(TechnicalLead technicalLead);
    void setPM(ProductManager productManager);
    void addFresher(Fresher fresher);
}


接口的实现:

public class CallAllocatorImpl implements CallAllocator {

    private TechnicalLead technicalLead;
    private ProductManager productManager;
    private List<Fresher> freshers = new ArrayList<Fresher>();

    @Override
    public Employee getCallHandler() throws NoEmployeeInTheHouseException {

        if (freshers.isEmpty() && technicalLead == null && productManager == null) {
            throw new NoEmployeeInTheHouseException();
        }

        if (!freshers.isEmpty()) {
            Employee fresher = freshers.get(new Random().nextInt(freshers.size()));
            if (fresher.getCanHandle()) {
                return fresher;
            }
        }

        if (technicalLead != null && technicalLead.getCanHandle()) {
            return technicalLead;
        }

        if (productManager != null && productManager.getCanHandle()) {
            return productManager;
        }

        throw new NoEmployeeInTheHouseException();

    }
    @Override
    public void setTL(TechnicalLead technicalLead) {
        this.technicalLead = technicalLead;
    }
    @Override
    public void setPM(ProductManager productManager) {
        this.productManager = productManager;
    }
    @Override
    public void addFresher(Fresher fresher) {
        if (fresher.isFree()) {
            freshers.add(fresher);
        }
    }
}


员工类别:

public class Employee {
    private boolean free;
    private boolean canHandle;
    public boolean isFree() {
        return free;
    }
    public void setFree(boolean free) {
        this.free = free;
    }
    public boolean getCanHandle() {
        return canHandle;
    }
    public void setCanHandle(boolean canHandle) {
        this.canHandle = canHandle;
    }
}


我有3个类别,名称分别为Fresher,TechnicalLead和ProductManager。它们都扩展了Employee,但没有覆盖任何方法或任何内容。.

这是我的TestClass:

public class TestClass {

    public static void main(String[] args) throws NoEmployeeInTheHouseException {

        CallAllocator callAllocator = new CallAllocatorImpl();

        Fresher fresherOne = new Fresher();
        fresherOne.setCanHandle(false);
        fresherOne.setFree(true);

        Fresher fresherTwo = new Fresher();
        fresherTwo.setCanHandle(true);
        fresherTwo.setFree(true);

        Fresher fresherThree = new Fresher();
        fresherThree.setCanHandle(false);
        fresherThree.setFree(true);

        Fresher fresherFour = new Fresher();
        fresherFour.setCanHandle(false);
        fresherFour.setFree(false);

        callAllocator.addFresher(fresherOne);
        callAllocator.addFresher(fresherTwo);
        callAllocator.addFresher(fresherThree);
        callAllocator.addFresher(fresherFour);

        TechnicalLead technicalLead = new TechnicalLead();
        technicalLead.setCanHandle(false);
        technicalLead.setFree(true);

        callAllocator.setTL(technicalLead);

        ProductManager productManager = new ProductManager();
        productManager.setCanHandle(true);
        productManager.setFree(true);

        callAllocator.setPM(productManager);

        Employee callHandler = callAllocator.getCallHandler();
        System.out.println(callHandler.getClass().getSimpleName());
    }
}


那我该如何改进这个代码?有什么建议?

#1 楼

首先,我不确定您是在实际编写需求时就实施它。描述说:

来电必须分配给有空的新人。如果新生无法处理呼叫,则他或她必须升级为技术负责人。

这听起来像如果没有免费的新生,则根本不应该处理呼叫(抛出异常) ?),而不是跳到TL。这种做法在现实世界中是有道理的:如果没有可用的更新,呼叫者最好稍后再回叫,而不是浪费技术主管的时间来呼叫应该能够更新的处理。例如,无论调用什么方法,都可以计划捕获该异常并执行addCallToUnhandledCallQueue()或其他任何操作。

这还可以解决问题的实质,我相信这是试图让您回答的问题责任链模式。在这种模式下,负责处理命令(在这种情况下为调用)的每个对象(在这种情况下为Employee)都包含逻辑,以检查它是否能够处理给定的命令,如果不能,则还知道链中的下一个对象可以调用。

这种模式的一个好处就是坚持了开/关原则。正如您将在下面看到的那样,执行诸如添加新员工类型或稍微更改结构之类的操作不太可能需要您在if{...} else{...}中摆弄getCallHandler()逻辑。此外,这意味着Employee只需了解其直接上司,而不是某些大师班必须知道并保持整个员工结构(这很快就会变得不愉快,尤其是在您需要添加其他方法也需要了解这一点的情况下)结构)。

考虑到这显然是作为采访问题来解决的,因此,一个元收益是,如果有人在采访中问我这个问题,我很确定他们会希望我谈论这种模式,即使无论您最终认为有更好的解决方案的原因是什么,理解这一点很重要,只要能够智能地描述为什么拒绝它即可。

因此,使用此模式,您的getCallHandler(Call call)方法将看起来像这样:

Employee fresher = getAnyFreeFresher(); //Should throw if there are none
return fresher.handle(call);


然后Employee类看起来像:

public class Employee{
    private Employee boss;

    private bool canHandle(Call call){
    //...
    }

    public Employee handle(Call call){
        if(canHandle(call)){
            return this;
        }
        if(boss == null){
            //Nobody in the chain could handle, throw
        }
        return boss.handle(call);
    }
}


这是一个非常粗糙的轮廓,关于老板的设置方式,有很多细节需要填写,您可能希望员工类型从Employee继承以实现canHandle等。

评论


\ $ \ begingroup \ $
感谢您的回答,我明白您的意思。也许您是对的,也许我完全错了要求。 :)
\ $ \ endgroup \ $
–科雷·图吉(Koray Tugay)
14年2月28日在19:32

\ $ \ begingroup \ $
关于要点的绝对协议:您没有解决问题,您正在接受采访。您的首要任务是不了解问题;是要在面试官的眼中了解问题。
\ $ \ endgroup \ $
–vals
14年2月28日在22:00

#2 楼

总体来说,代码很容易阅读。

一些比较挑剔的小项。

请不要让我们使用booleans,而要使用enums



public enum EmployeeStatus {
    OnCall,
    Available        
}


随着需求的变化,这允许您添加更多状态:

public enum EmployeeStatus {
    OnCall,
    Available,
    OutToLunch,
    OnVacation,
    OnBreak
}


我不喜欢getCallHandler()可以填充新鲜水果。我会注入列表,或者注入一家能够做到这一点的工厂。这将使代码解耦,并在将来使单元测试变得更加容易。

我也不喜欢set方法。使用继承应该消除对继承的需求。诚然,我现在看不到解决方案,但是这里有解决方案。基本上,通过使用不同的方法来处理不同的实例,您将自己与这三种类型联系在一起。如果添加第四个类型,例如ProductExpert,会发生什么?您现在必须更改此类以对其进行处理。

canHandle方法似乎应该基于状态枚举进行计算。这样,您只需要在类中设置一个标志,而不是两个即可。

public class Employee {

    // code

    public boolean isFree() {
        return status == EmployeeStatus.Available;
    }

    // code
}

Fresher fresherTwo = new Fresher();
fresherTwo.setStatus(EmployeeStatus.Available);


我也不喜欢名称'NoEmployeeInTheHouseException',我发现它有点因果关系。我会做“ NoEmployeeAvailableException”行之类的事情。

这是一个好的开始,我很高兴您关心这些东西。继续努力。

评论


\ $ \ begingroup \ $
你是什么意思,我不喜欢getCallHandler()可以填充新生。 ?此方法不填充新生吗?
\ $ \ endgroup \ $
–科雷·图吉(Koray Tugay)
2014年2月28日在19:22

\ $ \ begingroup \ $
我认为当我上课时可能会冒充自己。最常用的类比是,您不希望汽车知道如何制造自身,而是希望使用其他机器将其组装起来……
\ $ \ endgroup \ $
– Jeff Vanzella
14年2月28日在19:31

#3 楼

首先,我将对您的实现进行评论,然后在最后提出我的版本。

从顶部到底部,我们从这里开始:

public interface CallAllocator {
    public Employee getCallHandler() throws NoEmployeeInTheHouseException;
    void setTL(TechnicalLead technicalLead);
    void setPM(ProductManager productManager);
    void addFresher(Fresher fresher);
}



“雇员”对于处理呼叫的人来说太笼统了。我会称之为ICallHandler。实际的员工类别与处理调用的讨论无关,它们不应成为模型设计的一部分。
setTLsetPMaddFresher方法都使用过于实现的术语和参数。接口定义应尽可能抽象。

下一个:

if (freshers.isEmpty() && technicalLead == null && productManager == null) {
    throw new NoEmployeeInTheHouseException();
}


我看到在您的实现中,您可以处理Fresher /线索/经理为空或为空。这是状态管理,而不是建模。最好使用显式接口方法来捕获模型是否可用的概念。

getCanHandle确实是尴尬的是,canHandle会更自然
获取下一个可用更新的概念应有其自己的方法:您可以考虑不同的实现方式,例如随机选择任何免费的更新,或者选择最少的,或者选择最多的

好吧,这是我的描述建模非常准确的解决方案:

if (!freshers.isEmpty()) {
    Employee fresher = freshers.get(new Random().nextInt(freshers.size()));
    if (fresher.getCanHandle()) {
        return fresher;
    }
}


说明中的某些部分,而未定义的部分则有意未实现,例如:


如果有很多免费的新生,该选哪个? ->随便实现
如果没有免费的新生,该怎么办? -> null表示没有人要处理,尽管我管理我对此部分不太喜欢

其他要注意的事项:


接口是简短地说,只有吸气剂,没有变数
该类的成员都是最终的,并且该类在构造时已完全定义,没有任何猜测的空间了就处理呼叫而言,技术主管和产品经理就像呼叫者一样,都是呼叫处理程序,因此使用接口很有意义,无需为它们提供专用的类

当然,这是不可扩展的。该说明本身通过指定一个技术负责人和一个产品经理来排除可伸缩性。我将通过将多个级别建模为呼叫中心链来解决此问题:

interface ITicket {}

interface ICallHandler {
    boolean isAvailable();
    boolean canHandle(ITicket ticket);
}

interface ICallHandlerPicker {
    ICallHandler getAvailableCallHandler();
}

interface ICallCenter {
    ICallHandler getCallHandler(ITicket ticket);
}

class SingleLeadSingleManagerCallCenter implements ICallCenter {
    private final ICallHandlerPicker picker;
    private final ICallHandler lead;
    private final ICallHandler manager;

    SingleLeadSingleManagerCallCenter(ICallHandlerPicker picker, ICallHandler lead, ICallHandler manager) {
        this.picker = picker;
        this.lead = lead;
        this.manager = manager;
    }

    @Override
    public ICallHandler getCallHandler(ITicket ticket) {
        ICallHandler handler = picker.getAvailableCallHandler();
        if (handler == null) {
            // nobody available. perhaps throw new NoSuchElementException() ?
            return null;
        }
        if (handler.canHandle(ticket)) {
            return handler;
        }
        if (lead.isAvailable() && lead.canHandle(ticket)) {
            return lead;
        }
        return manager;
    }
}


然后,我们可以根据这些更具扩展性的方式在说明中实现呼叫中心类别为:

class MultiLevelCallCenter implements ICallCenter {
    private final ICallHandlerPicker picker;
    private final ICallCenter nextCallCenter;

    MultiLevelCallCenter(ICallHandlerPicker picker, ICallCenter nextCallCenter) {
        this.picker = picker;
        this.nextCallCenter = nextCallCenter;
    }

    @Override
    public ICallHandler getCallHandler(ITicket ticket) {
        ICallHandler handler = picker.getAvailableCallHandler();
        if (handler == null) {
            // nobody available. perhaps throw new NoSuchElementException() ?
            return null;
        }
        if (handler.canHandle(ticket)) {
            return handler;
        }
        return nextCallCenter.getCallHandler(ticket);
    }
}

class UltimateCallCenter implements ICallCenter {
    private final ICallHandler handler;

    UltimateCallCenter(ICallHandler handler) {
        this.handler = handler;
    }

    @Override
    public ICallHandler getCallHandler(ITicket ticket) {
        return handler.isAvailable() ? handler : null;
    }
}


再次,我省略了新生的细节以及如何挑选它们。这些没有在描述中指定,并且没有真正的意义。这使您可以随意注入所需的任何实现。

#4 楼

命名问题



CallAllocatorImpl有一组更新,TL和PM。因此,我认为将其命名为OfficeCallCenter似乎可行。

getCanHandle具有误导性。您通常不会看到方法名称同时具有getcan。其次要处理什么?呼叫。因此canHandleCall似乎是合理的。
setCanHandle也是如此。


正确回答问题


可以有多个员工,但仅一个TL或PM


闻到单调的作品。

所以setTL需要更多的LOC

public void setTL(TechnicalLead technicalLead) {
    if(technicalLead == null) {
        this.technicalLead = technicalLead;
    }
    else {
        throw new TLAlreadyExistsException("Only one Team-Leader allowed").
    }
}


setPM也是如此。

评论


\ $ \ begingroup \ $
好吧,有两点分歧。首先,即使添加了“ ish”,我也不会使用“单身”一词。单例模式是一件事,不是。其次,该例外在那里毫无意义。如果设置的东西已经存在,则将其替换,而不是尝试添加另一个。您实际上建议的是不变性,这与仅拥有一个不变性几乎没有关系。仅拥有一个TechnicalLead而不是一个集合就足够了。
\ $ \ endgroup \ $
–本·亚伦森
2014-2-28在23:07

\ $ \ begingroup \ $
CallAllocatorImpl是CallAllocator的实现,因此,如果您更改实现的名称,则也需要更改接口的名称。
\ $ \ endgroup \ $
–马克·安德烈(Marc-Andre)
2014年3月1日在1:49

#5 楼

命名:
我不知道Java编码约定,但是setter和getter的名称(如setCanHandle())对我来说有点尴尬。

我认为雇员是否可以处理一个调用可以被该类封装。在我自己的C#实现中,我决定使用责任链模式,这种模式在这种情况下看起来更合适。

CallHandler类:

public interface ICallHandler
{
    IResponse Handle(ICallContext callContext); 
    bool IsFree { set; get; }
    bool CanHandle { get; } 
}

public abstract class CallHandlerBase : ICallHandler
{
    private ICallHandler _successor;

    public CallHandlerBase(ICallHandler successor){
        _successor = successor;
    }

    //logic whether a call handler can handle a call
    public abstract bool CanHandle { get; }

    public virtual bool IsFree { set; get; }

    public virtual IResponse Handle(ICallContext callContext)
    {
        IResponse response;
        if (CanHandle && IsFree){
            //Handle logic
            response = new Response();
        } else {
            //escalate to the successor
            response = _successor.Handle(callContext);
        }
        return response;
    }
}

public class Fresher : CallHandlerBase{ }

public class TeamLead : CallHandlerBase{ }

public class ProductManager : CallHandlerBase
{ 
    //PM always can handle a call   
    public override bool CanHandle{
        return true;
    }
}


我也将封装新的逻辑。在这种情况下,保持新鲜的ObjectPool看起来非常方便。

public class CallCenter
{   
    private ICallHandler _teamLead;
    private ICallHandler _productManager;   
    private ObjectPool<ICallHandler> _freshersPool;

    public class CallCenter(ICallHandler productManager, ICallHandler teamLead, Func<ICallHandler> createFresher){      
        _freshersPool = new ObjectPool<ICallHandler> (createFresher);
    }

    public ICallHandler GetCallHandler(){
        fresher = _objectPool.GetObject();
        return fresher;
    }

    public void SetFree(ICallHandler handler){
        _freshersPool.PutObject(handler);
    }
}


客户代码: