web-dev-qa-db-fra.com

Java Problème de conception: appliquer la séquence d'appel de méthode

Il y a une question qui m'a été récemment posée lors d'une interview.

Problème: Il existe une classe destinée à profiler le temps d'exécution du code. La classe est comme:

Class StopWatch {

    long startTime;
    long stopTime;

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

}

Le client doit créer une instance du chronomètre et appeler les méthodes en conséquence. Le code utilisateur peut perturber l'utilisation des méthodes conduisant à des résultats inattendus. Les appels Ex, start (), stop () et getTime () doivent être en ordre.

Cette classe doit être "reconfigurée" afin que l'utilisateur ne puisse pas perturber la séquence.

J'ai proposé d'utiliser une exception personnalisée si stop () est appelé avant start (), ou de faire des vérifications if/else, mais l'intervieweur n'était pas satisfait.

Existe-t-il un modèle de conception pour gérer ce genre de situations?

Edit: Les membres de classe et les implémentations de méthode peuvent être modifiés.

57
Mohitt

Nous utilisons couramment StopWatch de Apache Commons StopWatch vérifier le modèle de la façon dont ils ont fourni.

IllegalStateException est levée lorsque l'état du chronomètre est incorrect.

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.

Simple.

23
vels4j

Il y a d'abord le fait que l'implémentation d'un propre Java profiler est une perte de temps, car de bons sont disponibles (c'était peut-être l'intention derrière la question).

Si vous souhaitez appliquer l'ordre des méthodes correct au moment de la compilation, vous devez renvoyer quelque chose avec chaque méthode de la chaîne:

  1. start() doit retourner un WatchStopper avec la méthode stop.
  2. WatchStopper.stop() doit alors renvoyer un WatchResult avec la méthode getResult().

La construction externe de ces classes auxiliaires ainsi que d'autres moyens d'accéder à leurs méthodes doivent bien sûr être évitées.

83
Waog

Avec des modifications mineures de l'interface, vous pouvez faire de la séquence de méthodes la seule qui puisse être appelée - même au moment de la compilation!

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

L'utilisation est simple - chaque méthode renvoie une classe différente qui n'a que les méthodes actuellement applicables. Fondamentalement, l'état du chronomètre est encapsulé dans le système de type.


Dans les commentaires, il a été souligné que même avec la conception ci-dessus, vous pouvez appeler deux fois stop(). Si je considère cela comme une valeur ajoutée, il est théoriquement possible de se visser dessus. Ensuite, la seule façon dont je peux penser serait quelque chose comme ceci:

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

Cela diffère de l'affectation en omettant la méthode stop(), mais c'est aussi potentiellement une bonne conception. Tout dépendrait alors des exigences précises ...

36
Petr Janeček

Peut-être qu'il s'attendait à cette "reconfiguration" et la question ne concernait pas du tout la séquence des méthodes:

class StopWatch {

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

Une fois mûrement réfléchi

Avec le recul, il semble qu'ils recherchaient le exécuter autour du modèle . Ils sont généralement utilisés pour faire des choses comme imposer la fermeture des flux. Ceci est également plus pertinent en raison de cette ligne:

Existe-t-il un modèle de conception pour gérer ce genre de situations?

L'idée est de donner à la classe qui "exécute" quelque chose pour faire quelque chose. Vous utiliserez probablement Runnable mais ce n'est pas nécessaire. (Runnable a le plus de sens et vous verrez pourquoi bientôt.) Dans votre classe StopWatch ajoutez une méthode comme celle-ci

public long measureAction(Runnable r) {
    start();
    r.run();
    stop();
    return getTime();
}

Vous appelleriez alors comme ça

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

Cela le rend infaillible. Vous n'avez pas à vous soucier de la gestion de l'arrêt avant le démarrage ou des personnes oubliant d'appeler l'un et pas l'autre, etc. La raison pour laquelle Runnable est Nice est parce que

  1. Standard Java, pas la vôtre ni celle d'un tiers
  2. Les utilisateurs finaux peuvent mettre tout ce dont ils ont besoin dans le Runnable à faire.

(Si vous l'utilisiez pour forcer la fermeture de flux, vous pouvez mettre les actions qui doivent être effectuées avec une connexion à la base de données à l'intérieur afin que l'utilisateur final n'ait pas à se soucier de la façon de l'ouvrir et de la fermer et que vous les forciez simultanément à fermer correctement.)

Si vous le vouliez, vous pourriez faire du StopWatchWrapper à la place laisser StopWatch non modifié. Vous pouvez également faire en sorte que measureAction(Runnable) ne renvoie pas d'heure et rendre getTime() public à la place.

La méthode Java 8 pour l'appeler est encore plus simple

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

Une troisième pensée (espérons-le, finale): il semble que ce que l'intervieweur recherchait et ce qui est le plus voté est de lancer des exceptions basées sur l'état (par exemple, si stop() est appelée avant start() ou start() après stop()). C'est une bonne pratique et en fait, selon les méthodes dans StopWatch ayant une visibilité autre que privée/protégée, il est probablement préférable d'avoir que de ne pas avoir. Mon seul problème avec ceci est que le fait de lancer des exceptions à lui seul ne fera pas appliquer une séquence d'appel de méthode.

Par exemple, considérez ceci:

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

Ce n'est pas parce qu'il lance IllegalStateException que la bonne séquence est appliquée, cela signifie simplement que les séquences incorrectes sont refusées (et je pense nous pouvons tous convenir que les exceptions sont ennuyeuses, heureusement, il ne s'agit pas d'une exception vérifiée).

La seule façon que je connaisse pour vraiment appliquer les méthodes sont appelées correctement est de le faire vous-même avec le modèle d'exécution autour ou les autres suggestions qui font des choses comme return RunningStopWatch et StoppedStopWatch que je présume avoir une seule méthode, mais cela semble trop complexe (et OP a mentionné que l'interface ne pouvait pas être modifiée, il est vrai que la suggestion de non-wrapper que j'ai faite le fait). Donc, à ma connaissance, il n'y a aucun moyen de appliquer le bon ordre sans modifier l'interface ou ajouter plus de classes .

Je suppose que cela dépend vraiment de ce que les gens définissent comme "appliquer une séquence d'appel de méthode". Si seules les exceptions sont levées, alors la compilation ci-dessous

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

Il est vrai que ce ne sera pas run, mais il semble tellement plus simple de remettre un Runnable et de rendre ces méthodes privées, laissez l'autre se détendre et gérer vous-même les détails embêtants . Ensuite, il n'y a pas de devinettes. Avec cette classe, c'est évident l'ordre, mais s'il y avait plus de méthodes ou les noms n'étaient pas si évidents, cela peut commencer à être un casse-tête.


Réponse originale

Plus de modification avec le recul : OP mentionne dans un commentaire,

"Les trois méthodes doivent rester intactes et ne sont qu'une interface avec le programmeur. Les membres de la classe et l'implémentation de la méthode peuvent changer."

Donc, ce qui suit est faux car il supprime quelque chose de l'interface. (Techniquement, vous pouvez l'implémenter comme une méthode vide, mais cela semble être une chose stupide à faire et trop confuse.) J'aime un peu cette réponse si la restriction n'était pas là et qu'elle semble être une autre "preuve à toute épreuve" "façon de le faire donc je vais le laisser.

Pour moi, quelque chose comme ça semble être bon.

class StopWatch {

    private final long startTime;

    public StopWatch() {
        startTime = ...
    }

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

La raison pour laquelle je pense que cela est bon, c'est que l'enregistrement est en cours de création d'objet donc il ne peut pas être oublié ou fait dans le désordre (ne peut pas appeler la méthode stop() si elle n'existe pas).

Un défaut est probablement la dénomination de stop(). Au début, je pensais que peut-être lap() mais cela implique généralement un redémarrage ou une sorte (ou au moins un enregistrement depuis le dernier tour/départ). Peut-être que read() serait mieux? Cela imite l'action de regarder l'heure sur un chronomètre. J'ai choisi stop() pour le garder similaire à la classe d'origine.

La seule chose dont je ne suis pas sûr à 100% est de savoir comment obtenir le temps. Pour être honnête, cela semble être un détail plus mineur. Tant que les deux ... Dans le code ci-dessus obtiennent l'heure actuelle de la même manière, cela devrait aller.

19
Captain Man

Je suggère quelque chose comme:

interface WatchFactory {
    Watch startTimer();
}

interface Watch {
    long stopTimer();
}

Il sera utilisé comme ceci

 Watch watch = watchFactory.startTimer();

 // Do something you want to measure

 long timeSpentInMillis = watch.stopTimer();

Vous ne pouvez rien invoquer dans le mauvais ordre. Et si vous appelez stopTimer deux fois, vous obtenez un résultat significatif à la fois (peut-être qu'il vaut mieux le renommer en measure et renvoyer l'heure réelle à chaque fois qu'il est invoqué)

5
talex

Lancer une exception lorsque les méthodes ne sont pas appelées dans le bon ordre est courant. Par exemple, Thread's start lancera un IllegalThreadStateException s'il est appelé deux fois.

Vous auriez probablement dû mieux expliquer comment l'instance saurait si les méthodes sont appelées dans le bon ordre. Cela peut être fait en introduisant une variable d'état et en vérifiant l'état au début de chaque méthode (et en le mettant à jour si nécessaire).

5
Eran

Vraisemblablement, la raison d'utiliser un chronomètre est que l'entité qui s'intéresse au temps est distincte de l'entité qui est responsable du démarrage et de l'arrêt des intervalles de temps. Si ce n'est pas le cas, les modèles utilisant des objets immuables et permettant au code d'interroger un chronomètre à tout moment pour voir combien de temps s'est écoulé à ce jour seraient probablement meilleurs que ceux utilisant un objet chronomètre mutable.

Si votre but est de capturer des données sur le temps passé à faire diverses choses, je dirais que vous pourriez être mieux servi par une classe qui construit une liste d'événements liés au timing. Une telle classe peut fournir une méthode pour générer et ajouter un nouvel événement lié à la synchronisation, qui enregistrerait un instantané de son heure créée et fournirait une méthode pour indiquer son achèvement. La classe externe fournirait également une méthode pour récupérer une liste de tous les événements de synchronisation enregistrés à ce jour.

Si le code qui crée un nouvel événement de temporisation fournit un paramètre indiquant son objectif, un code à la fin qui examine la liste pourrait vérifier si tous les événements qui ont été lancés ont été correctement terminés et identifier ceux qui ne l'ont pas été; il pourrait également identifier si des événements étaient entièrement contenus dans d'autres ou se chevauchaient mais n'étaient pas contenus en eux. Étant donné que chaque événement aurait son propre statut indépendant, le fait de ne pas fermer un événement ne doit pas interférer avec les événements ultérieurs ni entraîner la perte ou la corruption des données de synchronisation qui leur sont liées (comme cela pourrait se produire si, par exemple, un chronomètre avait été accidentellement laissé en marche alors qu'il le devrait). ont été arrêtés).

Bien qu'il soit certainement possible d'avoir une classe de chronomètre mutable qui utilise les méthodes start et stop, si l'intention est que chaque action "stop" soit associée à une action "start" particulière, ayant la " start "action return un objet qui doit être" stoppé "assurera non seulement une telle association, mais permettra un comportement sensible à atteindre même si une action est démarrée et abandonnée.

3
supercat

Cela peut également être fait avec Lambdas dans Java 8. Dans ce cas, vous passez votre fonction à la classe StopWatch puis dites au StopWatch d'exécuter ce code .

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

Je sais que cela a déjà été répondu, mais je n'ai pas trouvé de réponse invoquant le générateur avec interfaces pour le flux de contrôle, voici donc ma solution: (Nommez les interfaces mieux que moi: 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;
    }

}

Utilisation:

StoppingStopWatch sw = StopWatch.createAndStart();
//Do stuff
long time = sw.stop().getTime();
1
NeeL

Je vais suggérer que l'application de la séquence d'appel de méthode résout le mauvais problème; le vrai problème est une interface peu conviviale où l'utilisateur doit être au courant de l'état du chronomètre. La solution consiste à supprimer toute exigence de connaître l'état du chronomètre.

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

}

Cela permet une utilisation simple de la méthode mark avec un résultat retourné et une journalisation incluse.

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

Cela donne le résultat Nice de;

Première marque: [avant la boucle 1] à l'instant 1436537674704
Marque: [après la boucle 1] 1037 ms depuis la marque [avant la boucle 1]
Marque: [après la boucle 2] 2008 ms depuis la marque [après la boucle 1]

0
Qwerky

Selon la question de l'entrevue, il semble aimer ceci

Class StopWatch {

    long startTime;
    long stopTime;
    public StopWatch() {
    start();
    }

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

}

}

Alors maintenant, tous les utilisateurs doivent créer un objet de la classe StopWatch au début et getTime () doit appeler à la fin

Par exemple

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