정적 유형을 활용하여 객체 상태 관리

알렉스 블로그
가 발생했습니다 많은 이야기 소프트웨어 엔지니어링 세계에서는 OOP, 특히 변경 가능한 상태와 관련된 문제에 대해 설명합니다. 부작용 없는 함수형 프로그래밍이 종종 해결책으로 선전됩니다. 가변성에 대한 몇 가지 두드러진 점이 있지만 실제 문제는 암시적으로 변경 가능한 개체를 순진하게 사용하는 데서 발생합니다. 적절하게 수행된다면 변경 가능한 상태는 코드를 추론하는 데 유용한 도구가 될 수 있으며 미래의 개발자가 자연스럽고 원활하게 올바른 코드 변경을 할 수 있도록 안내할 수 있습니다. 이 게시물에서 우리는 객체 상태를 나타내기 위해 유형을 사용하는 클래스 설계 원칙으로 이어진 골칫거리 중 일부를 공유할 것입니다.

상태 저장 협력자

예부터 시작해 보겠습니다. Rescale에서는 REST API를 사용하여 작업에 대한 메타데이터를 추적합니다. 작업자 노드는 API에 쿼리하여 스핀업할 가상 하드웨어에 대한 정보를 얻고, 클러스터 노드는 API에 쿼리하여 실행할 분석에 대한 정보를 얻습니다. 둘 다 암호화된 토큰을 전달하여 인증합니다. 첫 번째 클라이언트 인터페이스는 다음과 같습니다.

공용 인터페이스 JobMetadataClient { 분석 getAnalytic(long jobId, 자격 증명 자격 증명); CoreSummary getCoreSummary(long jobId, 자격 증명 자격 증명); ... }

이 인터페이스는 각 요청마다 항상 올바른 자격 증명을 전달하는 데 유용했지만 약간 번거로웠습니다. 새로운 API 클라이언트를 사용하기 위해 이전 코드를 리팩터링하면서 각 메서드 호출에 대한 자격 증명을 만들어야 했습니다. 각 작업자는 작업에 대한 작업을 수행한 다음 해당 작업의 인증 토큰을 사용하여 많은 요청을 하므로 지속적으로 동일한 자격 증명 개체를 생성하거나 전달해야 했습니다. 이로 인해 우리가 원하는 만큼 API에 크게 의존하는 것이 번거로워졌습니다.
다음으로 생각한 것은 작업자가 작업에 대한 작업을 가져올 때 API 클라이언트에 자격 증명을 한 번 설정한 다음 이미 자격 증명을 받았다는 가정 하에 모든 도우미 개체가 클라이언트를 사용하도록 할 수 있다는 것이었습니다. 인터페이스는 다음과 같았습니다.

공용 인터페이스 JobMetadataClient { void setCredentials(자격 증명 자격 증명); 분석 getAnalytic(long jobId); CoreSummary getCoreSummary(long jobId); ... }

이제 대부분의 호출 코드는 자격 증명에 대한 걱정 없이 쿼리 메서드를 호출할 수 있습니다. 이를 통해 API 클라이언트 메서드를 훨씬 쉽게 사용할 수 있다는 목표가 달성되었지만 클라이언트를 코드베이스의 새 영역에 도입할 때마다 자격 증명 설정을 잊어버렸습니다. 또한 실행되는 컨텍스트에 대해 암묵적인 가정을 하는 코드를 작성하게 되었고, 따라서 재사용성이 떨어지고 취약해졌습니다. 시스템의 한 부분을 변경하면 다른 부분이 손상될 가능성이 있으며 이는 소프트웨어 시스템에서 피해야 할 원칙 중 하나입니다.
첫 번째 구현은 단일 상태 협력자라고 부르는 것입니다. 이는 단지 메소드 모음일 뿐이며 개발자는 인스턴스가 있을 때 해당 동작에 대해 쉽게 추론할 수 있습니다. 두 번째는 암시적 상태 변경이 있기 때문에 상황을 그렇게 명확하게 나타내지 않습니다. 개발자가 인스턴스를 확보하면 쿼리 메서드를 호출할 수 있고 해당 메서드가 작동할 가능성이 있다는 것을 알 수 있습니다. 인증 상태를 이해하려면 시스템에 대한 더 많은 지식이 필요합니다.
정적 타이핑을 활용하여 미래 개발자에게 인증 상태를 즉시 명백하게 보여주는 더 나은 디자인이 있습니다. 첫 번째 클라이언트 인터페이스로 돌아가서 모든 메서드 호출에 자격 증명을 요구할 수 있지만 래퍼 클래스도 제공할 수 있습니다. 그 유형은 상태를 나타냅니다.:

공용 인터페이스 JobMetadataClient { 분석 getAnalytic(long jobId, 자격 증명 자격 증명); CoreSummary getCoreSummary(long jobId, 자격 증명 자격 증명); ... } ... public class AuthenticatedMetadataClient { private final Credentials 자격 증명; 개인 최종 JobMetadataClient 메타데이터클라이언트; 공개 분석 getAnalytic(long jobId) { return this.metadataClient.getAnalytic(jobId, this.credentials); } ... }

이제 개발자가 AuthenticatedMetadataClient 인스턴스가 공동 작업자로 있는 클래스에서 작업하는 경우 해당 클래스에 인증이 있고 인증이 손실되지 않는다는 것을 확실히 알 수 있습니다. 생성자에서 AuthenticatedMetadataClient 인스턴스를 사용하는 새 클래스를 작성하는 경우 해당 클래스는 인증이 이미 제공된 경우에만 사용할 수 있습니다. 미래의 개발자는 클래스에서 클라이언트 개체로 무엇을 할 수 있는지 확인하고 IDE는 적절한 방법을 제안할 것입니다. 그들은 시스템의 일부를 추론하기 위해 전체 시스템에 대한 많은 정보를 머릿속에 보관할 필요가 없습니다. 이는 코드베이스 작업을 위한 강력한 도구입니다.

메모리에 상태를 누적하는 중

API 클라이언트에는 괜찮았지만 그 클래스는 실제로는 그렇지 않았습니다. 필요한 것 실제 상태가 API에 보관되어 있기 때문에 상태를 변경합니다. 상태 변경 사항을 유지하기 전에 메모리에 누적하려는 경우는 어떻습니까? Rescale의 코드베이스에서 또 다른 예를 들어보겠습니다. 우리는 초기 매개변수에 대해 다양한 값을 사용하여 분석을 실행하고 "최적" 결과를 선택하는 최적화 소프트웨어를 사용합니다. 우리는 최적의 실행이 결정되면 최적의 실행을 위한 초기 매개 변수 값을 보유하는 CaseWorkflow 클래스를 사용하여 해당 워크플로를 나타냅니다. 우리는 모든 것이 완료된 후에도 해당 값을 유지하고 싶습니다.
따라서 처음에는 단일 메서드로 모든 초기화 및 정리 작업을 수행하는 매우 중요해 보이는 코드가 있었습니다.

공공 무효 runWorkflow(CaseWorkflow 워크플로) { doSomeInitialization1(); doSomeInitialization2(); for(InitialParametersiniParamters:workflow.getParams()) { //매개변수를 실행하고 최적인 경우 워크플로에서 설정합니다. } doSomeCleanup1(); doSomeCleanup2(); //이 예에서 중요한 줄 persistOptimalParameters(workflow.getOptimalParameters()); }

우리는 이것을 다음을 사용하여 리팩토링하기로 결정했습니다. 라이프사이클 리스너 책임을 분리하고 코드를 더 쉽게 이해하고 단위 테스트할 수 있도록 합니다. 우리는 다음과 같은 인터페이스를 작성했습니다.

공용 인터페이스 WorkflowLifecycleListener { void informWorkflowStarting(CaseWorkflow 워크플로); void informParamtersRun(IntialParametersinitialParameters); void informWorkflowCompleted(); }

그리고 다음 리스너를 사용하도록 원래 메서드를 리팩터링했습니다.

public void run(CaseWorkflow 워크플로우, ListenerFactory 공장) { Collection lifecycleListener = Factory.createListeners(workflow); for(WorkflowLifecycleListener 리스너 : lifecycleListeners) { Listener.notifyWorkflowStarting(workflow); } for(InitialParametersinitialParamters:workflow.getParams()) { //프로세스 매개변수 for(WorkflowLifecycleListener 리스너 : lifecycleListeners) { Listener.notifyParametersRun(initialParameters); } } for(WorkflowLifecycleListener 리스너 : lifecycleListeners) { Listener.notifyWorkflowCompleted(); } }

다양한 유형의 워크플로우에 대해 다양한 리스너 세트를 원했기 때문에 팩토리 객체를 사용했지만 여기서는 관련이 없습니다. 관련된 것은 각 리스너 객체가 범위 내에서 생성되어 단일 워크플로에 연결된다는 것입니다. 이것이 바로 당시에 다음 청취자가 의미가 있었던 이유입니다.

공개 클래스 PersistOptimalParametersListener는 WorkflowLifecycleListener를 구현합니다. { private final 초기 매개변수 최적 매개변수; 공개 PersistOptimalParametersListener(CaseWorkflow 워크플로) { this.optimalParamters = 워크플로.getOptimalParameters(); } @Override public void informWorkflowStarting(CaseWorkflow 워크플로우) { } @Override public void informParamtersRun(IntialParameters params) { } @Override void informWorkflowCompleted() { persistOptimalParameters(this.optimalParameters); } }

이 모든 설명을 보면 실수가 분명해 보입니다. 즉, 이 리스너가 인스턴스화될 때 최적의 매개변수가 워크플로우에 설정되지 않았습니다. 당시에는 빈 컬렉션일 뿐이지만 리팩토링 중에 잊어버리기 쉽습니다. 이를 염두에 두려면 전체 최적화 시스템에 대한 많은 맥락이 필요합니다. 우리는 이런 실수를 방지하고 워크플로 상태를 미래의 개발자에게 전달하기 위해 더 세분화된 유형을 사용할 수 있을지 궁금했습니다. 예.
여기서 중요한 문제는 Java에서 일반적인 getter 및 setter 메서드를 사용한다는 것입니다. getOptimalParameters 메소드가 있는 클래스는 해당 메소드가 적절하게 호출될 수 있는 시기를 개발자에게 알려주지 않습니다. 해당 클래스는 자격 증명을 자체적으로 설정할 수 있도록 허용하는 API 클라이언트와 같은 암시적 상태 변경을 사용합니다. 대신 호출하기에 적합하지 않은 경우 해당 메서드가 전혀 포함되지 않도록 개체를 작성해야 합니다.

공용 클래스 CaseWorkflow { ... 공용 CompletedWorkflow 완료(InitalParameters OptimalInitialParameters) { ... } ... } 공용 클래스 CompletedWorkflow 확장 CaseWorkflow { 개인 최종 초기 매개변수 OptimalInitialParameters; 공개 초기 매개변수 getOptimalParamters() { ... } }

첫 번째 예의 AuthenticatedClient와 마찬가지로 이제 CompletedWorkflow에서 작동하는 메서드를 작성하고 해당 상태를 확인할 수 있습니다. 클래스에서 사용 가능한 메서드가 알려주기 때문에 언제 설정되는지에 대한 모든 내용을 기억할 필요가 없습니다.

요약

이러한 예의 공통 요소는 Java의 유형 시스템을 객체의 가능한 상태를 문서화하기 위한 도구로 활용한다는 것입니다. IDE 메서드 제안의 도움으로 정보 유형이 있는 객체에 대한 추론이 자연스럽고 원활해집니다. 또한 유형은 개체 동작을 올바르게 이해하는 데 필요한 컨텍스트를 줄여 생산성에 도움이 됩니다.

저자

  • 애덤 매켄지

    CTO로서 Adam은 HPC 및 고객 성공 팀을 관리하는 책임을 맡고 있습니다. Adam은 Boeing에서 경력을 시작하여 787년 동안 XNUMX을 작업하면서 구조 및 소프트웨어 엔지니어링 프로젝트를 관리하고 날개를 설계, 분석 및 최적화했습니다. Adam은 오레곤 주립대학교에서 우등으로 기계공학 학사학위를 취득했습니다.

비슷한 게시물