- 投稿日:2021-07-15T17:53:02+09:00
【Java発展学習12日目】テストの分類とその品質保証、リファクタリングについて
品質段階 プログラムの開発品質を判定する段階は、以下の通り。 段階 状態 内容 判定方法 1 コンパイルエラー 文法上の誤り コンパイル 2 ランタイムエラー・異常終了 プログラム実行中に異常終了が発生 テスト 3 仕様非準拠 正常終了するものの仕様通りでない テスト 4 完成 全機能が仕様通りに正常終了 テスト 4段階のテスト テストを段階別に分類すると、以下のようになる。 テスト テスト対象 単体テスト(UT; Unit Test) ある一つの「クラス」または「機能」 結合テスト(IT; Integration Test) 単体テストが完了した複数の「クラス」・「機能」 総合テスト(ST; System Test) 結合テストが完了した全ての「クラス」・「機能」 受け入れテスト(UAT; User Acceptance Test) システム一式 ( 発注者が実施 ) 3種類のテスト テストを目的別に分類すると、以下のようになる。 テスト 実施者 内容 顧客要件テスト 顧客またはプロジェクト 仕様通りの機能であることを確認 品質保証テスト プロジェクト 性能および信頼性の改善・保証 開発者テスト 開発者 設計および開発の洗練 テストケース(test case) テストケースは正常系と異常系の2種類を組み合わせて作成する。 同値分割(equivalence partitioning)は、有効値と無効値の集合の中から、任意の代表値を選択し使用する手法であり、 境界値分析(boundary value analysis)は、条件分岐の境界値付近の値を使用する手法である。 MavenプロジェクトでのJUnitの利用(VSCode) Visual Studio CodeのMavenプロジェクトでJUnitを導入する場合、 テストクラスで利用するAssertクラスの静的メソッドと、 テストメソッドを表す@Testアナテイションを定義するTestクラスをインポートする必要がある。 サンプルコード src/main/TestClass.java public class TestClass { private String str = ""; private int i = 0; // コンストラクタ public TestClass(){} public TestClass(String str) { this.str = str; } public TestClass(int i) { this.i = i; } public TestClass(String str, int i) { this.str = str; this.i = i; } // ゲッタ public String getStr() { return this.str; } public int getI() { return this.i; } // セッタ public void setStr(String str) { this.str = str; } public void setI(int i) { this.i = i; } @Override public String toString() { return "(" + this.str + ", " + this.i + ")"; } public void plus(int i) { this.i += i; } public void minus(int i) { this.i -= i; } } src/test/TestClassTest.java import static org.junit.Assert.*; import org.junit.Test; public class TestClassTest { // インスタンス化テスト // -> テストメソッドには@Testアナテイションを付与 @Test public void instantiate() { // インスタンスの生成 TestClass tc = new TestClass("ABC", 123); assertEquals("ABC", tc.getStr()); assertEquals(123, tc.getI()); } // 増減テスト @Test public void plusMinus() { // インスタンスの生成 TestClass tc = new TestClass(); tc.setI(123); tc.plus(27); assertEquals(150, tc.getI()); tc.minus(27); assertEquals(123, tc.getI()); } } JUnitによるテスト % mvn test [INFO] Scanning for projects... ... [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running TestClassTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.02 s - in TestClassTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.314 s [INFO] Finished at: 2021-07-15T13:10:04+09:00 [INFO] ------------------------------------------------------------------------ アサーション(assertion) アサーションを用いることで、テスト対象クラスの各メソッドに対して、想定値でない場合はプログラムを強制停止させることができる。 ただし、アサーション機能を有効するためには-eaオプションをつけてクラスファイルを実行する必要がある。 アサーション機能を有効にしたプログラム実行 % java -ea <メインクラスのFQCN> 契約による設計(DbC; Design by Contract) 主なアサーションの利用場面は、以下の通り。 場面 内容 呼び出し時 引数が正常値(=nullでない)であることを保証 処理の終了時 戻り値や実行結果が正常値であることを保証 常時 インスタンスの内部状態が正常値であることを保証 サンプルコード TestClass.java public class TestClass { private String str = ""; private int i = 0; // コンストラクタ public TestClass(){} public TestClass(String str) { this.str = str; } public TestClass(int i) { this.i = i; } public TestClass(String str, int i) { this.str = str; this.i = i; } // ゲッタ public String getStr() { return this.str; } public int getI() { return this.i; } // セッタ public void setStr(String str) { this.str = str; } public void setI(int i) { this.i = i; } @Override public String toString() { return "(" + this.str + ", " + this.i + ")"; } public void plus(int i) { this.i += i; if (i >= 0) { // アサーションによるテスト assert this.i >= (this.i - i); } else { assert this.i < (this.i - i); } } public void minus(int i) { this.i -= i; if (i >= 0) { // アサーションによるテスト(エラーメッセージつき) assert this.i <= (this.i + i) : "calculation has failed."; } else { assert this.i > (this.i + i) : "calculation has failed."; } } } メトリクス ソフトウェア開発で利用される主なメトリクスは、以下の通り。 メトリクス 視覚化対象 コードカバレッジ テスト品質 LOC(Lines of Code) プログラム規模 CC(Cyclomatic Complexity; 循環的複雑度) メソッドの複雑度※分岐・ループ処理によって数値が上昇 WWC(Weighted Method Per Class) クラス内の全メソッドのCCの合計値 コードカバレッジ(code coverage) テスト品質のメトリクスとなるコードカバレッジの種類は、主に以下の3つ。 レベル カバレッジ 計算式 C0 命令網羅率 テストで通過する命令数 / コード全体の命令数 × 100 [%] C1 分岐網羅率 テストで通過する分岐経路数 / コード全体の分岐経路数 × 100 [%] C2 条件網羅率 テストで通過する単純分岐経路数 / コード全体の複数条件分岐経路数 × 100 [%] Jacoco カバレッジを計測する場合、OSSであるJacocoライブラリを利用する。 また、MavenプロジェクトでJacocoを利用する場合、POMファイルは以下のように追記することで、 MavenセントラルからJacocoのJARファイルを自動でダウンロードすることができる。 pom.xml <!-- 追記箇所(dependenciesタグ) --> <dependency> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.7</version> </dependency> <!-- 追記箇所(build/pluginManagement/pluginsタグ) --> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.7</version> </plugin> Jacocoによるカバレッジ計測 % mvn jacoco:prepare-agent test jacoco:report [INFO] Scanning for projects... ... [INFO] --- jacoco-maven-plugin:0.8.7:report (default-cli) @ maven --- [INFO] Loading execution data file <Mavenプロジェクトのディレクトリパス>/target/jacoco.exec [INFO] Analyzed bundle 'maven' with 2 classes [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.804 s [INFO] Finished at: 2021-07-15T15:43:33+09:00 [INFO] ------------------------------------------------------------------------ Jacocoによるカバレッジ計測を実行すると、target/site/jacocoフォルダに カバレッジ計測結果が記述されたHTMLファイルが自動生成される。 リファクタリングの手法 主なリファクタリング手法は、以下の通り。 重複部分をメソッドで再定義 メソッドやクラスの分割 外部ライブラリや直近バージョンのJavaライブラリでの代用 コメントの修正 静的コード解析(static code analysis) 静的コード解析を行う主な開発ツールは、以下の通り。 ツール 解析内容 SpotBugs バグが生じる可能性のある記述 Checkstyle コーディングスタイル pom.xml <!-- 追記箇所(dependenciesタグ) --> <!-- SpotBugs --> <dependency> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs-maven-plugin</artifactId> <version>4.2.3</version> </dependency> <!-- Checkstyle --> <dependency> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-checkstyle-plugin</artifactId> <version>3.1.2</version> </dependency> <!-- 追記箇所(build/pluginManagement/pluginsタグ) --> <!-- SpotBugs --> <plugin> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs-maven-plugin</artifactId> <version>4.2.3</version> </plugin> <!-- Checkstyle --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-checkstyle-plugin</artifactId> <version>3.1.2</version> </plugin> <!-- 追記箇所(reporting/pluginsタグ) --> <!-- SpotBugs --> <plugin> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs-maven-plugin</artifactId> <version>4.2.3</version> </plugin> <!-- Checkstyle --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-checkstyle-plugin</artifactId> <version>3.1.2</version> </plugin> SpotBugs/Checkstyleによる静的コード解析 % mvn site [INFO] Scanning for projects... ... [INFO] Generating "SpotBugs" report --- spotbugs-maven-plugin:4.2.3:spotbugs [INFO] Generating "Checkstyle" report --- maven-checkstyle-plugin:3.1.2:checkstyle ... 用語集 用語 内容 スコープ(scope) 開発プロジェクトの作業範囲。 テストクラス(テストドライバ) テスト対象クラスのメインクラスを担うクラス。 テスティングフレームワーク(testing framework) テストケースの記述・実行・検証方法を定めた機能群。 パラメトリックテスト(parametric test) テストデータを微調整しながら複数回実行するテスト。 アサーション(assertion) テストケースをメインコードに記述できる仕組み。 コードカバレッジ(code coverage) テストにおいて、各テストケースによる命令文(=コード)の網羅(=カバー)率を定量的に測定する指標。 メトリクス(metrics) 品質や事象を数値化したもの。 技術的負債(technical debt) ソースコードの複雑さ。 リグレッションテスト(regression test) リファクタリング後に再度実施する単体テスト。 デグレード(degrade) リファクタリングの結果、バグが混入すること。
- 投稿日:2021-07-15T16:21:28+09:00
Couchbase Java SDK解説:分散トランザクションプログラミング入門
概要 分散トランザクションによって、複数のドキュメントにまたがるドキュメントの変更(ミューテーション )、つまりインサート、リプレース、およびデリートに対して、一つトランザクションスコープ内で実行することができるようになります。トランザクションがコミットされるまで、これらのミューテーションは、他のトランザクション(Couchbaseデータプラットフォームの他の部分のどこにも)に反映されません。 Couchbaseの分散トランザクションは、次のような特徴を持ちます。 開発者がトランザクションをロジックのブロックとして表現できる、高レベルで使いやすいAPI。 高いパフォーマンスとスケーラビリティをサポートするための、リードコミットの分離レベル。 エラー処理(他のトランザクションとの競合に関するものを含む) 要件 製品バージョン Couchbase Server6.6.1以降 Couchbase Javaクライアント3.1.5以降 前提 Couchbaseクラスターの全てのノードが同期するようにNTPが構成されている アプリケーションが、予約されている拡張属性(XATTR)フィールドを使用していない。 バケットにドキュメントのレプリカが設定されている。 実行準備 Couchbaseトランザクションライブラリをプロジェクトに追加します。 Gradleの例: dependencies { compile group: 'com.couchbase.client', name: 'couchbase-transactions', version: '1.1.8' } Mavenの例: <dependency> <groupId>com.couchbase.client</groupId> <artifactId>couchbase-transactions</artifactId> <version>1.1.8</version> </dependency> これにより、Couchbase Java SDKを含め、このライブラリの推移的な依存関係が自動的に取り込まれます。 インポートするCouchbaseパッケージは次のとおりです。 import com.couchbase.client.core.cnc.Event; import com.couchbase.client.core.cnc.RequestSpan; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryProfile; import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.tracing.opentelemetry.OpenTelemetryRequestSpan; import com.couchbase.transactions.TransactionDurabilityLevel; import com.couchbase.transactions.TransactionGetResult; import com.couchbase.transactions.TransactionQueryOptions; import com.couchbase.transactions.TransactionResult; import com.couchbase.transactions.Transactions; import com.couchbase.transactions.config.PerTransactionConfigBuilder; import com.couchbase.transactions.config.TransactionConfigBuilder; import com.couchbase.transactions.deferred.TransactionSerializedContext; import com.couchbase.transactions.error.TransactionCommitAmbiguous; import com.couchbase.transactions.error.TransactionFailed; import com.couchbase.transactions.log.IllegalDocumentState; import com.couchbase.transactions.log.LogDefer; import com.couchbase.transactions.log.TransactionCleanupAttempt; import com.couchbase.transactions.log.TransactionCleanupEndRunEvent; 以下のコードでは、下記のパッケージも利用しています。 import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; import io.opentelemetry.api.trace.Span; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; コード例による解説 Transactionsオブジェクトの構築 アプリケーションは、Transactionsオブジェクトをただ一つだけ持ちます。 // Couchbaseクラスターの初期化 Cluster cluster = Cluster.connect("localhost", "Administrator", "password"); Bucket bucket = cluster.bucket("travel-sample"); Collection collection = bucket.defaultCollection(); // シングルTransactionsオブジェクトの作成 Transactions transactions = Transactions.create(cluster); 耐久性(Durability)設定 Transactionsオブジェクト作成時に、以下のようにオプションを指定することができます。 Transactions transactions = Transactions.create(cluster, TransactionConfigBuilder.create().durabilityLevel(TransactionDurabilityLevel.PERSIST_TO_MAJORITY) .logOnFailure(true, Event.Severity.WARN) .build()); デフォルト構成では、Majority耐久性設定を使用してすべての変更(ミューテーション)が実行されます。つまり、トランザクションが続行される前に、書き込みが過半数のレプリカに対してメモリレベルで実行されます。 上記の例では、TransactionDurabilityLevel.PERSIST_TO_MAJORITYによって、すべてのミューテーションが、トランザクションが続行される前に、アクティブノードとレプリカノードの過半数に対して、物理ストレージに書き込まれます。これにより、レイテンシは高くなりますが、永続化に関する高い信頼性要件を実現することができます。 Couchbaseの永続化オプションには、Noneのレベルも存在しますが(この場合、アクティブノードのメモリ上への反映が行われた後、レプリカへの反映や物理ストレージへの反映が行われる前に、処理が続行されます)、トランザクション操作においては推奨されておらず、サポートされていません。耐久性がNoneに設定されている場合、トランザクションの原子性は保証されません。 トランザクション・プログラミングの基本 トランザクション・プログラミングの基本的な考え方としては、アプリケーションがラムダブロックとしてロジックを提供し、トランザクションライブラリがトランザクションのコミットを処理します。 ここで、ラムダは、複数回実行される可能性があることを理解しておく必要があります(別のトランザクションとの一時的な競合など、一部のタイプの一時的なエラーを処理するため)。 Couchbase Javaクライアントと同様に、トランザクションライブラリは、同期と非同期のどちらのモードでも使用できます。 同期モード try { transactions.run((ctx) -> { // 'ctx'はAttemptContextオブジェクトです。 // ... トランザクションロジックは、このラムダブロック内に記述されます ... // この'ctx.commit()'メソッドの呼び出しはオプションです。 // この記述が省略されていても、ラムダを抜ける際に、コミットされます。 ctx.commit(); }); } catch (TransactionCommitAmbiguous e) { // 単純化のため、標準エラー出力にログを出力 System.err.println("Transaction possibly committed"); for (LogDefer err : e.result().log().logs()) { System.err.println(err.toString()); } } catch (TransactionFailed e) { System.err.println("Transaction did not reach commit point"); for (LogDefer err : e.result().log().logs()) { System.err.println(err.toString()); } } 同期モードの方が、記述・理解が簡単ですが、非同期APIを使用すると、スレッドプールを使用せずに、リアクティブスタイルでアプリケーションを構築できます。これにより、リソース利用効率が高まり、よりスケーリングできます。 非同期モード(Reactorライブラリを使用) Mono<TransactionResult> result = transactions.reactive().run((ctx) -> { // 'ctx'はAttemptContextオブジェクトです。providing asynchronous versions of the // AttemptContext methods return // トランザクションロジックは、このラムダブロック内に記述されます。: 例として、'get'と'remove'を利用 ctx.get(collection.reactive(), "document-id").flatMap(doc -> ctx.remove(doc)) // この'ctx.commit()'メソッドの呼び出しはオプションです。 // この記述が省略されていても、ラムダを抜ける際に、コミットされます。 .then(ctx.commit()); }).doOnError(err -> { if (err instanceof TransactionCommitAmbiguous) { // 単純化のため、標準エラー出力にログを出力 System.err.println("Transaction possibly committed: "); } else { System.err.println("Transaction failed: "); } for (LogDefer e : ((TransactionFailed) err).result().log().logs()) { System.err.println(err.toString()); } }); // 通常、さらに非同期で処理を連鎖させます。 // 単純化のため、ここで結果をブロックしています TransactionResult finalResult = result.block(); リアクティブプログラミング一般に関係する注意点 一部のAttemptContextReactiveメソッド、 例えば、remove、ではMono<Void>が返されます。 これは、次のイベントをトリガーするのではなく、完了イベントをトリガーします。そのため、ここではthenを使用することになります。flatMapまたはそれに類したものを使用するのではないことに注意してください。 サンプルコード 主なトランザクション操作の簡単な要約を、コメントを交えたサンプルコードとして、示します。 同期モード try { TransactionResult result = transactions.run((ctx) -> { // ドキュメント(doc-a)をデータベースにインサート ctx.insert(collection, "doc-a", JsonObject.create()); // ドキュメント(doc-a)をデータベースから取得 // ドキュメントが存在するかどうか不明な場合は、ctx.getOptionalを使用 Optional<TransactionGetResult> docOpt = ctx.getOptional(collection, "doc-a"); // ドキュメントが必ず存在する場合は、ctx.getを使用 // ドキュメントが存在しない場合、トランザクションが失敗します TransactionGetResult docA = ctx.get(collection, "doc-a"); // ドキュメント(doc-b)を取得し、値を追加した後、 // 変更されたドキュメントで、データベース内のドキュメントをリプレイス TransactionGetResult docB = ctx.get(collection, "doc-b"); JsonObject content = docB.contentAs(JsonObject.class); content.put("transactions", "are awesome"); ctx.replace(docB, content); // ドキュメント(doc-c)の取得と削除 TransactionGetResult docC = ctx.get(collection, "doc-c"); ctx.remove(docC); ctx.commit(); }); } catch (TransactionCommitAmbiguous e) { System.err.println("Transaction possibly committed"); for (LogDefer err : e.result().log().logs()) { System.err.println(err.toString()); } } catch (TransactionFailed e) { System.err.println("Transaction did not reach commit point"); for (LogDefer err : e.result().log().logs()) { System.err.println(err.toString()); } } 非同期モード Mono<TransactionResult> result = transactions.reactive().run((ctx) -> { return // ドキュメント(doc-a)をデータベースにインサート ctx.insert(collection.reactive(), "doc-a", JsonObject.create()) // ドキュメント(doc-b)を取得し、値を追加した後、 // 変更されたドキュメントで、データベース内のドキュメントをリプレイス .then(ctx.get(collection.reactive(), "doc-b")).flatMap(docB -> { JsonObject content = docB.contentAs(JsonObject.class); content.put("transactions", "are awesome"); return ctx.replace(docB, content); }) // ドキュメント(doc-c)の取得と削除 .then(ctx.get(collection.reactive(), "doc-c")).flatMap(doc -> ctx.remove(doc)) // コミット .then(ctx.commit()); }).doOnError(err -> { if (err instanceof TransactionCommitAmbiguous) { System.err.println("Transaction possibly committed: "); } else { System.err.println("Transaction failed: "); } for (LogDefer e : ((TransactionFailed) err).result().log().logs()) { // 単純化のため、標準エラー出力にログを出力 System.err.println(err.toString()); } }); // 通常、さらに非同期で処理を連鎖させます。 // 単純化のため、ここで結果をブロックしています result.block(); 最後に 今回は、Couchbase分散トランザクションプログラミングの入門編として、サンプルコードを、コメントを交えて、紹介するところまで行いました。 別の機会に、より詳細な解説を発表したいと思います。 参考情報 Couchbase公式ドキュメント Java SDK / Advanced Data Operations / Distributed ACID Transactions Javadoc couchbase-transactions API
- 投稿日:2021-07-15T08:35:05+09:00
JEP 400: UTF-8 by Default
動きがあったので備忘。 file.encodingやnative.encoding(SE 17以降)で Java SE API default文字セットに一律:UTF-8が指定される仕様変更。 従来はplatform defaultでした。 JEPで提唱の通り、主にアジアロケール設定のWindowsユーザに影響あり。 つまり日本っすね。。。 In other environments, the risk of changing the default charset to UTF-8 after more than twenty years may be significant. We expect the main impact will be to users of Windows in Asian locales, and possibly some server environments in Asian and other locales. Possible scenarios include:(割愛) JEP現時点(2021/7/15)での情報によれば、コード変更したくない場合には-Dfile.encoding=COMPATで回避。 今般OpenJDK masterブランチに修正がcommitされました。 https://github.com/openjdk/jdk/pull/4733/commits/107210cf466f87ca84c1209e33934565576c6f59
- 投稿日:2021-07-15T07:58:56+09:00
Java Stream API を無頓着に使うのはやめよう
はじめに Java 8で導入されたStream APIはとても便利なので、筆者もふだんから開発で活用しています。 ところが、Stream APIを使うことで逆に可読性が落ちたり、見通しが悪くなっているコードというのもしばしば目にします。 本稿では、可読性が高く見通しの良いコード(クリーンコード)を書くためのテクニックを紹介します。 なお、本稿のソースコードはAdoptOpenJDK 16で動作確認をしました。 こんなコード書いてませんか? 簡単なサンプルとして、以下のレシピ検索サービスの実装を題材にします。 RecipeService.java // レシピ検索サービス public class RecipeService { private Recipes recipes; public RecipeService(Recipes recipes) { this.recipes = recipes; } // 指定した材料を含み、指定した時間(分)内に作れるレシピを、評価の高い順に3つ推薦する public List<Recipe> getRecommendation(String ingredientName, int minutes) { return recipes.recipes().stream() .filter(r -> r.cookingMinutes() <= minutes) .filter(r -> r.ingredients().stream().anyMatch( i -> i.name().equals(ingredientName))) .sorted(Comparator.comparing(Recipe::stars).reversed()) .limit(3L).toList(); } } 「指定した材料を含み、指定した時間(分)内に作れるレシピを、評価の高い順に3つ推薦する」というアプリケーション仕様をStream APIを使った処理として実装しています。 サンプルなのでそこまで読みにくいということもないのですが、実際のプロダクトコードはもっと複雑になりがちです。 例えば、 filterに渡すラムダ式がワンライナーではなく、長いコードブロックとなっている ラムダ式が何重にもネストされている といった具合で可読性がひどく低下したコードを目にすることがあります。 たしかにStream API/ラムダ式を使って記述した処理は、以前のforループを使ったJavaコードと比べてより宣言的で読みやすさが向上します。しかしStream API/ラムダ式を使っただけではクリーンなコードとは言えません。 以降、サンプルを少しずつリファクタリングしていきましょう。 説明変数 まずはサンプルの以下の部分に注目します。 RecipeService.java .filter(r -> r.cookingMinutes() <= minutes) .filter(r -> r.ingredients().stream().anyMatch( i -> i.name().equals(ingredientName))) ネストされているものも含めて3つのラムダ式があります。それぞれ短いものですが、何をする処理なのか、読み手がコードを読み解き理解をする必要があります。 説明変数とは処理結果をローカル変数に代入し、その変数に意図を込めた名前を与えることで、コードの見通しをよくする実装テクニックです。 Javaのラムダ式は関数インタフェースを実装するコード断片なので、対応する関数インタフェース型の変数に代入して使うことができます。 リファクタリングして以下のようになりました。 RecipeService.java // 指定した材料を含み、指定した時間(分)内に作れるレシピを、評価の高い順に3つ推薦する public List<Recipe> getRecommendation(String ingredientName, int minutes) { // 説明変数 Predicate<Recipe> thatIsCookedInSpecifiedMinutes = r -> r.cookingMinutes() <= minutes; Predicate<Recipe> thatContainsIngredientWithSpecifiedName = r -> r.ingredients().stream() .anyMatch(i -> i.name().equals(ingredientName)); return recipes.recipes().stream() .filter(thatIsCookedInSpecifiedMinutes) .filter(thatContainsIngredientWithSpecifiedName) .sorted(Comparator.comparing(Recipe::stars).reversed()) .limit(3L).toList(); } Stream APIを使った処理が、変更前より読みやすくなったと思います。thatIs... という述語的な命名をすることで、自然な英語に近い感じで読めるように工夫しました。 sortedに渡しているComparatorにも説明変数を適用してみましょう。 RecipeService.java // 指定した材料を含み、指定した時間(分)内に作れるレシピを、評価の高い順に3つ推薦する public List<Recipe> getRecommendation(String ingredientName, int minutes) { // 説明変数 Predicate<Recipe> thatIsCookedInSpecifiedMinutes = r -> r.cookingMinutes() <= minutes; Predicate<Recipe> thatContainsIngredientWithSpecifiedName = r -> r.ingredients().stream() .anyMatch(i -> i.name().equals(ingredientName)); Comparator<Recipe> byStarsDescending = Comparator.comparing(Recipe::stars).reversed(); return recipes.recipes().stream() .filter(thatIsCookedInSpecifiedMinutes) .filter(thatContainsIngredientWithSpecifiedName) .sorted(byStarsDescending) .limit(3L).toList(); } すっきりしました。 ついでにもう一つリファクタリング。Predicateは合成関数andを持っているので、filter処理は1回にまとめることができます。 RecipeService.java return recipes.recipes().stream() .filter(thatIsCookedInSpecifiedMinutes .and(thatContainsIngredientWithSpecifiedName)) .sorted(byStarsDescending) .limit(3L).toList(); ドメインオブジェクトに振る舞いを持たせる 次に、処理自体がサービスに記述するのがふさわしいのかを考えます。 ドメイン駆動設計(DDD)の考え方では、ビジネス上の重要な知識やルールは、エンティティ、値オブジェクト、ドメインサービスなどのドメイン層のオブジェクトに実装するのがよいとされます。 何が重要な知識やルールかは、そのドメインの専門家との対話を通して見出していく必要がありますが、ここでは仮に「評価の高い順に3つ推薦する」という仕様がそれにあたるとします。 この振る舞いを、Recipes(レシピ集)クラスへ移動させます。 Recipes.java // レシピ集。子に複数のレシピを持つ public record Recipes(List<Recipe> recipes) { // レシピの推薦 public List<Recipe> recommend(Predicate<Recipe> bySearchCondition) { Comparator<Recipe> byStarsDescending = Comparator.comparing(Recipe::stars).reversed(); return recipes.stream() .filter(bySearchCondition) .sorted(byStarsDescending) .limit(3L).toList(); } } どんな条件でレシピを検索するかはユースケース毎によると考え、Predicate型の引数で渡すようにしました。 また、小さな工夫ですが、メソッド名をgetRecommendationという味気のない名前からrecommendに変更することで、ドメインオブジェクトが生き生きとしてきました(気がします)。 サービス側の実装は以下のように変わります。 RecipeService.java // 指定した材料を含み、指定した時間(分)内に作れるレシピを、評価の高い順に3つ推薦する public List<Recipe> getRecommendation(String ingredientName, int minutes) { // 説明変数 Predicate<Recipe> thatIsCookedInSpecifiedMinutes = r -> r.cookingMinutes() <= minutes; Predicate<Recipe> thatContainsIngredientWithSpecifiedName = r -> r.ingredients().stream() .anyMatch(i -> i.name().equals(ingredientName)); return recipes.recommend( thatIsCookedInSpecifiedMinutes.and( thatContainsIngredientWithSpecifiedName)); } Tell, Don't Ask ここまででかなりコードの見通しがよくなりましたが、まだ気になる点があります。 RecipeService.java Predicate<Recipe> thatContainsIngredientWithSpecifiedName = r -> r.ingredients().stream() .anyMatch(i -> i.name().equals(ingredientName)); このラムダ式では、レシピの材料リストを取得して材料名がマッチするものがあるかどうかを判定していますが、ちょっと処理が長く、書き過ぎている感がありますね。 Tell, Don't Askという有名なオブジェクト指向設計原則があります。日本語に訳すと「尋ねるな、命じよ。」です。 何かを達成するために、呼び出し側から細かなデータをオブジェクトに尋ねて処理を行うのではなく、そのデータを持っているオブジェクトに対して何をしてほしいのかを命じよ、というものです。 データを管理するオブジェクトに振る舞いを持たせるという、オブジェクト指向の基本ですね。 Recipe(レシピ)クラスに振る舞いを追加します。 Recipe.java // レシピ。子に複数の材料を持つ。 public record Recipe(String name, int cookingMinutes, List<Ingredient> ingredients, int stars) { public boolean hasAnyIngredients(Predicate<Ingredient> predicate) { return ingredients.stream().anyMatch(predicate); } } サービス側の実装は、この新しく追加されたメソッドを使って以下のようになります。 RecipeService.java Predicate<Recipe> thatContainsIngredientWithSpecifiedName = r -> r.hasAnyIngredients(i -> i.name().equals(ingredientName)); さらに同様のリファクタリングをいくつか加えて、最終的にサービスの実装は以下となりました。 RecipeService.java // 指定した材料を含み、指定した時間(分)内に作れるレシピを、評価の高い順に3つ推薦する public List<Recipe> getRecommendation(String ingredientName, int minutes) { // 説明変数 Predicate<Recipe> thatIsCookedInSpecifiedMinutes = r -> r.canBeCookedIn(minutes); Predicate<Recipe> thatContainsIngredientWithSpecifiedName = r -> r.hasAnyIngredients(i -> i.isNamed(ingredientName)); return recipes.recommend( thatIsCookedInSpecifiedMinutes.and( thatContainsIngredientWithSpecifiedName)); } まとめ Java8でのStream API/ラムダ式の導入は画期的なもので、これによりJavaプログラムも関数型指向、宣言的な記述が可能となりました。 一方でその便利さゆえに何も考えずにユースケース層・サービス層で何でもかんでも処理を書いてしまうと、本来ドメインオブジェクトに持たせてしかるべきロジックがドメイン層の外側に漏れ出してしまい、結果としてドメインモデル貧血症というアンチパターンに陥ってしまいます。 振る舞いをどこに置くか、はまさに設計行為そのものです。Stream APIを使う際にもそれを意識するかしないかで設計品質に差が生まれます。また、実装コードに説明変数などのテクニックを用いて可読性を向上することは、その後の保守性担保に寄与します。 本稿でご紹介したテクニックが役立つ場面があれば幸いです。 (参考)ソースコード リファクタリング後の最終的なソースコードです。 Recipes.java // レシピ集。子に複数のレシピを持つ public record Recipes(List<Recipe> recipes) { // レシピの推薦 public List<Recipe> recommend(Predicate<Recipe> bySearchCondition) { Comparator<Recipe> byStarsDescending = Comparator.comparing(Recipe::stars).reversed(); return recipes.stream() .filter(bySearchCondition) .sorted(byStarsDescending) .limit(3L).toList(); } } Recipe.java // レシピ。子に複数の材料を持つ。 public record Recipe(String name, int cookingMinutes, List<Ingredient> ingredients, int stars) { public boolean hasAnyIngredients(Predicate<Ingredient> predicate) { return ingredients.stream().anyMatch(predicate); } public boolean canBeCookedIn(int minutes) { return cookingMinutes <= minutes; } } Ingredient.java // 材料 public record Ingredient(String name) { public boolean isNamed(String name) { return this.name.equals(name); } } RecipeService.java // レシピ検索サービス public class RecipeService { private Recipes recipes; public RecipeService(Recipes recipes) { this.recipes = recipes; } // 指定した材料を含み、指定した時間(分)内に作れるレシピを、評価の高い順に3つ推薦する public List<Recipe> getRecommendation(String ingredientName, int minutes) { // 説明変数 Predicate<Recipe> thatIsCookedInSpecifiedMinutes = r -> r.canBeCookedIn(minutes); Predicate<Recipe> thatContainsIngredientWithSpecifiedName = r -> r.hasAnyIngredients(i -> i.isNamed(ingredientName)); return recipes.recommend( thatIsCookedInSpecifiedMinutes.and( thatContainsIngredientWithSpecifiedName)); } }
- 投稿日:2021-07-15T03:51:31+09:00
[Processing] 筒を描く
こんな プログラム コンストラクタの引数はとりあえず2種類 Tube(半径,淵の厚さ,長さ,角度) Tube(半径,淵の厚さ,角度) 使う時は tube.draw(角度); tube.draw(); 難点はインスタンス生成時にstrokeとfillが決まること Tube.java Tube t; void setup() { size(800, 800, P3D); frameRate(30); t = new Tube(200, 50, 500, 360); } void draw() { background(50); translate(width/2, height/2); rotateY(frameCount * 0.016f); t.draw(frameCount); //t.draw(); } public class Tube { private PVector[] inLine, outLine; public float radius; public float width,length; public int maxRadian; public PShape[] tube; public Tube(float radius, float width,float length, int maxRadian) { this.radius = radius; this.width = width; this.length = length; this.maxRadian = min(max(maxRadian, 0), 360) + 1;//clamp init(); } public Tube(float radius, float width, int maxRadian){ this(radius,width,width,maxRadian); } private void init() { inLine = new PVector[maxRadian]; outLine = new PVector[maxRadian]; float z = this.length * 0.5f; for (int i=0; i<maxRadian; i++) { float x = radius * cos(radians(i - 90)); float y = radius * sin(radians(i - 90)); PVector in = new PVector(x, y, z); x = this.width * cos(radians(i - 90)); y = this.width * sin(radians(i - 90)); PVector out = PVector.add(in, new PVector(x, y)); inLine[i] = in; outLine[i] = out; } tube = createTube(maxRadian); } private PShape[] createTube(int radian) { PShape[] tube = new PShape[6]; float z = this.length * 0.5f; for (int i=0; i<6; i++) { tube[i] = createShape(); } tube[0].beginShape(TRIANGLE_STRIP);//手前 tube[1].beginShape(TRIANGLE_STRIP);//奥 tube[2].beginShape(QUAD_STRIP);//内側 tube[3].beginShape(QUAD_STRIP);//外側 for (int i=0; i<radian; i++) { tube[0].vertex(inLine[i].x, inLine[i].y, z); tube[0].vertex(outLine[i].x, outLine[i].y, z); tube[1].vertex(inLine[i].x, inLine[i].y, -z); tube[1].vertex(outLine[i].x, outLine[i].y, -z); tube[2].vertex(inLine[i].x, inLine[i].y, -z); tube[2].vertex(inLine[i].x, inLine[i].y, z); tube[3].vertex(outLine[i].x, outLine[i].y, -z); tube[3].vertex(outLine[i].x, outLine[i].y, z); } tube[0].endShape(); tube[1].endShape(); tube[2].endShape(); tube[3].endShape(); //始まりの蓋 tube[4].beginShape(); tube[4].vertex(inLine[0].x, inLine[0].y, -z); tube[4].vertex(outLine[0].x, outLine[0].y, -z); tube[4].vertex(outLine[0].x, outLine[0].y, z); tube[4].vertex(inLine[0].x, inLine[0].y, z); tube[4].endShape(); //終わりの蓋 int last = radian -1; tube[5].beginShape(); tube[5].vertex(inLine[last].x, inLine[last].y, -z); tube[5].vertex(outLine[last].x, outLine[last].y, -z); tube[5].vertex(outLine[last].x, outLine[last].y, z); tube[5].vertex(inLine[last].x, inLine[last].y, z); tube[5].endShape(); return tube; } private void draw(PShape[] tube){ for (int i=0; i<tube.length; i++) { shape(tube[i]); } } public void draw(int radian) { radian = max(radian, 0); if (radian >= maxRadian) { draw(); return; } PShape[] t = createTube(radian); draw(t); } public void draw() { draw(tube); } }
- 投稿日:2021-07-15T00:46:40+09:00
デザインパターン勉強会⑤Singleton
はじめに ZOOM勉強会の議事録です。 「増補改訂版Java言語で学ぶデザインパターン入門」を読んで、プログラム作成・パターンの理解を行います。 第5回はSingletonです。 Singleton インスタンスが1つしか無いことを保証するパターンです。 Singletonクラスの役割は以下です。 Singleton唯一のインスタンスを提供するクラス また、コードは以下です。 Singleton Public Class Singleton '唯一のインスタンス Private Shared singleton = New Singleton() '外部からはインスタンス化できない Private Sub New() End Sub '唯一のインスタンスを返す Public Shared Function GetInstance() As Singleton Return singleton End Function End Class Singletonパターンでは、以下の手順で唯一のインスタンスを保証しています。 コンストラクタをPrivateにし、外部からはインスタンス生成できない フィールドに唯一のインスタンスを用意し、クラスのロード時にのみインスタンス化する メリット インスタンスが1つであることが保証される 1クラス内で完結しているパターンのため導入が簡単 デメリット 多くの場所からSingletonクラスを使用している場合、Singletonクラスの変更が多くの場所に影響を及ぼす Singletonの派生クラスはSingletonでなくなるため、Singletonにしたい場合は再度実装する必要がある 使う側はこのクラスがSingletonであり、独自に定義されたメソッドでインスタンスを取得すると知っていなければならない 議論 調べたところSingletonは、デザインパターンの中でも不評なようです。 その理由として、Singletonは実質グローバル変数であるというような理由が見受けられました。 Singletonを活用する方法の議論 あまりSingletonは使わない方がいいのではという話しになる一方で、状態を持たない定数のような形であればいいのではという意見が出ました。 例えばマルバツゲームのマスの状態を表す「◯・✕・空」などの状態そのものをクラスで表現する場合です。 以下のような形です。 Maru Public Class Maru : Implements ISquare Private Shared ReadOnly maru = New Maru() Private Sub New() End Sub Public Shared Function GetInstance() As ISquare Return maru End Function Public Function GetSquareImage() As Image Implements ISquare.GetSquareImage Return My.Resources.Maru End Function End Class このようにすることで、盤上にある同一の状態を1つのインスタンスのみで表現できます。 するとメモリの使用量が節約できるという意見でした。 Singletonに代わる方法 Singletonがだめなら他の方法が無いか調べ、アジャイルソフトウェア開発の奥義 第2版でMonostateパターンというものを見つけました。 Monostate インスタンス自体を一つにするのでなく、インスタンスの振る舞いを1つにします。 振る舞いを1つにすることで別々のインスタンスがあたかも1つのインスタンスかのように振る舞います。 Monostate Public Class Monostate Private Shared itsX As Integer = 0 Public Sub New() End Sub Public Sub SetX(ByVal x As Integer) itsX = x End Sub Public Function GetX() As Integer Return itsX End Function End Class 以上のようにすべてのフィールド変数をShared(javaだとstatic)にし、すべてのインスタンスが、共有するフィールド変数にアクセスすることによって同じ振る舞いを実現しています。 メリット 通常のクラスのようにインスタンス化して使える 派生クラスにフィールドを共有させることができる 静的メソッドを強制されないため、メソッドをオーバーライドして異なる振る舞いを提供できる デメリット Singletonよりインスタンスの生成・削除の機会が多い 以上のようにSingletonが構造で唯一のインスタンスを保証している一方で、Monostateは振る舞いで唯一性を保証しています。 まとめ 第5回はSingletonを勉強しました。 話が色々逸れてしまいましたが、複数の方法を比較するとその方法の何が強くて何が弱いかを理解しやすかったです。 それぞれの強みを知って、適材適所にプログラムを書けたらと思います。