静的型付けを利用してオブジェクトの状態を管理する

アレックスブログ
があった たくさんの話 ソフトウェア エンジニアリングの世界では、OOP と特に可変状態に関連する苦痛について説明します。 副作用のない関数型プログラミングは、解決策としてよく宣伝されます。 変更可能性についてはいくつかの顕著な点がありますが、実際の問題は、暗黙的に変更可能なオブジェクトを単純に使用することから発生します。 適切に実行されれば、可変状態はコードについて推論するための便利なツールとなり、将来の開発者を自然かつシームレスに正しいコード変更に導くことができます。 この投稿では、型を使用してオブジェクトの状態を示すというクラス設計の原則に至った頭痛の種をいくつか共有します。

ステートフル コラボレーター

例から始めましょう。 Rescale では、REST API を使用してジョブに関するメタデータを追跡します。 ワーカー ノードは API にクエリを実行して、起動する仮想ハードウェアに関する情報を取得し、クラスター ノードは API にクエリを実行して、実行する分析に関する情報を取得します。 どちらも、暗号化されたトークンを渡すことによって認証されます。 最初のクライアント インターフェイスは次のようになりました。

public Interface JobMetadataClient { Analysis getAnalysis(long jobId, Credentials credentials); Coresummary getCoresummary(long jobId, Credentials 資格情報); ... }

このインターフェイスは、各リクエストで常に正しい認証情報を確実に渡すのに適していましたが、少し面倒でした。 新しい API クライアントを使用するために古いコードをリファクタリングしたため、メソッド呼び出しごとに資格情報を作成する必要がありました。 各ワーカーはジョブのタスクを取得し、そのジョブの認証トークンを使用して多くのリクエストを行うため、常に同一の認証情報オブジェクトを作成するか、認証情報オブジェクトを渡す必要がありました。 そのため、API にどれだけ依存するかが面倒になってしまいました。
次に考えたのは、ワーカーがジョブのタスクを取得するときに API クライアントに認証情報を一度設定させ、その後、認証情報をすでに取得しているものとしてすべてのヘルパー オブジェクトがそのクライアントを使用できるようにすることでした。 そのときのインターフェースは次のようなものでした。

public Interface JobMetadataClient { void setCredentials(Credentials credentials); 分析 getAnalysis(long jobId); Coresummary getCoresummary(long jobId); ... }

ほとんどの呼び出しコードは、資格情報を気にせずにクエリ メソッドを呼び出すことができるようになりました。 これにより、API クライアント メソッドの使用がはるかに簡単になるという目標は達成されましたが、コードベースの新しい領域にクライアントを導入するたびに、資格情報の設定を忘れてしまいます。 また、実行されるコンテキストについて暗黙の仮定を置くコードを作成することになり、そのため再利用性が低く、より脆弱になりました。 システムの一部を変更すると、他の部分が破損する可能性があります。これは、ソフトウェア システムでは避けるべき原則の XNUMX つです。
最初の実装は、単一状態コラボレーターと私が呼ぶものです。 これはメソッドの単なるバッグであり、開発者はインスタンスを持っていればその動作を簡単に推論できます。 XNUMX つ目は、暗黙的な状態変更があるため、それほど明らかではありません。 開発者がインスタンスを取得すると、クエリ メソッドを呼び出すことができ、それらのメソッドはおそらく機能することがわかります。 認証状態を理解するには、システムに関するより多くの知識が必要です。
静的型付けを活用して、将来の開発者に認証状態がすぐにわかるようにする、より優れた設計があります。 最初のクライアント インターフェイスに戻って、すべてのメソッド呼び出しで認証情報を要求できますが、ラッパー クラスも提供します。 その型がその状態を示す:

public Interface JobMetadataClient { Analysis getAnalysis(long jobId, Credentials credentials); Coresummary getCoresummary(long jobId, Credentials 資格情報); ... } ... public class AuthenticatedMetadataClient { private 最終認証情報 認証情報; プライベート最終 JobMetadataClient メタデータクライアント; public Analysis getAnalysis(long jobId) { return this.metadataClient.getAnalysis(jobId, this.credentials); } ... }

これで、開発者がコラボレータとして AuthenticatedMetadataClient のインスタンスを持つクラスを操作する場合、そのクラスに認証があり、それが失われないことが確実にわかります。 コンストラクターで AuthenticatedMetadataClient のインスタンスを受け取る新しいクラスを作成する場合、それらのクラスは認証がすでに提供されている場合にのみ使用できます。 将来の開発者は、クライアント オブジェクトで何ができるかをクラスから確認し、IDE が適切なメソッドを提案します。 システムの一部について推論するために、システム全体に関する多くの情報を頭の中に保持する必要がなくなります。 これらはコードベースで作業するための強力なツールです。

メモリ内の状態の蓄積

API クライアントにとってはこれで問題ありませんでしたが、そのクラスでは実際にはそうではありませんでした。 必要 実際の状態は API に保持されているため、状態を変更できます。 状態の変更を永続化する前にメモリに蓄積したい場合はどうすればよいでしょうか? Rescale のコードベースから別の例を見てみましょう。 当社では、初期パラメーターの値を変化させて分析を実行し、「最適な」結果を選択する最適化ソフトウェアを使用しています。 そのワークフローを、最適な実行が決定された後の初期パラメーターの値を保持するクラス (たとえば、CaseWorkflow) で表します。 すべてが完了したら、これらの値を永続化したいと考えています。
したがって、最初は、すべての初期化とクリーンアップのアクションを XNUMX つのメソッドで実行する、非常に命令的に見えるコードがありました。

public void runWorkflow(CaseWorkflow ワークフロー) { doSomeInitialization1(); doSomeInitialization2(); for(InitialParametersInitialParamters:workflow.getParams()) { //パラメータを実行し、最適であればワークフローに設定します } doSomeCleanup1(); doSomeCleanup2(); //この例の重要な行persistOptimalParameters(workflow.getOptimalParameters()); }

これを次を使用してリファクタリングすることにしました ライフサイクルリスナー 責任を分離し、コードの理解と単体テストを容易にします。 次のようなインターフェースを書きました。

public Interface WorkflowLifecycleListener { void NoticeWorkflowStarting(CaseWorkflow workflow); voidnotifyParamtersRun(IntialParametersinitialParameters); void notificationWorkflowCompleted(); }

そして、これらのリスナーを使用するために元のメソッドをリファクタリングしました。

public void run(CaseWorkflow ワークフロー, ListenerFactory Factory) { 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(); } }

さまざまな種類のワークフローに対してさまざまなリスナーのセットが必要だったので、ファクトリ オブジェクトを使用しましたが、それはここでは関係ありません。 関連するのは、各リスナー オブジェクトがスコープ内で作成され、単一のワークフローに関連付けられていることです。 だからこそ、当時は次のようなリスナーの発言が理にかなっていました。

public class PersistOptimalParametersListener は WorkflowLifecycleListener を実装します。 public PersistOptimalParametersListener(CaseWorkflow ワークフロー) { this.optimalParamters = workflow.getOptimalParameters(); @Override public void NoticeWorkflowStarting(CaseWorkflow workflow) { } @Override public void NoticeParamtersRun(IntialParameters params) { } @Override void NoticeWorkflowCompleted() {persistOptimalParameters(this.optimalParameters); } }

これだけ説明すると、間違いは明らかです。このリスナーがインスタンス化される時点で、ワークフローに最適なパラメーターが設定されていないのです。 その時点では、それらは単なる空のコレクションですが、リファクタリングの最中では、それは忘れがちです。 このことを念頭に置くには、最適化システム全体について多くのコンテキストを理解する必要があります。 私たちは、この間違いを防ぎ、ワークフローの状態を将来の開発者に伝えるために、よりきめの細かい型を使用できないだろうかと考えました。 はい。
ここでの重要な問題は、Java で一般的なゲッター メソッドとセッター メソッドの使用でした。 getOptimalParameters メソッドを持つクラスは、そのメソッドをいつ適切に呼び出すことができるかを開発者に伝えません。 このクラスは、資格情報の設定をそれ自体に許可する API クライアントと同様に、暗黙的な状態変更を使用します。 代わりに、呼び出しが適切でない場合には、それらのメソッドがまったく含まれないようにオブジェクトを作成する必要があります。

public class CaseWorkflow { ... public CompletedWorkflow complete(InitialParameters最適InitialParameters) { ... } ... } public class CompletedWorkflow extends CaseWorkflow { private Final InitialParameters最適InitialParameters; public InitialParameters getOptimalParamters() { ... } }

最初の例の AuthenticatedClient と同様に、CompletedWorkflow で動作し、その状態を確認するメソッドを作成できるようになりました。 クラスで利用可能なメソッドが教えてくれるので、何がいつ設定されるかをすべて覚えておく必要はありません。

まとめ

これらの例に共通する要素は、オブジェクトの可能な状態を文書化するツールとして Java の型システムを活用していることでした。 IDE メソッドの提案の助けを借りて、有益な型を持つオブジェクトについての推論が自然かつスムーズになります。 また、これらの型により、オブジェクトの動作を正しく理解するために必要なコンテキストが削減され、生産性が向上します。

著者

  • Adam McKenzie

    アダムは CTO として、HPC チームとカスタマー サクセス チームの管理を担当しています。 アダムはボーイングでキャリアをスタートし、787 年間 XNUMX に取り組み、主翼の設計、分析、最適化を行う構造およびソフトウェア エンジニアリング プロジェクトを管理しました。 アダムはオレゴン州立大学で機械工学の学士号を優秀な成績で取得しています。

類似の投稿