簡體   English   中英

Java設計問題:強制執行方法調用序列

[英]Java Design Issue: Enforce method call sequence

最近有一個問題在接受我采訪時被問到。

問題 :有一個類用於分析代碼的執行時間。 這堂課就像:

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檢查,我建議使用自定義異常,但是面試官不滿意。

是否有設計模式來處理這種情況?

編輯 :可以修改類成員和方法實現。

首先,有一個事實是,實現一個自己的Java分析器是浪費時間,因為好的可用(可能這是問題背后的意圖)。

如果要在編譯時強制執行正確的方法順序,則必須返回鏈中每個方法的內容:

  1. start()必須使用stop方法返回WatchStopper
  2. 然后WatchStopper.stop()必須使用getResult()方法返回一個WatchResult

當然,必須防止外部構建這些輔助類以及訪問其方法的其他方法。

通過對界面進行微小更改,您可以使方法序列成為唯一可以調用的方法 - 即使在編譯時也是如此!

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()方法的賦值不同,但這也是可能很好的設計。 所有這些都取決於具體要求......

我們通常使用來自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.

直接前進。

也許他期望這種“重新配置”並且問題根本不是方法序列:

class StopWatch {

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

一旦給予更多的思考

事后看來,聽起來他們正在尋找圍繞模式執行 它們通常用於執行諸如強制關閉流之類的操作。 由於這條線,這也更相關:

是否有設計模式來處理這種情況?

這個想法是你給那些“執行”某些類來做事情的東西。 你可能會使用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() measureAction(Runnable) public。

Java 8調用它的方式更簡單

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

第三個(希望最終)認為:似乎面試官在尋找什么被upvoted(例如,如果基於狀態的最拋出異常stop()之前被調用start()start()stop() )。 這是一個很好的做法,事實上,根據StopWatch具有除private / protected之外的可見性的方法,它可能比沒有更好。 我的一個問題是單獨拋出異常不會強制執行方法調用序列。

例如,考慮一下:

 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()方法)。

一個缺陷可能是stop()的命名。 起初我認為也許是lap()但這通常意味着重新啟動或某種類型(或至少從最后一圈/開始以來的錄音)。 也許read()會更好? 這模仿了觀看秒表時間的動作。 我選擇了stop()來保持它與原始類相似。

我唯一不能100%肯定的是如何獲得時間。 說實話,這似乎是一個更小的細節。 只要兩個...在上面的代碼中以相同的方式獲得當前時間就應該沒問題。

在未以正確順序調用方法時拋出異常是常見的。 例如,如果調用兩次, Threadstart將拋出IllegalThreadStateException

您應該更好地解釋實例如何知道方法是否以正確的順序調用。 這可以通過引入狀態變量,並在每個方法的開頭檢查狀態(並在必要時更新它)來完成。

我建議像:

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

據推測,使用秒表的原因是對時間感興趣的實體不同於負責啟動和停止時間間隔的實體。 如果不是這種情況,那么使用不可變對象並允許代碼隨時查詢秒表以查看到目前為止已經過了多少時間的模式可能比使用可變秒表對象的模式更好。

如果您的目的是捕獲有關花費多少時間做各種事情的數據,我建議您最好通過構建時序相關事件列表的類來提供服務。 這樣的類可以提供生成和添加新的與時序相關的事件的方法,該事件將記錄其創建的時間的快照並提供指示其完成的方法。 外部類還將提供一種方法來檢索到目前為止注冊的所有定時事件的列表。

如果創建新定時事件的代碼提供了指示其目的的參數,則檢查列表的最后代碼可以確定所有已啟動的事件是否已正確完成,並識別任何未啟動的事件; 它還可以識別是否有任何事件完全包含在其他事件中或與其他事件重疊但未包含在其中。 因為每個事件都有自己獨立的狀態,所以無法關閉一個事件不會干擾任何后續事件或導致與它們相關的定時數據的任何丟失或損壞(例如,如果秒表被意外地保持運行時可能會發生已經停止了)。

雖然有一個可變的秒表類使用startstop方法當然是可能的,但如果意圖是每個“停止”動作與特定的“開始”動作相關聯,則具有“開始”動作的對象必須返回“停止“不僅會確保這種關聯,而且即使行動開始並被放棄,也會實現合理的行為。

我知道這已經得到了解答,但找不到一個帶有控制流接口的調用構建器的答案,所以這是我的解決方案:(以比我更好的方式命名接口: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

}

}

所以現在所有用戶都需要在開始時創建StopWatch類的對象,並且getTime()需要在End調用

例如

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

我將建議強制執行方法調用序列是解決錯誤的問題; 真正的問題是一個不友好的界面,用戶必須知道秒表的狀態。 解決方案是刪除任何了解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之后]

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM