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

Moh*_*itt 57 java oop design-patterns

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

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

Class StopWatch {

    long startTime;
    long stopTime;

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

}
Run Code Online (Sandbox Code Playgroud)

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

必须"重新配置"该类,以便可以防止用户弄乱序列.

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

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

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

Wao*_*aog 84

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

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

  1. start()必须WatchStopper使用stop方法返回a .
  2. 然后WatchStopper.stop()必须WatchResultgetResult()方法返回一个.

当然,必须防止外部构建这些辅助类以及访问其方法的其他方法.

  • 好家伙.这就是我喜欢Java的原因.在一个月内,该库的维护者正在使用AbstractWatchStopperFactory来支持不同类型的秒表. (18认同)
  • 在这种情况下,设计模式是将状态机应用于解决方案,其中在每个状态下只能进行有效的转换. (6认同)
  • 唯一的缺点是不同类别的状态逻辑碎片化.这是我不喜欢GoF的State Pattern的部分.http://stackoverflow.com/a/30888746/1168342将所有逻辑保留在一个类中. (2认同)

Pet*_*ček 37

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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


在评论中,有人指出,即使采用上述设计,您也可以拨打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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 什么都没有,而且两个调用都是有效的 - 我实际上认为这是一个附加值,两个调用返回一个不同的`FinishedStopwatch`,这样你就可以重复使用一个`RunningStopwatch`进行多次测量.实际上,虽然即使错误地使用它也很可能是正确的,但有可能以某种可怕的方式搞砸了这一点.如果我们想要屏蔽它,我们应该省略`stop()`实例,并且只有在获得运行的秒表实例时才提供`getElapsedNanos()`. (4认同)
  • @usr不是吗?我有时使用这种模式.它对构造复杂的对象很有用,我把它称为菜单模式(不知道它是否已经有名称),而且在野外,它用于例如Guava的[`MultimapBuilder`](http:// docs. guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/MultimapBuilder.html).我仍然很想知道为什么你认为它不适合生产. (4认同)
  • 这可能是面试官的意思,但它不是生产的正确选择. (3认同)
  • @Thomas再次,这是一个更高层次的设计问题.基本思想是静态工厂方法更灵活,可以有更有意义的名称,更自然地支持API更改等.在这种情况下,感觉就像一个自然的契合.基本思想来自Josh Bloch的高效Java书. (2认同)

vel*_*s4j 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.
Run Code Online (Sandbox Code Playgroud)

直接前进.

  • 这与OP所说的有何不同?_"如果在`start()`之前调用`stop()`,我建议使用自定义异常,......但是面试官不满意."_ (31认同)
  • vels4j,我想这种方法正是Eran所提出的.似乎我在正确的轨道上,但无法巩固我的答案.所以我想如果要为方法调用维护一个序列,我们将从状态图开始并定义有效/无效转换. (2认同)

Cap*_*Man 20

一旦给予更多的思考

事后看来,听起来他们正在寻找围绕模式执行.它们通常用于执行诸如强制关闭流之类的操作.由于这条线,这也更相关:

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

这个想法是你给那些"执行"某些类来做事情的东西.你可能会使用,Runnable但没有必要.(Runnable最有意义,你很快就会明白为什么.)在你的StopWatch课堂上添加一些像这样的方法

public long measureAction(Runnable r) {
    start();
    r.run();
    stop();
    return getTime();
}
Run Code Online (Sandbox Code Playgroud)

然后你会这样称呼它

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);
Run Code Online (Sandbox Code Playgroud)

这使它变得愚蠢.您不必担心在开始之前处理停止或者人们忘记呼叫一个而不是另一个,等等.原因Runnable很好是因为

  1. 标准的java类,不是您自己的或第三方
  2. 最终用户可以将他们需要的任何内容放入其中Runnable.

(如果您使用它来强制关闭流,那么您可以将需要完成的操作放在数据库连接中,这样最终用户就不必担心如何打开和关闭它并同时强制它们关闭它恰当.)

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

Java 8调用它的方式更简单

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

第三个(希望是最终的)思想:看起来面试官正在寻找什么以及最受欢迎的是基于国家抛出异常(例如,如果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;
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我知道真正执行该方法被称为正确的唯一方法就是自己与执行周围图案或其他建议,做这样的事情回报做到这一点RunningStopWatch,并StoppedStopWatch说我相信只有一个方法,但这似乎过于复杂(OP提到界面无法更改,诚然,我做的非包装建议虽然这样做.因此,据我所知,如果不修改界面或添加更多类,就无法强制执行正确的顺序.

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

StopWatch stopWatch = new StopWatch();
stopWatch.getTime();
stopWatch.stop();
stopWatch.start();
Run Code Online (Sandbox Code Playgroud)

确实,它不会运行,但它只是简单地交给一个Runnable并使这些方法变得私密,让另一个放松并自己处理讨厌的细节.然后没有猜测的工作.有了这个类,它显然是顺序,但如果有更多的方法或名称不是那么明显,它可能开始是一个令人头痛的问题.


原始答案

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

"这三种方法应保持不变,只是程序员的接口.类成员和方法实现可以改变."

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

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

class StopWatch {

    private final long startTime;

    public StopWatch() {
        startTime = ...
    }

    public long stop() {
        currentTime = ...
        return currentTime - startTime;
    }
}
Run Code Online (Sandbox Code Playgroud)

我认为这是好的原因是记录是在对象创建期间所以它不能被遗忘或无序完成(stop()如果它不存在则不能调用方法).

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

我唯一不能100%肯定的是如何获得时间.说实话,这似乎是一个更小的细节.只要...在上面的代码中都以相同的方式获得当前时间就应该没问题.


Ada*_*ker 19

也许他期望这种"重新配置"并且问题根本不是方法序列:

class StopWatch {

   public static long runWithProfiling(Runnable action) {
      startTime = now;
      action.run();
      return now - startTime;
   }
}
Run Code Online (Sandbox Code Playgroud)

  • @Mohitt但我的答案不是关于线程,Runnable只是一个接口,可以包装方法执行和测量花费的时间.它应该在单线程环境中使用. (6认同)

tal*_*lex 6

我建议像:

interface WatchFactory {
    Watch startTimer();
}

interface Watch {
    long stopTimer();
}
Run Code Online (Sandbox Code Playgroud)

它会像这样使用

 Watch watch = watchFactory.startTimer();

 // Do something you want to measure

 long timeSpentInMillis = watch.stopTimer();
Run Code Online (Sandbox Code Playgroud)

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


Era*_*ran 5

在未以正确顺序调用方法时抛出异常是常见的.例如,Threadstart将抛出IllegalThreadStateException,如果叫了两声.

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

  • 伊兰,看来你是对的.也许他希望通过例子给出具体答案.谢谢你的建议. (2认同)