Подтвердить что ты не робот

Проблема с дизайном Java: последовательность вызова метода Enforce

Есть вопрос, который недавно был задан мне в интервью.

Проблема. Существует класс, предназначенный для профилирования времени выполнения кода. Класс похож:

Class StopWatch {

    long startTime;
    long stopTime;

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

}

Ожидается, что клиент создаст экземпляр StopWatch и вызовет методы. Пользовательский код может испортить использование методов, приводящих к неожиданным результатам. Ex, start(), stop() и getTime() должны быть в порядке.

Этот класс должен быть "переконфигурирован", чтобы пользователь не мог помешать последовательности.

Я предложил использовать настраиваемое исключение, если stop() вызывается перед запуском() или выполняет некоторые проверки if/else, но интервьюер не был удовлетворен.

Есть ли шаблон проектирования для обработки таких ситуаций?

Изменить: можно модифицировать элементы классов и реализации методов.

4b9b3361

Ответ 1

Мы обычно используем 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.

Прямо вперед.

Ответ 2

Во-первых, существует тот факт, что реализация собственного профилировщика Java - пустая трата времени, поскольку доступны хорошие (возможно, это было замысел вопроса).

Если вы хотите применить правильный порядок методов во время компиляции, вам нужно вернуть что-то с каждым методом в цепочке:

  • start() должен возвращать WatchStopper с помощью метода stop.
  • Затем WatchStopper.stop() должен вернуть WatchResult с помощью метода getResult().

Внешняя конструкция этих вспомогательных классов, а также другие способы доступа к их методам должны быть предотвращены, конечно.

Ответ 3

При незначительных изменениях интерфейса вы можете сделать последовательность методов единственной, которая может быть вызвана - даже во время компиляции!

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(), но также и потенциально хороший дизайн. Все тогда будут зависеть от точных требований...

Ответ 4

Возможно, он ожидал этой "реконфигурации", и вопрос не касался последовательности методов:

class StopWatch {

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

Ответ 5

После получения более продуманной мысли

Оглядываясь назад, похоже, что они искали выполнить вокруг шаблона. Обычно они используются для выполнения таких действий, как принудительное закрытие потоков. Это также более актуально в связи с этой строкой:

Существует ли шаблон проектирования для обработки таких ситуаций?

Идея заключается в том, что вы даете то, что делает "выполнение вокруг" некоторого класса, чтобы что-то делать с. Вероятно, вы используете 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 хороша тем, что

  • Стандартный класс java, а не ваш собственный или сторонний
  • Конечные пользователи могут использовать все, что им нужно, в Runnable.

(Если вы использовали его для принудительного закрытия потока, тогда вы могли бы выполнить действия, которые должны выполняться с подключением к базе данных внутри, поэтому конечному пользователю не нужно беспокоиться о том, как его открывать и закрывать, и одновременно вы вынуждаете чтобы закрыть его правильно.)

Если бы вы хотели, вы могли бы сделать несколько StopWatchWrapper, а оставить StopWatch немодифицированным. Вы также можете сделать measureAction(Runnable) не возвращать время и сделать getTime() public вместо этого.

Способ Java 8 называть его еще проще

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

Третья (надеюсь, окончательная) мысль: кажется, что искал интервьюер, и то, что больше всего нравится, бросает исключения, основанные на состоянии (например, если 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;
        // ...
    }
}

Просто потому, что это бросание IllegalStateException не означает, что правильная последовательность принудительно применяется, это просто означает, что ненадлежащим последовательностям отказано (и я думаю, что все мы согласны с исключениями, это раздражает, к счастью, это не проверенное исключение).

Единственный способ, которым я знаю, чтобы действительно обеспечить, чтобы методы вызывались правильно, - это сделать это самостоятельно с шаблоном выполнения вокруг или другими предложениями, которые делают такие вещи, как return RunningStopWatch и StoppedStopWatch, которые, как я полагаю, имеют только один метод, но это кажется слишком сложным (и 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%, - это то, как получить время. Честно говоря, это выглядит более мелкой. Пока оба ... в приведенном выше коде получают текущее время так же, как должно быть хорошо.

Ответ 6

Выбрасывание исключения, когда методы не вызываются в правильном порядке, является общим. Например, Thread start будет вызывать IllegalThreadStateException, если вызывается дважды.

Вероятно, вам следовало лучше объяснить, как экземпляр узнает, вызваны ли методы в правильном порядке. Это можно сделать, введя переменную состояния и проверив состояние в начале каждого метода (и, при необходимости, обновив его).

Ответ 7

Я предлагаю что-то вроде:

interface WatchFactory {
    Watch startTimer();
}

interface Watch {
    long stopTimer();
}

Он будет использоваться следующим образом

 Watch watch = watchFactory.startTimer();

 // Do something you want to measure

 long timeSpentInMillis = watch.stopTimer();

Вы не можете ссылаться на что-либо в неправильном порядке. И если вы вызываете stopTimer дважды, вы получаете осмысленный результат и время (возможно, лучше переименовать его в measure и возвращать фактическое время при каждом вызове)

Ответ 8

Это также можно сделать с помощью Lambdas в Java 8. В этом случае вы передаете свою функцию классу 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}
}

Ответ 9

Предположительно, причина использования секундомера заключается в том, что объект, который интересуется временем, отличается от объекта, ответственного за запуск и остановку интервалов времени. Если это не так, шаблоны, использующие неизменяемые объекты и позволяющие коду запрашивать секундомер в любое время, чтобы узнать, сколько времени прошло до настоящего времени, вероятно, будут лучше, чем те, которые используют изменяемый объект секундомера.

Если ваша цель состоит в том, чтобы собирать данные о том, сколько времени тратится на выполнение различных вещей, я бы предложил, чтобы вам лучше всего служил класс, который строит список связанных с синхронизацией событий. Такой класс может предоставить способ генерации и добавления нового события, связанного с хронометражем, которое будет записывать моментальный снимок его созданного времени и предоставить способ для указания его завершения. Внешний класс также предоставит метод для извлечения списка всех событий синхронизации, зарегистрированных на сегодняшний день.

Если код, создающий новое событие синхронизации, предоставляет параметр, указывающий его назначение, код в конце, который проверяет список, может определить, были ли все инициированные события надлежащим образом завершены и идентифицировать все, что не было; он также может определить, были ли какие-либо события полностью заключены внутри других или перекрывались другими, но не содержались в них. Поскольку каждое событие имеет свой собственный независимый статус, неспособность закрыть одно событие не должно мешать каким-либо последующим событиям или вызывать потерю или повреждение связанных с ними временных данных (как это может произойти, если, например, секундомер был случайно оставлен, когда он должен были остановлены).

В то время как, безусловно, возможно иметь изменяемый класс секундомера, который использует методы start и stop, если намерение состоит в том, что каждое действие "stop" должно быть связано с конкретным действием "начать", имея действие "начать" вернуть объект, который должен быть "остановлен", не только обеспечит такую ​​связь, но и позволит достичь разумного поведения, даже если действие начато и прекращено.

Ответ 10

Я знаю, что на это уже был дан ответ, но не удалось найти ответ, вызывающий построитель с интерфейсами для потока управления, поэтому вот мое решение: (Назовите интерфейсы лучше, чем я: 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(), чтобы вызвать в End

Например,

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] 1037мс с меткой [до цикла 1]
Mark: [after loop 2] 2008ms с метки [после цикла 1]