【问题标题】:Java Design Issue: Enforce method call sequenceJava 设计问题:强制方法调用顺序
【发布时间】:2015-09-02 12:16:11
【问题描述】:

最近在一次采访中问了我一个问题。

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

Class StopWatch {

    long startTime;
    long stopTime;

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

}

客户端应创建 StopWatch 的实例并相应地调用方法。用户代码可能会弄乱导致意外结果的方法的使用。例如,start()、stop() 和 getTime() 调用应该按顺序进行。

必须“重新配置”此类,以防止用户弄乱序列。

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

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

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

【问题讨论】:

  • 你可以改班吗?面试官是否表示他想静态捕获程序员错误(在编译时)?
  • 这三个方法应该保持不变,并且只是程序员的接口。类成员和方法实现可以改变。
  • 您可以通过引入包装器静态解决它:StoppedStopWatchstart() 方法返回 RunningStopWatchstop() 方法。 (然后阻止对底层StopWatch 的任何直接使用,例如将其设为私有包。)
  • 除了你在问题中所做的之外,你是否在面试中详细说明了你提出的方法?因为我希望一个理性的面试官不会满足于这么多有待澄清的事情,即使他/她正在寻找这些解决方案中的任何一个。

标签: java oop design-patterns


【解决方案1】:

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

如果你想在编译时强制执行正确的方法顺序,你必须为链中的每个方法返回一些东西:

  1. start() 必须使用 stop 方法返回 WatchStopper
  2. 那么WatchStopper.stop() 必须使用getResult() 方法返回一个WatchResult

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

【讨论】:

  • 这种情况下的设计模式是将状态机应用于解决方案,其中每个状态只能进行有效转换。
  • 唯一的缺点是状态逻辑在不同类中的碎片化。这是我不喜欢 GoF 的状态模式的部分。 stackoverflow.com/a/30888746/1168342 将所有逻辑保存在一个类中。
  • 天啊。这就是我喜欢Java的原因。在一个月内,这个库的维护者正在开发 AbstractWatchStopperFactory 以支持不同类型的秒表。
  • @immibis,类似StopWatch => RunningStopWatch => StoppedStopWatch 可能会更好命名(或者只是在后两者之间交替?)。这描述了对象状态而不是可用的方法。 IMO,可用的方法从上下文中会有些明显,这个名称使对象代表的内容更加明显。
【解决方案2】:

通过对接口的细微更改,您可以使方法序列成为唯一可以调用的方法序列 - 即使在编译时!

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

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


在cmets中,有人指出即使采用上述设计,您也可以调用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() 方法的赋值不同,但这也是潜在的好设计。然后,一切都将取决于确切的要求...

【讨论】:

  • 是什么阻止您在 RunningStopwatch 实例上调用 stop 两次?
  • 什么都没有,两个调用都是有效的——我实际上认为这是一个附加值,两个调用都返回一个不同的FinishedStopwatch,以便您可以重复使用一个RunningStopwatch 进行多次测量。现在,事实上,虽然即使是错误的用法也很可能是正确的,但也有可能以某种可怕的方式把它搞砸。如果我们甚至想要屏蔽这种情况,我们应该省略stop() 实例,并且仅在获得运行的秒表实例后才提供getElapsedNanos()
  • 这大概是面试官的意思,但不是生产的正确选择。
  • @usr 不是吗?我使用这种模式......有时。它对于构造复杂的对象很有用,我称之为菜单模式(不知道它是否已经有名字),在野外,它被用于例如为番石榴的MultimapBuilder。我仍然很想知道您为什么认为它不适合生产。
  • @Thomas 这又是一个更高层次的设计问题。基本思想是静态工厂方法更灵活,可以有有意义的名称,更自然地支持 API 更改等。在这种情况下,感觉很自然。基本思想来自 Josh Bloch 的《Effective Java》一书。
【解决方案3】:

我们通常使用来自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.

直截了当。

【讨论】:

  • 这与 OP 所说的有什么不同? “如果在start() 之前调用stop(),我建议使用自定义异常,...,但面试官不满意。”
  • vels4j,我想这种方法正是 Eran 提出的。似乎我走在正确的轨道上,但无法巩固我的答案。所以我想如果方法调用需要维护一个序列,我们将从状态图开始并定义有效/无效转换。
  • @Mohitt 如果我知道有基准解决方案,我会毫无疑问地使用它。
【解决方案4】:

经过深思熟虑

事后看来,他们似乎在寻找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 中添加他们需要做的任何事情。

(如果您使用它来强制关闭流,那么您可以将需要通过数据库连接完成的操作放入其中,这样最终用户就不必担心如何打开和关闭它,同时您可以强制他们正确地关闭它。)

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

Java 8 的调用方式更加简单

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

第三个(希望是最终的)想法:似乎面试官在寻找什么并且被赞成最多的是基于状态抛出异常(例如,如果 stop() 在 @ 之前被调用987654335@ 或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() 方法)。

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

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

【讨论】:

    【解决方案5】:

    也许他预料到了这种“重新配置”,而问题根本与方法顺序无关:

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

    【讨论】:

    • AdamSkyWalker,我问他是否想测量线程的执行时间,但他提到他正在寻找单线程环境中的简单方法。
    • @Mohitt 但我的答案不是关于线程,Runnable 只是一个接口,它提供了包装方法执行和测量花费时间的可能性。它应该在单线程环境中使用。
    • 我认为这可能是面试官所期望的(尽管现在它在语法上并不正确)。最好的回应可能是提供几种设计并讨论每种设计的优缺点。
    【解决方案6】:

    我建议如下:

    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】:

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

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

      【讨论】:

      • 您也可以重复使用startTime,例如在秒表开始之前将其设置为-1
      • Eran,看来你是对的。也许他想用例子来具体回答。感谢您的建议。
      • @aioobe 这当然是一个选项,但我不确定这是否足够,因为您可能不想在停止手表时重置开始和结束时间(因为 getTime 不会给出你是正确的时间),所以如果不添加状态变量,你将无法区分开始的手表和停止的手表。
      【解决方案8】:

      这也可以使用 Java 8 中的 Lambda 来完成。在这种情况下,您将函数传递给 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}
      }
      

      【讨论】:

      • 请注意,此代码也可以在没有 Lambdas 的情况下工作,使用不太简洁的匿名内部类语法,或 Runnable 的任何实现(嵌套类、顶级类等)
      • @Max 好点,它真的应该读为“用 Runnables 完成”而不是“用 Java 8 中的 Lambdas 完成”。
      【解决方案9】:

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

      如果您的目的是收集有关花在做各种事情上的时间的数据,我建议您最好使用构建与时间相关的事件列表的类。这样的类可以提供一种方法来生成和添加新的与时间相关的事件,该事件将记录其创建时间的快照并提供一种方法来指示其完成。外部类还将提供一种方法来检索迄今为止注册的所有计时事件的列表。

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

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

      【讨论】:

        【解决方案10】:

        我知道这个问题已经得到解答,但找不到一个答案,它调用带有 interfaces 的控制流的构建器,所以这是我的解决方案: (以比我更好的方式命名接口: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();
        

        【讨论】:

          【解决方案11】:

          根据面试问题,好像是这样的

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

          【讨论】:

            【解决方案12】:

            我将建议执行方法调用顺序正在解决错误的问题;真正的问题是一个不友好的界面,用户必须知道秒表的状态。解决方案是取消任何了解 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 之后] 距离标记 [循环 1 之前] 1037 毫秒
            标记:[循环 2 之后] 2008 毫秒,因为标记 [循环 1 之后]

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-02-03
              • 1970-01-01
              • 2011-04-28
              • 1970-01-01
              相关资源
              最近更新 更多