web-dev-qa-db-ja.com

Java設計の問題:メソッド呼び出しシーケンスを強制する

最近インタビューで私に尋ねられた質問があります。

問題:コードの実行時間をプロファイルするためのクラスがあります。クラスは次のようなものです:

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チェックをいくつか実行した場合のカスタム例外の使用を提案しましたが、インタビュアーは満足していませんでした。

このような状況を処理するためのデザインパターンはありますか?

編集:クラスのメンバーとメソッドの実装を変更できます。

57
Mohitt

私たちは通常、StopWatchを Apache Commons 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.

まっすぐ進む。

23
vels4j

まず、独自のJavaプロファイラを実装するのは時間の無駄です。良いものが利用可能であるためです(おそらくそれが質問の背後にある意図でした)。

コンパイル時に正しいメソッドの順序を強制したい場合は、チェーン内の各メソッドで何かを返す必要があります。

  1. start()は、stopメソッドでWatchStopperを返す必要があります。
  2. 次に、WatchStopper.stop()getResult()メソッドを使用してWatchResultを返す必要があります。

もちろん、これらのヘルパークラスの外部での構築や、メソッドにアクセスする他の方法は禁止する必要があります。

83
Waog

インターフェイスに小さな変更を加えることで、メソッドシーケンスを、コンパイル時でも呼び出すことができる唯一のメソッドシーケンスにすることができます。

_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()を2回呼び出すことができることが指摘されました。それは付加価値だと思いますが、理論的にはめちゃくちゃになることは可能です。次に、私が考えることができる唯一の方法は次のようなものです:

_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()メソッドを省略している点で割り当てとは異なりますが、これも潜在的に優れた設計です。そして、すべてが正確な要件に依存します...

36
Petr Janeček

たぶん彼はこの「再構成」を期待していて、メソッドのシーケンスについての質問はまったくありませんでした:

class StopWatch {

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

一度考えて

後から考えると、彼らは パターンを実行する を探していたようです。これらは通常、ストリームを強制的に閉じるなどの目的で使用されます。これは、次の行により、より関連性があります。

このような状況に対処するためのデザインパターンはありますか?

アイデアは、何かを「実行」するクラスに何かを行うためのものを与えることです。おそらく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 */});
_

3番目の(うまくいけば最後の)考え:インタビュアーが探していたものと最も賛成されているものは、状態に基づいて例外をスローしているようです(たとえば、 stop()の前にstart()を呼び出すか、start()stop()の後に呼び出した場合)。これは良い習慣であり、実際、StopWatchのメソッドがprivate/protected以外の可視性を持っているかどうかによっては、持っているよりも持っているほうが良いでしょう。これに関する私の1つの問題は、例外をスローするだけでは、メソッド呼び出しシーケンスが強制されないことです

たとえば、次のことを考慮してください。

_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がスローされているからといって、適切なシーケンスが強制されているわけではありませんこれは、不適切なシーケンスが拒否されたことを意味します(そして私は例外が厄介であることに全員が同意できます。幸いにも、これは確認済みの例外ではありません)。

メソッドが正しく呼び出されることを本当に強制することがわかっている唯一の方法は、execute aroundパターンまたはreturn RunningStopWatchStoppedStopWatchのようなことを行う他の提案を自分で行うことです1つの方法だけですが、これは過度に複雑に見えます(そしてOPは、インターフェイスを変更できないと述べましたが、確かに、私が作成した非ラッパーの提案はこれを行います)。したがって、私の知る限りでは、インターフェイスを変更せずに適切な順序をenforceする方法はありません。クラスを追加します。

それは、人々が「メソッド呼び出しシーケンスを強制する」という意味をどう定義するかに依存していると思います。例外のみがスローされた場合、以下がコンパイルされます

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

確かにrunしませんが、Runnableを渡してそれらのメソッドを非公開にし、他のメソッドをリラックスさせて処理することは非常に簡単に思えます厄介なことはあなた自身を詳述しています。その後、推測作業はありません。このクラスでは順序は明らかですが、メソッドが多い場合や名前がそれほど明確でない場合は、頭痛の種になる可能性があります。


元の答え

より後からの編集:OPがコメントで言及している

「3つのメソッドはそのままにしておく必要があり、プログラマーへのインターフェースにすぎません。クラスメンバーとメソッドの実装は変更される可能性があります。」

したがって、インターフェースから何かが削除されるため、以下は間違っています。 (技術的には、空のメソッドとして実装することもできますが、それは馬鹿げたことのようで、混乱しすぎます。)制限がなく、別の「ばかげた証拠」である場合、私はこの答えのように思います「それをする方法だから私はそれを残します。

私にはこのようなものが良いようです。

_class StopWatch {

    private final long startTime;

    public StopWatch() {
        startTime = ...
    }

    public long stop() {
        currentTime = ...
        return currentTime - startTime;
    }
}
_

これが良いと思う理由は、記録がオブジェクトの作成中なので、忘れたり順不同にしたりできないためです(存在しない場合はstop()メソッドを呼び出せません)。

1つの欠点は、おそらくstop()の命名です。最初は多分lap()だと思いましたが、それは通常、再起動または何らかのソート(または少なくとも最後のラップ/スタート以降の記録)を意味します。おそらくread()のほうがいいのでは?これはストップウォッチで時間を見る動作を模倣しています。元のクラスと同様に保つためにstop()を選択しました。

100%確実ではない唯一のことは、時間を取得する方法です。正直に言うと、それはもっと細かいことのようです。上記のコードの両方の_..._が同じ方法で現在の時刻を取得している限り、問題ありません。

19
Captain Man

私は次のようなものを提案します:

interface WatchFactory {
    Watch startTimer();
}

interface Watch {
    long stopTimer();
}

このように使われます

 Watch watch = watchFactory.startTimer();

 // Do something you want to measure

 long timeSpentInMillis = watch.stopTimer();

間違った順序で何かを呼び出すことはできません。また、stopTimerを2回呼び出すと、どちらの場合も意味のある結果が得られます(おそらく、名前をmeasureに変更して、呼び出すたびに実際の時間を返す方がよいでしょう)。

5
talex

メソッドが正しい順序で呼び出されない場合に例外をスローするのが一般的です。たとえば、Threadstartを2回呼び出すと、IllegalThreadStateExceptionがスローされます。

メソッドが正しい順序で呼び出されているかどうかインスタンスがどのように認識するかをよりよく説明しているはずです。これは、状態変数を導入し、各メソッドの開始時に状態を確認する(そして必要に応じて更新する)ことで実行できます。

5
Eran

おそらくストップウォッチを使用する理由は、時間に関心のあるエンティティが、タイミング間隔の開始と停止を担当するエンティティとは異なるためです。そうでない場合は、不変オブジェクトを使用し、いつでもストップウォッチにクエリを実行して現在までの経過時間を確認できるようにするパターンは、可変ストップウォッチオブジェクトを使用するパターンよりも優れています。

目的がさまざまなことに費やされている時間に関するデータを収集することである場合、タイミング関連のイベントのリストを作成するクラスが最適なサービスを提供することをお勧めします。このようなクラスは、作成された時間のスナップショットを記録し、その完了を示すメソッドを提供する、新しいタイミング関連のイベントを生成および追加するメソッドを提供します。外部クラスは、これまでに登録されたすべてのタイミングイベントのリストを取得するメソッドも提供します。

新しいタイミングイベントを作成するコードがその目的を示すパラメーターを提供する場合、リストを調べる最後のコードは、開始されたすべてのイベントが適切に完了したかどうかを確認し、完了していないイベントを識別します。また、イベントが完全に他のイベント内に含まれているか、他のイベントと重複していても、それらのイベント内に含まれていないかどうかも識別できます。各イベントには独自のステータスがあるため、1つのイベントを閉じない場合、後続のイベントに干渉したり、それらに関連するタイミングデータを損失したり破損したりする必要はありません(たとえば、ストップウォッチが誤って実行されていた場合に発生する可能性があります)。停止されています)。

startおよびstopメソッドを使用する変更可能なストップウォッチクラスを作成することは確かに可能ですが、各「停止」アクションが特定の「開始」アクションに関連付けられ、 「停止」する必要があるオブジェクトを返す「開始」アクションは、そのような関連付けを保証するだけでなく、アクションが開始されて中止された場合でも、賢明な動作を実現できます。

3
supercat

これは、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}
}
3
David Grinberg

私はこれがすでに回答されていることを知っていますが、制御フローに対してinterfacesでビルダーを呼び出す回答を見つけることができませんでした。

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();
1
NeeL

メソッド呼び出しシーケンスを強制することで、間違った問題を解決することをお勧めします。実際の問題は、ユーザーがストップウォッチの状態を認識しなければならない、使いにくいインターフェースです。解決策は、ストップウォッチの状態を知るための要件を削除することです。

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の後]マークから1037ミリ秒[ループ1の前]
マーク:[ループ2の後]マークから2008ミリ秒[ループ1の後]

0
Qwerky

インタビューごとの質問として、このように思われます

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()
0
Abhishek Mishra