Javadevise问题:强制执行方法调用序列

最近有一个问题是在面试中问我的。

问题 :有一个类用于描述代码的执行时间。 这个class是这样的:

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

期望客户端创buildStopWatch的实例并相应地调用方法。 用户代码可能会搞乱导致意想不到的结果的方法的使用。 例如,start(),stop()和getTime()调用应该是按顺序的。

这个类必须“重新configuration”,以防止用户搞乱序列。

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

有处理这种情况的devise模式吗?

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

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

当秒表状态错误时,抛出IllegalStateExceptionexception。

 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. 

直向前。

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

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

  1. start()必须用stop方法返回一个WatchStopper
  2. 然后WatchStopper.stop()必须用getResult()方法返回WatchResult

那些帮助者类的外部build构以及其他访问他们方法的方法当然必须被阻止。

通过对接口进行细微的更改,您可以使方法顺序成为唯一可以调用的顺序 – 即使在编译时也是如此!

 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; } } 

用法很简单 – 每个方法都返回一个不同的类,只有当前适用的方法。 基本上,秒表的状态是包装的types系统。


在评论中指出,即使采用上述devise,也可以调用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()方法的分配不同,但这也是一个很好的devise。 所有的都将取决于具体的要求…

也许他预计这个“重构”,问题不在于方法的顺序:

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

一旦有更多的想法

事后看来,这听起来像是在寻找周围的执行模式 。 他们通常用于执行诸如强制closuresstream的操作。 这也是由于这条线更相关:

有处理这种情况的devise模式吗?

这个想法是你给那些在周围“执行”的东西做些什么。 你可能会使用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中完成。

(如果您正在使用它来强制closuresstream,那么您可以将需要在数据库连接中完成的操作放在里面,以便最终用户不必担心如何打开和closures它,同时强制它们closures它正确。)

如果你想,你可以做一些StopWatchWrapper而不用修改StopWatch 。 你也可以使measureAction(Runnable)不返回一个时间,而是让getTime()公开。

Java 8调用它的方法甚至更简单

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

第三个(希望是最后的)想法:这似乎是面试官正在寻找的东西,最常被抛出的是抛出基于状态的exception(例如,如果在start()之前调用stop() start()或者在stop()之后调用start() stop() )。 这是一个很好的做法,事实上,取决于StopWatch中除private / protected之外的可见性方法,最好还是不要这样做。 我的一个问题是抛出exception本身不会强制执行方法调用序列。

例如,考虑一下:

 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并不意味着正确的序列被强制执行, 这只是意味着不正确的序列被拒绝 (我认为我们都可以同意exception是烦人的,幸运的是这不是一个检查exception)。

我知道真正强制执行的方法是正确调用的唯一方法是自己用周围的执行模式或其他build议,如返回StoppedStopWatchStoppedStopWatch ,我认为只有一个方法,但这似乎过于复杂(和OP提到,接口不能改变,无可否认,我提出的非包装build议做到了这一点)。 所以据我所知,没有修改界面或添加更多类的强制顺序是没有办法的。

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

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

真的,它不会运行 ,但交付一个Runnable似乎是非常简单的,让这些方法是私人的,让另一个放松,自己处理烦人的细节。 那么没有猜测的工作。 有了这个类,这个顺序就显而易见了,但是如果有更多的方法,或者名称不是那么明显,那么可能会让人头疼。


原始答案

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

“这三种方法应该保持完整,只能与程序员接口,而类的成员和方法的实现可能会改变。

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

对我来说,这似乎是好事。

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

我认为这是好的原因是在创build对象时logging,所以它不能被遗忘或不按顺序(如果它不存在,不能调用stop()方法)。

一个缺陷可能是stop()的命名。 起初,我想也许是lap()但通常意味着重新开始或某种(或至less从上一圈/开始logging)。 也许read()会更好? 这模拟了在秒表上查看时间的动作。 我select了stop()来保持它与原始类相似。

我不是100%确定的唯一的事情是如何得到时间。 说实话,似乎是一个更小的细节。 只要在上面的代码中获得当前时间的方式应该是一样的。

当方法没有按正确顺序调用时抛出exception是很常见的。 例如,如果调用两次, Threadstart将会抛出一个IllegalThreadStateException

您应该可能已经更好地解释了实例是否会按照正确的顺序调用方法。 这可以通过引入一个状态variables,并检查每个方法开始时的状态(并在必要时进行更新)来完成。

我build议像这样:

 interface WatchFactory { Watch startTimer(); } interface Watch { long stopTimer(); } 

它会像这样使用

  Watch watch = watchFactory.startTimer(); // Do something you want to measure long timeSpentInMillis = watch.stopTimer(); 

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

这也可以通过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} } 

据推测,使用秒表的原因是对时间感兴趣的实体不同于负责启动和停止时间间隔的实体。 如果情况并非如此,则使用不可变对象的模式以及允许代码随时查询秒表以查看迄今为止已经过了多less时间可能比使用可变秒表对象的模式更好。

如果你的目的是捕捉多less时间在做各种事情上的数据,我build议你最好由一个build立一个与时间有关的事件列表的类来提供服务。 这样的类可以提供一种方法来生成和添加新的与时间有关的事件,该事件将logging其创build时间的快照并提供指示其完成的方法。 外部类还将提供一种方法来检索迄今为止已注册的所有计时事件的列表。

如果创build新定时事件的代码提供了一个指示其用途的参数,则在检查清单的末尾的代码可以确定所有已启动的事件是否已经正确完成,并确定没有发生的事件; 还可以确定是否有任何事件完全包含在其他事件中,或与其他事件重叠,但不包含在其中。 因为每个事件都有自己的独立状态,所以未能closures一个事件就不会干扰任何后续事件,或者导致与其相关的时间数据的丢失或损坏(例如,如果秒表应该是偶然发生的已经停止)。

尽pipe可能有一个使用startstop方法的可变秒表类,但如果每个“stop”动作的意图是与一个特定的“start”动作相关联,那么使用“start”动作返回一个必须是“停止“不仅会确保这种联系,而且即使开始和放弃行动,也可以实现明智的行为。

我知道这已经被回答了,但是找不到一个答案来为控制stream调用具有接口的构build器,所以这里是我的解决scheme:(以比我更好的方式命名接口: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(); 

按照面试问题,这似乎是这样的

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

所以现在所有的用户都需要在开始时创buildStopWatch类的对象,getTime()需要在End完成

例如

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

我会build议强制执行方法调用序列是解决错误的问题; 真正的问题是用户必须意识到秒表状态的不友好的界面。 解决方法是删除任何要求知道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方法,并返回结果并logging日志。

 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); 

这给出了很好的结果;

第一个标记:在时间1436537674704处[循环1之前]
标记:[循环1之后] 1037ms自标记[循环1之前]
Mark:[循环2之后] 2008ms以来,mark [循环1之后]