Nice programing

Java 디자인 문제 : 메서드 호출 시퀀스 적용

nicepro 2020. 12. 13. 11:05
반응형

Java 디자인 문제 : 메서드 호출 시퀀스 적용


최근 인터뷰에서 저에게 질문 한 질문이 있습니다.

문제 : 코드의 실행 시간을 프로파일 링하기위한 클래스가 있습니다. 수업은 다음과 같습니다.

Class StopWatch {

    long startTime;
    long stopTime;

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

}

클라이언트는 StopWatch의 인스턴스를 만들고 그에 따라 메서드를 호출해야합니다. 사용자 코드는 메서드 사용을 엉망으로 만들어 예기치 않은 결과를 초래할 수 있습니다. 예를 들어, start (), stop () 및 getTime () 호출이 순서대로 이루어져야합니다.

이 클래스는 사용자가 시퀀스를 엉망으로 만드는 것을 방지 할 수 있도록 "재구성"되어야합니다.

start () 전에 stop ()이 호출되거나 if / else 확인을 수행하는 경우 사용자 지정 예외 사용을 제안했지만 인터뷰어가 만족하지 않았습니다.

이러한 상황을 처리 할 수있는 디자인 패턴이 있습니까?

편집 : 클래스 멤버 및 메서드 구현을 수정할 수 있습니다.


우리는 일반적으로 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.

똑바로.


첫 번째는 좋은 것을 사용할 수 있기 때문에 자체 Java 프로파일 러를 구현하는 것은 시간 낭비라는 사실입니다 (아마도 그것이 질문의 의도 였을 것입니다).

컴파일 타임에 올바른 메서드 순서를 적용하려면 체인의 각 메서드와 함께 무언가를 반환해야합니다.

  1. start()WatchStopperstop 메서드를 사용하여 a를 반환해야합니다 .
  2. 그런 다음 메서드 와 함께 WatchStopper.stop()a를 반환 WatchResult해야합니다 getResult().

물론 이러한 도우미 클래스의 외부 생성과 해당 메서드에 액세스하는 다른 방법은 방지되어야합니다.


인터페이스를 약간만 변경하면 컴파일 타임에도 메서드 시퀀스를 호출 할 수있는 유일한 시퀀스로 만들 수 있습니다!

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()방법 을 생략함으로써 할당과는 다르지만 잠재적으로 좋은 디자인이기도합니다. 그러면 모든 것이 정확한 요구 사항에 따라 달라집니다 ...


한번 더 생각하면

돌이켜 보면 그들은 execute around pattern을 찾고있는 것처럼 들린다 . 일반적으로 스트림 종료를 강제하는 것과 같은 작업을 수행하는 데 사용됩니다. 이것은 또한 다음 줄로 인해 더 관련이 있습니다.

이러한 상황을 처리 할 수있는 디자인 패턴이 있습니까?

아이디어는 당신이 무언가를 할 수있는 어떤 클래스를 "실행"하는 것을 제공하는 것입니다. 아마도 사용할 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수 있습니다.

(스트림을 강제로 닫는 데 사용하는 경우 데이터베이스 연결로 수행해야하는 작업을 내부에 배치 할 수 있으므로 최종 사용자는이를 열고 닫는 방법에 대해 걱정할 필요가 없으며 동시에 강제로 닫을 수 있습니다. 제대로.)

원하는 경우 일부 StopWatchWrapperStopWatch수정 하지 않고 그대로 둘 있습니다. measureAction(Runnable)시간을 반환하지 getTime()않고 대신 공개 할 수도 있습니다 .

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고해서 적절한 시퀀스가 ​​적용 된다는 의미가 아니라 부적절한 시퀀스가 ​​거부된다는 의미 일뿐입니다 (예외가 성가시다는 점에 모두 동의 할 수 있다고 생각합니다. 다행히도 이것은 확인 된 예외가 아닙니다).

내가 진정으로 방법이 제대로 호출되어 시행 알고있는 유일한 방법은 주변의 실행 패턴 또는 반환 등의 작업을 수행 할 다른 제안 스스로를하는 것입니다 RunningStopWatchStoppedStopWatch(과 영업 이익 나는 단지 하나 개의 방법이 가정,하지만이 지나치게 복잡 보인다 인터페이스를 변경할 수 없다고 언급했지만 내가 만든 래퍼가 아닌 제안은이를 수행합니다). 따라서 내가 아는 한 인터페이스를 수정하거나 클래스를 추가하지 않고는 적절한 순서 적용 할 방법이 없습니다 .

사람들이 "메서드 호출 시퀀스 적용"을 의미하는 정의에 따라 실제로 달라지는 것 같습니다. 예외 만 발생하면 아래 컴파일됩니다.

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

사실 실행 되지는 않지만 a Runnable를 제출하고 해당 메소드를 비공개로 설정하고 다른 하나는 긴장을 풀고 성가신 세부 사항을 직접 처리하는 것이 훨씬 더 간단 해 보입니다 . 그러면 추측 작업이 없습니다. 이 클래스를 사용하면 순서가 분명하지만 메서드가 더 많거나 이름이 너무 명확하지 않으면 두통이 생길 수 있습니다.


원래 답변

더 많은 후견인 편집 : OP는 주석에서 언급합니다.

"세 가지 메서드는 그대로 유지되어야하며 프로그래머에 대한 인터페이스 일뿐입니다. 클래스 멤버와 메서드 구현은 변경 될 수 있습니다."

따라서 아래는 인터페이스에서 무언가를 제거하기 때문에 잘못되었습니다. (기술적으로는 빈 방법으로 구현할 수 있지만 어리석은 일과 너무 혼란스러워 보입니다.) 제한이 없었고 또 다른 "거짓 증거인 것 같으면이 대답을 좋아합니다." "방법은 그대로 두겠습니다.

나에게는 이와 같은 것이 좋은 것 같습니다.

class StopWatch {

    private final long startTime;

    public StopWatch() {
        startTime = ...
    }

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

이것이 좋다고 생각하는 이유는 기록이 객체 생성 중이기 때문에 잊혀지거나 순서대로 수행 될 수 없기 때문입니다 ( stop()존재하지 않으면 메서드를 호출 할 수 없음 ).

한 가지 결함은 아마도 stop(). 처음에는 아마 생각 lap()했지만 일반적으로 다시 시작하거나 일종의 (또는 적어도 마지막 랩 / 시작 이후 녹음)을 의미합니다. 아마도 read()더 좋을까요? 이것은 스톱워치에서 시간을 보는 동작을 모방합니다. 나는 stop()그것을 원래 수업과 유사하게 유지 하기 결정했습니다 .

내가 100 % 확신하지 못하는 유일한 것은 시간을 얻는 방법입니다. 솔직히 말해서 더 사소한 세부 사항 인 것 같습니다. ...위의 코드에서 둘 다 동일한 방식으로 현재 시간을 얻는 한 괜찮을 것입니다.


아마도 그는이 '재구성'을 예상했고 질문은 메소드 시퀀스에 관한 것이 아닙니다.

class StopWatch {

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

메서드가 올바른 순서로 호출되지 않을 때 예외가 발생하는 것은 일반적입니다. 예를 들어 Thread's startIllegalThreadStateExceptionif 호출이 두 번 발생합니다.

메서드가 올바른 순서로 호출되는지 인스턴스가 어떻게 알 수 있는지 더 잘 설명했을 것입니다. 이는 상태 변수를 도입하고 각 메소드 시작시 상태를 확인하여 수행 할 수 있습니다 (필요한 경우 업데이트).


다음과 같이 제안합니다.

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의 Lambda에서도 수행 할 수 있습니다.이 경우 함수를 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}
}

아마도 스톱워치를 사용하는 이유는 시간에 관심이있는 엔터티가 타이밍 간격을 시작하고 중지하는 엔터티와 구별되기 때문일 것입니다. 그렇지 않은 경우 변경 불가능한 객체를 사용하는 패턴과 코드가 언제든지 스톱워치를 쿼리하여 현재까지 얼마나 많은 시간이 경과했는지 확인하도록 허용하는 것이 변경 가능한 스톱워치 객체를 사용하는 것보다 낫습니다.

귀하의 목적이 다양한 작업을 수행하는 데 얼마나 많은 시간이 소요되는지에 대한 데이터를 캡처하는 것이라면, 타이밍 관련 이벤트 목록을 작성하는 클래스에서 가장 잘 봉사 할 수 있다고 제안합니다. 이러한 클래스는 생성 된 시간의 스냅 샷을 기록하고 완료를 표시하는 방법을 제공하는 새로운 타이밍 관련 이벤트를 생성 및 추가하는 방법을 제공 할 수 있습니다. 외부 클래스는 현재까지 등록 된 모든 타이밍 이벤트 목록을 검색하는 메서드도 제공합니다.

If the code which creates a new timing event supplies a parameter indicating its purpose, code at the end which examines the list could ascertain whether all events that were initiated have been properly completed, and identify any that had not; it could also identify if any events were contained entirely within others or overlapped others but were not contained within them. Because each event would have its own independent status, failure to close one event need not interfere with any subsequent events or cause any loss or corruption of timing data related to them (as might occur if e.g. a stopwatch had been accidentally left running when it should have been stopped).

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

I'm going to suggest that enforcing the method call sequence is solving the wrong problem; the real problem is a unfriendly interface where the user must be aware of the state of the stopwatch. The solution is to remove any requirement to know the state of the 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;
    }

}

This allows a simple use of the mark method with a result returned and logging included.

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

This gives the nice result of;

First mark: [before loop 1] at time 1436537674704
Mark: [after loop 1] 1037ms since mark [before loop 1]
Mark: [after loop 2] 2008ms since mark [after loop 1]

참고URL : https://stackoverflow.com/questions/30888581/java-design-issue-enforce-method-call-sequence

반응형