Java Design Issue: Enforce method call sequence

后端 未结 12 580
青春惊慌失措
青春惊慌失措 2021-02-02 05:27

There is a question which was recently asked to me in an interview.

Problem: There is a class meant to profile the execution time of the code. The class

12条回答
  •  野的像风
    2021-02-02 06:01

    Once given more thought

    In hindsight it sounds like they were looking for the execute around pattern. They're usually used to do things like enforce closing of streams. This is also more relevant due to this line:

    Is there a design pattern to handle these kind of situations?

    The idea is you give the thing that does the "executing around" some class to do somethings with. You'll probably use Runnable but it's not necessary. (Runnable makes the most sense and you'll see why soon.) In your StopWatch class add some method like this

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

    You would then call it like this

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

    This makes it fool proof. You don't have to worry about handling stop before start or people forgetting to call one and not the other, etc. The reason Runnable is nice is because

    1. Standard java class, not your own or third party
    2. End users can put whatever they need in the Runnable to be done.

    (If you were using it to enforce stream closing then you could put the actions that need to be done with a database connection inside so the end user doesn't need to worry about how to open and close it and you simultaneously force them to close it properly.)

    If you wanted, you could make some StopWatchWrapper instead leave StopWatch unmodified. You could also make measureAction(Runnable) not return a time and make getTime() public instead.

    The Java 8 way to calling it is even simpler

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

    A third (hopefully final) thought: it seems what the interviewer was looking for and what is being upvoted the most is throwing exceptions based on state (e.g., if stop() is called before start() or start() after stop()). This is a fine practice and in fact, depending on the methods in StopWatch having a visibility other than private/protected, it's probably better to have than not have. My one issue with this is that throwing exceptions alone will not enforce a method call sequence.

    For example, consider this:

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

    Just because it's throwing IllegalStateException doesn't mean that the proper sequence is enforced, it just means improper sequences are denied (and I think we can all agree exceptions are annoying, luckily this is not a checked exception).

    The only way I know to truly enforce that the methods are called correctly is to do it yourself with the execute around pattern or the other suggestions that do things like return RunningStopWatch and StoppedStopWatch that I presume have only one method, but this seems overly complex (and OP mentioned that the interface couldn't be changed, admittedly the non-wrapper suggestion I made does this though). So to the best of my knowledge there's no way to enforce the proper order without modifying the interface or adding more classes.

    I guess it really depends on what people define "enforce a method call sequence" to mean. If only the exceptions are thrown then the below compiles

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

    True it won't run, but it just seems so much simpler to hand in a Runnable and make those methods private, let the other one relax and handle the pesky details yourself. Then there's no guess work. With this class it's obvious the order, but if there were more methods or the names weren't so obvious it can begin to be a headache.


    Original answer

    More hindsight edit: OP mentions in a comment,

    "The three methods should remain intact and are only interface to the programmer. The class members and method implementation can change."

    So the below is wrong because it removes something from the interface. (Technically, you could implement it as an empty method but that seems to be like a dumb thing to do and too confusing.) I kind of like this answer if the restriction wasn't there and it does seem to be another "fool proof" way to do it so I will leave it.

    To me something like this seems to be good.

    class StopWatch {
    
        private final long startTime;
    
        public StopWatch() {
            startTime = ...
        }
    
        public long stop() {
            currentTime = ...
            return currentTime - startTime;
        }
    }
    

    The reason I believe this to be good is the recording is during object creation so it can't be forgotten or done out of order (can't call stop() method if it doesn't exist).

    One flaw is probably the naming of stop(). At first I thought maybe lap() but that usually implies a restarting or some sort (or at least recording since last lap/start). Perhaps read() would be better? This mimics the action of looking at the time on a stop watch. I chose stop() to keep it similar to the original class.

    The only thing I'm not 100% sure about is how to get the time. To be honest that seems to be a more minor detail. As long as both ... in the above code obtain current time the same way it should be fine.

提交回复
热议问题