Java设计问题:强制执行方法调用序列

时间:2015-06-17 10:12:40

标签: java oop design-patterns

最近有一个问题是我在接受采访时被问到的。

问题:有一个类用于分析代码的执行时间。这堂课就像:

Class StopWatch {

    long startTime;
    long stopTime;

    void start() {// set startTime}
    void stop() { // set stopTime}
    long getTime() {// return difference}

}

客户端应该创建StopWatch的实例并相应地调用方法。用户代码可能会搞乱使用导致意外结果的方法。 Ex,start(),stop()和getTime()调用应该是有序的。

这个课程必须重新配置"这样可以防止用户弄乱序列。

如果在start()之前调用stop(),或者做一些if / else检查,我建议使用自定义异常,但是面试官不满意。

是否有处理这类情况的设计模式?

编辑:可以修改类成员和方法实现。

12 个答案:

答案 0 :(得分:83)

首先,实现一个自己的Java分析器是浪费时间,因为可以使用好的(也许这就是问题背后的意图)。

如果要在编译时强制执行正确的方法顺序,则必须返回链中每个方法的内容:

  1. start()必须使用停止方法返回WatchStopper
  2. 然后WatchStopper.stop()必须使用WatchResult方法返回getResult()
  3. 当然,必须防止外部构建这些辅助类以及访问其方法的其他方法。

答案 1 :(得分:36)

通过对界面进行微小更改,您可以使方法序列成为唯一可以调用的方法 - 即使在编译时也是如此!

public class Stopwatch {
    public static RunningStopwatch createRunning() {
        return new RunningStopwatch();
    }
}

public class RunningStopwatch {
    private final long startTime;

    RunningStopwatch() {
        startTime = System.nanoTime();
    }

    public FinishedStopwatch stop() {
        return new FinishedStopwatch(startTime);
    }
}

public class FinishedStopwatch {
    private final long elapsedTime;

    FinishedStopwatch(long startTime) {
        elapsedTime = System.nanoTime() - startTime;
    }

    public long getElapsedNanos() {
        return elapsedTime;
    }
}

用法很简单 - 每个方法都返回一个只有当前适用方法的不同类。基本上,秒表的状态被封装在类型系统中。

在评论中,有人指出即使采用上述设计,您也可以拨打stop()两次。虽然我认为这是附加价值,但理论上可以将自己搞砸。然后,我能想到的唯一方法就是这样:

class Stopwatch {
    public static Stopwatch createRunning() {
        return new Stopwatch();
    }

    private final long startTime;

    private Stopwatch() {
        startTime = System.nanoTime();
    }

    public long getElapsedNanos() {
        return System.nanoTime() - startTime;
    }
}

这与通过省略stop()方法的分配不同,但这也是可能很好的设计。所有这些都取决于确切的要求......

答案 2 :(得分:23)

我们通常使用来自Apache Commons StopWatch的StopWatch检查它们提供的模式。

当秒表状态错误时抛出IllegalStateException。

public void stop()

Stop the stopwatch.

This method ends a new timing session, allowing the time to be retrieved.

Throws:
    IllegalStateException - if the StopWatch is not running.

直接向前。

答案 3 :(得分:19)

也许他期望这种“重新配置”,而问题根本不在于方法序列:

class StopWatch {

   public static long runWithProfiling(Runnable action) {
      startTime = now;
      action.run();
      return now - startTime;
   }
}

答案 4 :(得分:19)

一旦给出更多想法

事后看来,他们正在寻找execute around pattern。它们通常用于执行诸如强制关闭流之类的操作。由于这一行,这也更具相关性:

  

是否有设计模式来处理这种情况?

这个想法是你给那些“执行”某些类来做事情的东西。您可能会使用Runnable,但这不是必需的。 Runnable最有意义,你很快就会明白为什么。)在你的StopWatch课程中添加一些像这样的方法

public long measureAction(Runnable r) {
    start();
    r.run();
    stop();
    return getTime();
}

然后你会像这样称呼它

StopWatch stopWatch = new StopWatch();
Runnable r = new Runnable() {
    @Override
    public void run() {
        // Put some tasks here you want to measure.
    }
};
long time = stopWatch.measureAction(r);

这使它变得傻瓜。您不必担心在开始之前处理停止或者忘记呼叫一个而不是另一个等等。Runnable很好的原因是因为

  1. 标准java类,不是您自己的或第三方
  2. 最终用户可以在Runnable中填写所需内容。
  3. (如果您使用它来强制关闭流,那么您可以将需要完成的操作放在内部,以便最终用户无需担心如何打开和关闭它并同时强制他们要妥善关闭它。)

    如果您愿意,可以制作一些StopWatchWrapper,而不是StopWatch保持不变。您也可以让measureAction(Runnable)不返回时间,而是将getTime()公开。

    Java 8调用它的方式更简单

    StopWatch stopWatch = new StopWatch();
    long time = stopWatch.measureAction(() - > {/* Measure stuff here */});
    
      

    第三个(希望是最后的)想法:看起来面试官正在寻找什么,最受欢迎的是基于州的抛出异常(例如,如果调用stop()start()之前的start()stop()之前。这是一个很好的做法,事实上,根据StopWatch中除了私有/受保护之外的可见性的方法,它可能更好,而不是没有。我的一个问题是单独抛出异常不会强制执行方法调用序列。

         

    例如,考虑一下:

    class StopWatch {
        boolean started = false;
        boolean stopped = false;
    
        // ...
    
        public void start() {
            if (started) {
                throw new IllegalStateException("Already started!");
            }
            started = true;
            // ...
        }
    
        public void stop() {
            if (!started) {
                throw new IllegalStateException("Not yet started!");
            }
            if (stopped) {
                throw new IllegalStateException("Already stopped!");
            }
            stopped = true;
            // ...
        }
    
        public long getTime() {
            if (!started) {
                throw new IllegalStateException("Not yet started!");
            }
            if (!stopped) {
                throw new IllegalStateException("Not yet stopped!");
            }
            stopped = true;
            // ...
        }
    }
    
         

    仅仅因为它正在抛出IllegalStateException并不意味着强制执行正确的序列,它只是意味着不正确的序列被拒绝(我认为我们都同意异常令人讨厌,幸运的是这不是一个经过检查的例外)。

         

    我知道真正强制方法被正确调用的唯一方法是使用执行模式或其他建议来执行此操作,例如返回RunningStopWatchStoppedStopWatch只有一种方法,但这似乎过于复杂(并且OP提到接口无法更改,诚然,我做的非包装器建议虽然这样做)。所以据我所知,没有办法 强制执行 正确的顺序,而无需修改界面或添加更多类。

         

    我想这实际上取决于人们定义“强制执行方法调用序列”的含义。如果仅抛出异常,则以下编译

    StopWatch stopWatch = new StopWatch();
    stopWatch.getTime();
    stopWatch.stop();
    stopWatch.start();
    
         

    是的,它不会运行,但是传递Runnable并将这些方法设为私有似乎更简单,让另一个放松并自己处理讨厌的细节。然后没有猜测的工作。有了这个类,它显然是顺序,但如果有更多的方法或名称不那么明显,它可能会开始成为一个令人头痛的问题。

    原始答案

      

    更多事后编辑:OP在评论中提到

         
        

    “这三个方法应该保持不变,并且只是程序员的接口。类成员和方法实现可以改变。”

      
         

    所以下面的内容是错误的,因为它会从界面中删除一些内容。 (从技术上讲,你可以把它作为一个空方法实现,但这似乎是一个愚蠢的事情,太混乱。)我有点像这个答案,如果限制不存在,它似乎似乎是另一个“傻瓜证明“这样做的方式让我离开它。

    对我来说这样的事情似乎很好。

    class StopWatch {
    
        private final long startTime;
    
        public StopWatch() {
            startTime = ...
        }
    
        public long stop() {
            currentTime = ...
            return currentTime - startTime;
        }
    }
    

    我认为这是好的原因是记录是在对象创建期间,因此不能忘记或无序完成(如果不存在,则无法调用stop()方法。)< / p>

    一个缺陷可能是stop()的命名。起初我认为可能是lap(),但这通常意味着重新启动或某种类型(或至少从上一圈/开始以来的录音)。也许read()会更好?这模仿了观看秒表时间的动作。我选择了stop()来保持它与原始类似。

    我唯一不确定的是如何获得时间。说实话,这似乎是一个更小的细节。只要上面代码中的...都以相同的方式获得当前时间就可以了。

答案 5 :(得分:5)

在未按正确顺序调用方法时抛出异常很常见。例如,Thread&#39; s start如果被调用两次则会抛出IllegalThreadStateException

您应该更好地解释实例如何知道方法是否以正确的顺序调用。这可以通过引入状态变量,并在每个方法的开头检查状态(并在必要时更新它)来完成。

答案 6 :(得分:5)

我建议像:

interface WatchFactory {
    Watch startTimer();
}

interface Watch {
    long stopTimer();
}

它会像这样使用

 Watch watch = watchFactory.startTimer();

 // Do something you want to measure

 long timeSpentInMillis = watch.stopTimer();

你不能以错误的顺序调用任何东西。如果你两次调用stopTimer,你会得到有意义的结果(也许最好将它重命名为measure并在每次调用时返回实际时间)

答案 7 :(得分:3)

这也可以在Java 8中使用Lambdas完成。在这种情况下,您将函数传递给StopWatch类,然后告诉StopWatch执行该代码。

Class StopWatch {

    long startTime;
    long stopTime;

    private void start() {// set startTime}
    private void stop() { // set stopTime}
    void execute(Runnable r){
        start();
        r.run();
        stop();
    }
    long getTime() {// return difference}
}

答案 8 :(得分:3)

据推测,使用秒表的原因是对时间感兴趣的实体与负责启动和停止时间间隔的实体不同。如果不是这种情况,使用不可变对象的模式以及允许代码随时查询秒表以查看到目前为止已经过了多长时间的模式可能会比使用可变秒表对象的模式更好。

如果您的目的是捕获有关花费多少时间做各种事情的数据,我建议您可以通过构建时间相关事件列表的类来获得最佳服务。这样的类可以提供生成和添加新的与时序相关的事件的方法,该事件将记录其创建的时间的快照并提供指示其完成的方法。外部类还将提供一种方法来检索到目前为止注册的所有时间事件的列表。

如果创建新计时事件的代码提供了指示其目的的参数,则检查列表的末尾的代码可以确定所有已启动的事件是否已正确完成,并识别任何未发生的事件;它还可以识别是否有任何事件完全包含在其他事件中或与其他事件重叠但未包含在其中。因为每个事件都有自己独立的状态,所以无法关闭一个事件不会干扰任何后续事件或导致与它们相关的定时数据的任何丢失或损坏(例如,如果秒表被意外地保持运行时可能会发生已经停止了。)

虽然有可能使用startstop方法的可变秒表类,但如果意图是每个“停止”动作与特定的“开始”动作相关联,那么“开始”操作返回一个必须“停止”的对象不仅会确保这种关联,而且即使某个操作被启动和放弃,它也会实现合理的行为。

答案 9 :(得分:1)

我知道这已经得到了解答但是找不到一个带有接口的控制流调用构建器的答案,所以这是我的解决方案: (以比我更好的方式命名接口:p)

public interface StartingStopWatch {
    StoppingStopWatch start();
}

public interface StoppingStopWatch {
    ResultStopWatch stop();
}

public interface ResultStopWatch {
    long getTime();
}

public class StopWatch implements StartingStopWatch, StoppingStopWatch, ResultStopWatch {

    long startTime;
    long stopTime;

    private StopWatch() {
        //No instanciation this way
    }

    public static StoppingStopWatch createAndStart() {
        return new StopWatch().start();
    }

    public static StartingStopWatch create() {
        return new StopWatch();
    }

    @Override
    public StoppingStopWatch start() {
        startTime = System.currentTimeMillis();
        return this;
    }

    @Override
    public ResultStopWatch stop() {
        stopTime = System.currentTimeMillis();
        return this;
    }

    @Override
    public long getTime() {
        return stopTime - startTime;
    }

}

用法:

StoppingStopWatch sw = StopWatch.createAndStart();
//Do stuff
long time = sw.stop().getTime();

答案 10 :(得分:0)

根据面试问题,似乎喜欢这个

Class StopWatch {

    long startTime;
    long stopTime;
    public StopWatch() {
    start();
    }

    void start() {// set startTime}
    void stop() { // set stopTime}
    long getTime() {
stop();
// return difference

}

}

所以现在所有用户都需要在开始时创建StopWatch类的对象,而getTime()需要在结束时调用

例如

StopWatch stopWatch=new StopWatch();
//do Some stuff
 stopWatch.getTime()

答案 11 :(得分:0)

我建议强制执行方法调用序列是解决错误的问题;真正的问题是一个不友好的界面,用户必须知道秒表的状态。解决方案是删除任何了解StopWatch状态的要求。

public class StopWatch {

    private Logger log = Logger.getLogger(StopWatch.class);

    private boolean firstMark = true;
    private long lastMarkTime;
    private long thisMarkTime;
    private String lastMarkMsg;
    private String thisMarkMsg;

    public TimingResult mark(String msg) {
        lastMarkTime = thisMarkTime;
        thisMarkTime = System.currentTimeMillis();

        lastMarkMsg = thisMarkMsg;
        thisMarkMsg = msg;

        String timingMsg;
        long elapsed;
        if (firstMark) {
            elapsed = 0;
            timingMsg = "First mark: [" + thisMarkMsg + "] at time " + thisMarkTime;
        } else {
            elapsed = thisMarkTime - lastMarkTime;
            timingMsg = "Mark: [" + thisMarkMsg + "] " + elapsed + "ms since mark [" + lastMarkMsg + "]";
        }

        TimingResult result = new TimingResult(timingMsg, elapsed);
        log.debug(result.msg);
        firstMark = false;
        return result;
    }

}

这允许简单地使用mark方法,并返回结果并包含日志记录。

StopWatch stopWatch = new StopWatch();

TimingResult r;
r = stopWatch.mark("before loop 1");
System.out.println(r);

for (int i=0; i<100; i++) {
    slowThing();
}

r = stopWatch.mark("after loop 1");
System.out.println(r);

for (int i=0; i<100; i++) {
    reallySlowThing();
}

r = stopWatch.mark("after loop 2");
System.out.println(r);

这给出了很好的结果;

  

第一个标记:[在循环1之前]时间1436537674704
  标记:[循环1之后] 1037ms,因为标记[在循环1之前]
  标记:[循环2之后] 2008ms自标记[循环1之后]

相关问题