Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ユニットテストしやすいコードを書くために必要な考え方&実践的Tips

SYM
March 02, 2024

 ユニットテストしやすいコードを書くために必要な考え方&実践的Tips

ユニットテストしやすいコードを書くために必要な(ベースの)考え方・Tips を自分なりにまとめた物

※社内で行った勉強会のために作ったスライドの元スライド

SYM

March 02, 2024
Tweet

More Decks by SYM

Other Decks in Programming

Transcript

  1. Contents • 前置き • 品質とは? 保守性とは? ◦ 保守性が向上すると開発生産性の向上にも繋がる Why? • 本題:ユニットテストしやすいコードにするには?

    ◦ 保守性(特に△△性)の向上を追い求めれば自ずと近づく、そこに +1 する ▪ 保守性の向上に必要な考え方: 〇〇と✕✕ • 説明のための 実践的 Tips ◦ 1.概要と詳細 ◦ 2.具体と抽象  ※ここでの抽象 ≠ インターフェース ◦ 3.知識とロジック ▪ 補足:ドメインオブジェクト ◦ 4.ロジックと境界 ▪ 補足:デザインパターン ※強力な組み合わせ1つ説明 ▪ +1とは? • まとめ 2
  2. 前置き • もし、このような未来になるなら … 何が足りない? リプレイス できた! テストが  充実した! 開発

    生産性 向上 実際は… 思ってたように 開発生産性が 上がらない? こんなはずでは… その他諸々 できた! • (末端の) 開発プロセスを変えるためには、依存する物の改善が必須(コンウェイの法則と同様に) ➡ 内部品質(保守性) ➡ コードベース 3 理想 現実
  3. 品質とは? なぜ保守性か? • 製品品質:製品が顧客の要求や期待を満たすか ◦ 外部品質:エンドユーザにとっての価値 ◦ 内部品質:システム開発者にとっての価値 • SQuaREの品質特性(ソフトウェア製品品質)8項目

    機能適合性 機能がユーザのニーズを満たすか 性能効率性 実行時の性能やリソースの使用効率が良いか 互換性 他のシステムや製品と共に動作するか 使用性 ユーザーが容易に理解し、効果的、効率的に使用できるか 信頼性 一定期間正確かつ適切に動作するか セキュリティ 権限のない使用者からデータを保護できるか 保守性 ソフトウェアが修正・変更しやすいか 移植性 異なる環境間でソフトウェアを移行しやすいか 他を優先するあまり犠牲にしてしまうことも … 4 • ユーザやビジネスサ イドから求められる? • そもそも低いとサービ スとして成立しない? ※元ネタ:某氏の「質とスピード」のスライド
  4. 品質とは?- 内部品質(保守性)を上げると? • Why? ◦ 我々がコードを修正する時の大まかな作業 向上 開発生産性 の向上 コードを読む

    (考える) ・コードが理解しやすい ・修正箇所の特定が容易 コードを書く ・コードが修正しやすい ・再利用できる部品有 コードを読む (考える) コードを書く (横軸:所要時間) 開発 生産性 外部品 質 内部品質 (保守性) 更にバグに関して言えば ・バグの原因特定しやすい ・バグを作りこみにくい 5 • 保守性 ◦ モジュール性(独立した部品から成るか) ◦ 再利用性(使いまわせるか) ◦ 解析性 ≒ 理解容易性(コードの可読性が高いか) ◦ 修正性 ≒ 変更容易性(修正や変更がしやすいか) ◦ 試験性 ≒ テスト容易性(テストが実行しやすいか)
  5. 品質とは?- 内部品質(保守性) • 保守性 ◦ モジュール性(独立した部品から成るか) ◦ 再利用性(使いまわせるか) ◦ 解析性

    ≒ 理解容易性(コードの可読性が高いか) ◦ 修正性 ≒ 変更容易性(修正や変更がしやすいか) ◦ 試験性 ≒ テスト容易性(テストが実行しやすいか) Q.5つの中で どれが一番鍵と なりそうか? モジュール性 再 利 用 性 理 解 容 易 性 変 更 容 易 性 テス ト容 易 性 モジュール性が高い=独立した部品が多い  独立しているからこそ… • 他の場所で使い回しやすい • (単一責任で単純明快なら ) 理解しやすい • 変更を加えても他への影響が小さい • 単体でテストがしやすい 6
  6. 本題 - ユニットテストしやすいコードにするには? • 保守性(特にモジュール性)を上げるために必要な考え方: 保守性(特にモジュール性) を追い求めれば自然と近づく、そこに +1 する ▪

    この過程でモジュール性の 向上と共にテストしやすいコードも組み上がっていく • 説明のための実践的 Tips ◦ 1.概要と詳細 ◦ 2.具体と抽象 ◦ 3.知識とロジック ▪ 補足:ドメインオブジェクト ◦ 4.ロジックと境界 ▪ 補足:デザインパターン  ◦ 分解 と 集約 ▪ 小さな(独立した)部品に分け、組み合わせて全体を成す ≒ オブジェクト指向 • 小さくまとめて単体でも全体でも分かりやすくする Sampleコードは全てJava ※コードの内容より構造に 着目ください
  7. 本題 - 概要と詳細 • 文章を書く時や、何かについて説明をする時 ◦ 最初に概要 (もしくは結論) から入る、その後に詳細に入る ▪

    最初から詳細な説明から入られると … • 理解が追いつかない • 処理の部分部分を(意味のある単位で)関数やクラスに分けて名付けをする ◦ (名前で) 部分部分が何をやる処理かの概要を示す ◦ 関数やクラスの中身で詳細を示す ◦ (関数内に) いきなり詳細なロジックが書いてある場合 ▪ 頭~末尾まで全部(隅々まで)読まなければ… • 処理の全体像が掴めない ◦ = 認知負荷が高い (理解するために使う短期記憶のメモリ量と負荷が高い) → コードでも同じことが言えるのではないか? 9
  8. 本題 - 概要と詳細 // import文省略  ※Gson と OkHttp を使用 class GithubRepositoryCreator

    { public CreationResult execute(String githubAccessToken) throws IOException { Map<String, Object> bodyData = Map.ofEntries( Map.entry("name", "New-Repository-Name" ), Map.entry("description", "Created via API" ), Map.entry("private", false) ); String jsonBody = new Gson().toJson(bodyData); RequestBody reqBody = RequestBody.create(jsonBody, MediaType.parse("application/json" )); Request request = new Request.Builder() . url("https://api.github.com/user/repos" ) . post(reqBody) . addHeader("Authorization" , "Bearer " + githubAccessToken) . addHeader("Accept", "application/vnd.github+json" ) . build(); OkHttpClient client = new OkHttpClient(); try (Response response = client.newCall(request).execute()) { String responseBody = response.body().string(); JsonObject jsonObject = JsonParser.parseString(responseBody). getAsJsonObject (); if (response.isSuccessful()) { String htmlUrl = jsonObject.get("html_url").getAsString(); return CreationResult .success(htmlUrl); } String errorMessage = jsonObject.get("message").getAsString(); return CreationResult .failure(errorMessage); } } } Sample:いきなり詳細なロジックが書いてある例 10
  9. // (1) リクエスト生成 Request buildRequest(String accessToken) { Map<String, Object> bodyData

    = Map.ofEntries( Map.entry("name", "New-Repository-Name" ), Map.entry("description", "Created via API"), Map.entry("private", true) ); String jsonBody = new Gson().toJson(bodyData); RequestBody reqBody = RequestBody.create(jsonBody, MediaType.parse("application/json" )); return new Request.Builder() .url("https://api.github.com/user/repos" ) .post(reqBody) .addHeader("Authorization" , "Bearer " + accessToken) .addHeader("Accept", "application/vnd.github+json" ) .build(); } } // (3) レスポンス解析 CreationResult parseResponse( ApiResponse response) { JsonObject jsonObject = JsonParser .parseString(response.getResponseBody ()) .getAsJsonObject (); if (response.isSuccessful()) { String htmlUrl = jsonObject .get("html_url").getAsString(); return CreationResult .success(htmlUrl); } String errorMessage = jsonObject .get("message").getAsString(); return CreationResult .failure(errorMessage); } // (2) API呼び出し ApiResponse callApi(Request request) throws IOException { OkHttpClient client = new OkHttpClient(); try (Response response = client .newCall(request).execute()) { return new ApiResponse( response .isSuccessful(), response .body().string()); } } 本題 - 概要と詳細 public class GithubRepositoryCreator { public CreationResult execute( String accessToken) throws IOException { Request request = buildRequest(accessToken); ApiResponse response = callApi(request); return parseResponse(response); } 処理の概要を 示す 各関数 (or クラス) で 詳細を示す 各関数 (or クラス) で 詳細を示す 各関数 (or クラス) で 詳細を示す 各関数 (or クラス) で 詳細を示す 11
  10. // (1) リクエスト生成 Request buildRequest(String accessToken) { Map<String, Object> bodyData

    = Map.ofEntries( Map.entry("name", "New-Repository-Name" ), Map.entry("description", "Created via API" ), Map.entry("private", true) ); String jsonBody = new Gson().toJson(bodyData); RequestBody reqBody = RequestBody.create(jsonBody, MediaType.parse("application/json" )); return new Request.Builder() .url("https://api.github.com/user/repos" ) .post(reqBody) .addHeader("Authorization" , "Bearer " + accessToken) .addHeader("Accept", "application/vnd.github+json" ) .build(); } } // (3) レスポンス解析 CreationResult parseResponse( ApiResponse response) { JsonObject jsonObject = JsonParser .parseString(response.getResponseBody ()) .getAsJsonObject (); if (response.isSuccessful()) { String htmlUrl = jsonObject .get("html_url").getAsString(); return CreationResult .success(htmlUrl); } String errorMessage = jsonObject .get("message").getAsString(); return CreationResult .failure(errorMessage); } 本題 - 概要と詳細 // (2) API呼び出し ApiResponse callApi(Request request) throws IOException { OkHttpClient client = new OkHttpClient(); try (Response response = client .newCall(request).execute()) { return new ApiResponse( response .isSuccessful(), response .body().string()); } } 12 // Githubリポジトリ作成処理 public class GithubRepositoryCreator { public CreationResult execute( String accessToken) throws IOException { Request request = buildRequest(accessToken); ApiResponse response = callApi(request); return parseResponse(response); } テストができる 見たい場所へ辿れ、 見る範囲を絞れる
  11. 本題 - 概要と詳細 (based on 構造化プログラミング) 構造化プログラミング: • 「順次」「反復」「分岐」の3種の制御構造で処理の流れを記述する •

    目的:大きなプログラムを書いた時点でプログラムの正しさを証明する 手段: • 段階的抽象化 (ボトムアップで抽象化) • 段階的詳細化 (トップダウンで詳細 化) • プログラムの階層化 • 抽象化データ構造と、 その上で動作する抽象化文と の共同詳細化 (後のオブジェクト指向のクラ スに通じる) main関数 sub関数1 sub関数2 sub関数3 : : : : : : : ※プログラム全体のイメージ図 :関数 詳細なロジック 処理全体の 概要 を呼び出す関数名で表す 13 ➡ 処理全体の見通しを良くする
  12. 本題 - 具体と抽象 • 「具体と抽象」は応用が利く最強の思考法 ◦ 再利用できる汎用的な共通部品を作りたい時に使う ◦ 具体の部品を抽象化する (具体の皮を剥ぐ)

    、具体の皮を被せ直す Github のリポジトリ を作成する REST API を実行する処理 Github の REST API を実行する処理 抽象化 REST API を実行する処理 抽象化 Gitlab の プロジェクトを 作成する REST API を実行する処理 具体化(具体の皮を被せる) Gitlab プロジェクト作成 Hatena Blog の 記事 を投稿する REST API を実行する処理 具体化(具体の皮を被せる) Hatena Blog 記事投稿 15
  13. // (2) リクエスト生成 Request buildRequest(String accessToken) { Map<String, Object> bodyData

    = Map.ofEntries( Map.entry("name", "New-Repository-Name" ), Map.entry("description", "Created via API" ), Map.entry("private", true) ); String jsonBody = new Gson().toJson(bodyData); RequestBody reqBody = RequestBody.create(jsonBody, MediaType.parse("application/json" )); return new Request.Builder() .url("https://api.github.com/user/repos" ) .post(reqBody) .addHeader("Authorization" , "Bearer " + accessToken) .addHeader("Accept", "application/vnd.github+json" ) .build(); } } // (3) レスポンス解析 CreationResult parseResponse( ApiResponse response) { JsonObject jsonObject = JsonParser .parseString(response.getResponseBody ()) .getAsJsonObject (); if (response.isSuccessful()) { String htmlUrl = jsonObject .get("html_url").getAsString(); return CreationResult .success(htmlUrl); } String errorMessage = jsonObject .get("message").getAsString(); return CreationResult .failure(errorMessage); } 本題 - 具体と抽象 Sample (Githubリポジトリ作成API実行処理) の場合 → リクエストヘッダー/ボディ、URL、レスポンスデータ // (2) API呼び出し ApiResponse callApi(Request request) throws IOException { OkHttpClient client = new OkHttpClient(); try (Response response = client .newCall(request).execute()) { return new ApiResponse( response .isSuccessful(), response .body().string()); } } // 元のロジック(Githubリポジトリ作成処理) public class GithubRepositoryCreator { public CreationResult execute( String accessToken) throws IOException { Request request = buildRequest(accessToken); ApiResponse response = callApi(request); return parseResponse(response); } 16 実践例:処理固有の情報(具体)を抜き出す
  14. 本題 - 具体と抽象 // REST API 呼び出し処理 class RestApiClient {

    public String post( String url, Map<String, String> headers, Map<String, Object> bodyData) { Request request = buildRequest( url, headers, bodyData); ApiResponse response = callApi(request); return parseResponse(response); } // (1) リクエスト生成 private Request buildRequest( String url, Map<String, String> headers, Map<String, Object> bodyData) { String jsonBody = new Gson().toJson(bodyData); RequestBody reqBody = RequestBody.create(jsonBody, MediaType.parse("application/json" )); var builder = new Request.Builder() .url(url) .post(reqBody) for (Map.Entry<String, String> entry : headers.entrySet()) { builder.addHeader(entry.getKey(), entry.getValue()); } return builder.build(); } 実践例:処理固有の情報(具体)を抜き出す Sample (Githubリポジトリ作成API実行処理) の場合 → リクエストヘッダー/ボディ、URL、レスポンスデータ 抽象化した部品 URL、リクエストヘッダー /ボディは外か ら渡すように変更 具体情報を剥いだ部品: RestApiClient URL、リクエストヘッダー /ボディは外か ら渡すように変更 具体情報を剥いだ部品: RestApiClient URL、リクエストヘッダー /ボディは外か ら渡すように変更 // (2) API呼び出し private ApiResponse callApi(Request request) throws IOException { OkHttpClient client = new OkHttpClient(); try (Response response = client .newCall(request).execute()) { return new ApiResponse( response .isSuccessful(), response .body().string()); } } 17
  15. 本題 - 具体と抽象 // Githubのリポジトリ作成処理 class GithubRepositoryCreator { private final

    RestApiClient client; private static final String REPOS_URL = "https://api.github.com/user/repos" ; GithubRepositoryCreator (RestApiClient client) { this.client = client; } public CreationResult execute( String accessToken) throws IOException { Map<String, String> headers = buildHeaders( accessToken); Map<String, Object> bodyData = buildBody(); ApiResponse response = client.post( REPOS_URL, headers, bodyData); return parseResponse(response); } } // リクエストヘッダー生成 private Map<String, String> buildHeaders( String accessToken) { return Map.ofEntries( Map.entry("Authorization" , "Bearer " + accessToken), Map.entry("Accept", "application/vnd.github+json" ) ); } // リクエストボディ生成 private Map<String, Object> buildBody() { return Map.ofEntries( Map.entry("name", "New-Repository-Name" ), Map.entry("description", "Created via API" ), Map.entry("private", true) ); } // レスポンス解析 private CreationResult parseResponse( ApiResponse response) { JsonObject jsonObject = JsonParser .parseString(response.getResponseBody ()) .getAsJsonObject (); if (response.isSuccessful()) { String htmlUrl = jsonObject .get("html_url").getAsString(); return CreationResult .success(htmlUrl); } String errorMessage = jsonObject .get("message").getAsString(); return CreationResult .failure(errorMessage); } 実践例:処理固有の情報(具体)を 被せ直す Github Rest API固有の共通情報 GithubRepository Creator RestApiClient クラス図: 18
  16. 本題 - 具体と抽象 // Githubのリポジトリ作成処理 class GithubRepositoryCreator { private final

    GithubApiClient client; private static final String REPOS_PATH = "/user/repos"; GithubRepositoryCreator (GithubApiClient client) { this.client = client; } public CreationResult execute( String accessToken) throws IOException { Map<String, Object> bodyData = buildBody(); ApiResponse response = client.call( REPOS_PATH, accessToken, bodyData); return parseResponse(response); } } // Github の REST API 実行共通処理 class GithubRestApiClient { private final RestApiClient client; private static final String BASE_URL = "https://api.github.com" ; GithubRestApiClient (RestApiClient client) { this.client = client; } public ApiResponse call(String path, String accessToken, Map<String, Object> bodyData) { Map<String, String> headers = buildHeaders(); return client.post(BASE_URL + path, headers, bodyData); } private Map<String, String> buildHeaders( String accessToken) { return Map.ofEntries( Map.entry("Authorization" , "Bearer " + accessToken), Map.entry("Accept", "application/vnd.github+json" ) ); } } 実践例:Github Rest API固有の共通情報(具体)を     抜き出して、被せ直す GithubRestApiClient RestApiClient GithubRepositoryCreator クラス図: 19
  17. 本題 - 知識とロジック • 業務アプリケーションはデータを扱う、データに対して if 文や for文を使ってロジックを成す • だが

    if 文 や for文 は 複雑度 (※) を上げる (※)プログラムを理解するのに要する労力など if (Objects.isNull(value)) { xxxxxx; // 何らかの処理 } • その目的・扱い方 等 ◦ ≒ 特定のデータ、特定の機能や処理、システムの振る舞い等に関するような 詳細仕様 ◦ ≒ 業務や扱うデータ、システムに関する詳細な 知識 21 入力データのバリデーション? 以降の実行ロジックの判断? データの生成・初期化? 内部ステータスの判断? どういった 目的か? どう扱う必要が あるか?      etc. 別のデータの値の計算? Q. Why? A. 裏にある目的等を読み取る  (汲み取る) 必要があるから
  18. 本題 - 知識とロジック 22 public class OrderCalculator { public double

    execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(OrderData orderData) { double price = orderData.getPrice(); int quantity = orderData.getQuantity(); double discountRate = orderData.getDiscountRate(); String discountCode = orderData.getDiscountCode(); String specialOffer = orderData.getSpecialOffer(); return calculateTotal(price, quantity, discountRate, discountCode, specialOffer); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subtotal = applyDiscount(subtotal, discountRate); } subtotal = applyDiscount(subtotal, discountCode); if (Objects.isNull(specialOffer)) { subtotal = applySpecialProcessing(subtotal); } return subtotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode)) { return amount * 0.7; } else if ("silver".equals(discountCode)) { return amount * 0.8; } else if ("bronze".equals(discountCode)) { return amount * 0.8; } return amount; } private double applySpecialProcessing (double amount) { return amount + 20.0; } } public class OrderCalculator { public double execute(InputData inputData) { double price = inputData.getPrice(); int quantity = inputData.getQuantity(); double discountRate = inputData.getDiscountRate(); String discountCode = inputData.getDiscountCode(); String specialOffer = inputData.getSpecialOffer(); return calculateTotal( price, quantity, discountRate , discountCode , specialOffer ); } private double calculateTotal (double price, int quantity, double discountRate , String discountCode , String specialOffer ) { double subtotal = price * quantity; if (quantity >= 5) { subTotal = applyDiscount( subTotal, discountRate ); } subTotal = applyDiscount( subTotal, discountCode ); if (Objects.isNull(specialOffer )) { subTotal = applySpecialProcessing( subTotal); } return subTotal; } private double applyDiscount (double amount, String discountCode ) { if ("gold".equals(discountCode )) { return amount * 0.7; } else if ("silver".equals(discountCode )) { return amount * 0.8; } else if ("bronze".equals(discountCode )) { return amount * 0.8; } return amount; } private double applySpecial (double amount) { return amount + 20.0; } } ※ChatGPTに生成してもらった特に意味のないコードです if (Objects.isNull(specialOffer )) { subTotal = applySpecial( subTotal); } 「値が null を取り得る」と いう知識が埋もれる 何に繋がるか? ・考慮漏れ ・重複コードの作成 ・知識の分散 多量のコードの中に 無造作に置かれると …
  19. 本題 - 知識とロジック • ソフトに実現できる1手段: ドメインオブジェクト • if 文 や

    for文(を使ったロジック)の置き場所を考える必要がある ◦ データとそのデータを扱うロジックが分散する(知識の分散が起きる)から ◦ 1つの 「知識として確立(知識のカプセル化)」 することが重要 ▪ データとそのデータを扱うロジックをまとめることで知識を明確に表現する 23 ◦ (業務) データと、それを使ったロジック (判断/ 加工/計算など)を一体にしたもの ◦ データとロジックを1つにまとめることでロジッ クのコード重複を防ぐ ▪ 値オブジェクト ▪ コレクションオブジェクト ▪ 区分オブジェクト ▪ 列挙型の集合操作 『オブジェクト指向プログラミングでは、クラスは「デー タを整理」する手段ではありません。 クラスは「ロジックを整理」する手段 です。 金額・数量・日付・区分値など、ビジネスで扱う値の種 類を分類し、それぞれの 値に対する計算・判断のロ ジックをそれぞれのクラスに集めて整理するための 手段がクラスです。』 ※『ドメイン駆動設計の集約のわかりにくさの原因と集約を理解するためのヒント』  より引用   https://masuda220.hatenablog.com/entry/2021/05/07/142824 • 知識として確立する手段が クラスとなる
  20. 本題 - 知識とロジック // レスポンス解析 private CreationResult parseResponse( ApiResponse response)

    { JsonObject jsonObject = JsonParser .parseString(responseBody) .getAsJsonObject (); if (response.isSuccessful()) { String htmlUrl = jsonObject . get("html_url").getAsString(); return CreationResult .success(htmlUrl); } String errorMessage = jsonObject .get("message").getAsString(); return CreationResult .failure(errorMessage); } CreationResult を作成 するためのロジック ※概要と詳細、具体と抽象で出していた Sampleコード再び 24 class CreationResult { private final boolean isSuccessful; private final String htmlUrl; private final String errorMessage; private CreationResult (boolean isSuccessful, String htmlUrl, String errorMessage) { this.isSuccessful = isSuccessful; this.htmlUrl = htmlUrl; this.errorMessage = errorMessage; } public static CreationResult fromApiResponse ( ApiResponse apiResponse) { JsonObject jsonObject = JsonParser .parseString(apiResponse.getResponseBody ()) .getAsJsonObject (); if (apiResponse.isSuccessful()) { String htmlUrl = jsonObject . get("html_url").getAsString(); return new CreationResult (true, htmlUrl, ""); } String errorMessage = jsonObject .get("message").getAsString(); return new CreationResult (false, "", errorMessage); } // getter省略 } 作成方法のロジックは 知識として中で持つ (※規模が大きくなる場合は専用の Factoryクラス等を設けるのが吉)
  21. class Feature { public execute(LogicArguments logicArgs) { /*(省略)前段ロジック */ LinkageResultCode

    resultCode = validateLinkage (); PreCheckStatus preCheckStatus = executePreCheck (); boolean isForced = resolveForcedTarget (); executeLogic(resultCode, preCheckStatus , isForced, logicArgs); /*(省略)後続ロジック色々 */ } ResultData executeLogic(LinkageResultCode resultCode, PreCheckStatus preCheckStatus , boolean isForcedFullValid , LogicArguments logicArgs) { if (resultCode.isValid() && preCheckStatus .isOk()) { // 連携有効 return executeLogicA(logicArgs); } if (resultCode.isPartialValid () && preCheckStatus .isOk()) { // 連携部分的有効 return executeLogicB(logicArgs); } if (resultCode.isPartialValid () && isForced) { // 連携強制全有効 return executeLogicC(logicArgs); } if (resultCode.isEmpty() && preCheckStatus .isOk()) { // 連携済み return executeLogicD(logicArgs); } throw new IllegalArgumentsException ("~"); } /* executeLogicA ~Dの実装省略 */ } 本題 - 知識とロジック ・複数の値から状態が決まる ・状態に対応するロジック実行 変更前 • 1つの知識足りえるなら、知識を含むロジックごと切り離して1箇所に隔離(集約)する 25
  22. 本題 - 知識とロジック import com.google.common.annotations.VisibleForTesting; class LinkageStatus { public static

    final String OK = "OK"; public static final String PARTICAL_OK = "ParticalOK"; public static final String FORCED_OK = "ForcedOK"; public static final String COMPLETED = "Completed"; private final String value; @VisibleForTesting public LinkageStatus(String value) { this.value = value; } public LinkageStatus(LinkageResultCode resultCode, PreCheckStatus preCheckStatus, boolean isForced) { if (resultCode.isValid() && preCheckStatus.isOk()) { value = OK; return; } if (resultCode.isPartialValid() && preCheckStatus.isOk()) { value = PARTICAL_OK; return; } if (resultCode.isPartialValid() && isForcedFullValid) { value = FORCED_OK; return; } if (resultCode.isEmpty() && preCheckStatus.isOk()) { value = COMPLETED; return; } throw new IllegalArgumentsException("~"); } public boolean equals(String value) { return this.value.equals(value); } } class Feature { public execute(LogicArguments logicArgs) { /*(省略) 前段ロジック */ ResultCode resultCode = validateLinkage(); PreCheckStatus preCheckStatus = executePreCheck(); boolean isForced = resolveForcedTarget(); var status = new LinkageStatus(resultCode, preCheckStatus, isForced, logicArgs); executeLogic(status, logicArgs); /*(省略)後続ロジック色々 */ } ResultData executeLogic(LinkageStatus status, LogicArguments logicArgs) { if (status.equals(LinkageStatus.OK)) { return executeLogicA(logicArgs); } if (status.equals(LinkageStatus.PARTICAL_OK)) { return executeLogicB(logicArgs); } if (status.equals(LinkageStatus.FORCED_OK)) { return executeLogicC(logicArgs); } if (status.equals(LinkageStatus.COMPLETED)) { return executeLogicD(logicArgs); } throw new InvalidStatusException(status); } } 分割したものの、同じような if 文が増殖… 変更後 26 → 変更時とテストする時の事を考えてみよう
  23. class Feature { public execute(LogicArguments logicArgs) { /*(省略)前段ロジック */ LinkageResultCode

    resultCode = validateLinkage (); PreCheckStatus preCheckStatus = executePreCheck (); boolean isForced = resolveForcedTarget (); executeLogic(resultCode, preCheckStatus , isForcedFullValid , logicArgs); /*(省略)後続ロジック色々 */ } } ResultData executeLogic(LinkageResultCode resultCode, PreCheckStatus preCheckStatus , boolean isForcedFullValid , LogicArguments logicArgs) { if (resultCode.isValid() && preCheckStatus .isOk()) { // 連携有効 return executeLogicA(logicArgs); } if (resultCode.isPartialValid () && preCheckStatus .isOk()) { // 連携部分的有効 return executeLogicB(logicArgs); } if (resultCode.isPartialValid () && isForced) { // 連携強制全有効 return executeLogicC(logicArgs); } if (resultCode.isEmpty() && preCheckStatus .isOk()) { // 連携済み return executeLogicD(logicArgs); } throw new IllegalArgumentsException ("~"); } /* executeLogicA ~Dの実装省略 */ } 本題 - 知識とロジック 変更前 条件分岐とロジック実行を 一緒にテストせざるを得ない 条件式にもロジックにも変更が 入った場合、テストコードの修正 も大変 27
  24. import com.google.common.annotations.VisibleForTesting; class LinkageStatus { public static final String OK

    = "OK"; public static final String PARTICAL_OK = "ParticalOK"; public static final String FORCED_OK = "ForcedOK"; public static final String COMPLETED = "Completed"; private final String value; @VisibleForTesting public LinkageStatus(String value) { this.value = value; } public LinkageStatus(LinkageResultCode resultCode, PreCheckStatus preCheckStatus, boolean isForced) { if (resultCode.isValid() && preCheckStatus.isOk()) { value = OK; return; } if (resultCode.isPartialValid() && preCheckStatus.isOk()) { value = PARTICAL_OK; return; } if (resultCode.isPartialValid() && isForced) { value = FORCED_OK; return; } if (resultCode.isEmpty() && preCheckStatus.isOk()) { value = COMPLETED; return; } throw new IllegalArgumentsException("~"); } public boolean equals(String value) { return this.value.equals(value); } } class Feature { public execute(LogicArguments logicArgs) { /*(省略) 前段ロジック */ ResultCode resultCode = validateLinkage(); PreCheckStatus preCheckStatus = executePreCheck(); boolean isForced = resolveForcedTarget(); var status = new LinkageStatus(resultCode, preCheckStatus, isForced, logicArgs); executeLogic(status); /*(省略)後続ロジック色々 */ } ResultData executeLogic(LinkageStatus status, LogicArguments logicArgs) { if (status.equals(LinkageStatus.OK)) { return executeLogicA(logicArgs); } if (status.equals(LinkageStatus.PARTICAL_OK)) { return executeLogicB(logicArgs); } if (status.equals(LinkageStatus.FORCED_OK)) { return executeLogicC(logicArgs); } if (status.equals(LinkageStatus.COMPLETED)) { return executeLogicD(logicArgs); } throw new InvalidStatusException(status); } } 本題 - 知識とロジック 変更後 条件分岐だけに 集中できる 修正しやすい&テストしやすい 28 ロジック実行だけ に集中できる
  25. 本題 - 知識とロジック class Feature { ResultData executeLogic (LinkageStatus status,

    LogicArguments logicArgs) { if (status.equals(LinkageStatus .OK)) { return executeLogicA (logicArgs); } if (status.equals(LinkageStatus .PARTICAL_OK )) { return executeLogicB (logicArgs); } if (status.equals(LinkageStatus .FORCED_OK)) { return executeLogicC (logicArgs); } if (status.equals(LinkageStatus .COMPLETED)) { return executeLogicD (logicArgs); } throw new InvalidStatusException (status); } } Status 実行処理 OK executeLogicA PARTICAL_OK executeLogicB FORCED_OK executeLogicC COMPLETED executeLogicD Status と実行処理の対応という「知識(仕様)」とそ の制御のための「ロジック」が混在 • 仕様とも言える「知識」と、それを制御する「ロジック」は、完全に分離できるのであれば 分離してセットで置くと保守性が良い部品になりえる 変更前 29
  26. 本題 - 知識とロジック class Feature { private final Map<LinkageStatus, Function<LogicArguments

    , ResultData>> statusToLogicExecutor ; public Feature() { this.statusToLogicExecutor = Map.ofEntries( Map.entry(LinkageStatus.OK, args -> executeLogicA(args)), Map.entry(LinkageStatus.PARTIAL_OK, args -> executeLogicB(args)), Map.entry(LinkageStatus.FORCED_OK, args -> executeLogicC(args)), Map.entry(LinkageStatus.COMPLETED, args -> executeLogicD(args)) ); } // Feature(Map<Status, Function<LogicArguments, ResultData>> statusToLogicExecutor) { // this.statusToLogicExecutor = statusToLogicExecutor; // ※こうすればMap自体を入れ替えることもできる // } ResultData executeLogic(LinkageStatus status, LogicArguments logicArgs) { if (!statusToLogicExecutor .containsKey(status)) { throw new InvalidStatusException (status); } return statusToLogicExecutor .get(status).apply(logicArgs); } } 新規の Status が増えても ここに加えるだけで良い 新規の Status が増えても ロジックは一切変わらない • 仕様とも言える「知識」と、それを制御する「ロジック」は、完全に分離できるのであれば 分離してセットで置くと保守性が良い部品になりえる 30 ※次の「ロジックと境界」のための布石
  27. 機能B 機能A 本題 - ロジックと境界 • 境界 = インターフェース クラスA

    ロジック クラスB ロジック こちらに変更が 加わると… 少なからずこちらに影 響がある可能性が付 き纏う 32
  28. 機能B 機能A 本題 - ロジックと境界 クラスA ロジック クラスB ロジック Bridgeパターン

    変更の影響を受けない こちらが変更されても 33 • 境界 = インターフェース <<interface>>
  29. 本題 - ロジックと境界 Decorator パターン Strategyパターン 34 interface interface クラスA

    (ロジックA) クラスB (ロジックB) interface interface クラスA (ロジックB) クラスA (ロジックA) クラスC (ロジックC) クラスB (ロジックB) interface クラスA (ロジックA) クラスC (ロジックC) クラスB (ロジックB) 使用するロジックを 自由に切り替え可能 (Open-Closed原則に基づき) 自由に拡張(ラップ)可能 これらは「境界」があるからこそできること
  30. 本題 - ロジックと境界 ご紹介: Strategyパターン + Factoryクラス ➡ よく見る Strategyパターン

    実行するロジックを決 定する役割 (インターフェースにすれば、この ロジックも切り替え可能になり汎 用性が増す) 35
  31. 本題 - ロジックと境界 class Feature { ResultData executeLogic (LinkageStatus status,

    LogicArguments logicArgs) { if (status.equals(LinkageStatus .OK)) { return executeLogicA (logicArgs); } if (status.equals(LinkageStatus .PARTIAL_OK)) { return executeLogicB (logicArgs); } if (status.equals(LinkageStatus .FORCED_OK)) { return executeLogicC (logicArgs); } if (status.equals(LinkageStatus .COMPLETED)) { return executeLogicD (logicArgs); } throw new InvalidStatusException (status); } } 36 ※「知識とロジック」で出していた Sampleコード再び ↓ に Strategyパターン + Factoryクラス を適用すると…
  32. class ConcreteFactory { Strategy build(LinkageStatus status, LogicArguments logicArgs) { if

    (status.equals(LinkageStatus.OK)) { return new LogicA(logicArgs); } if (status.equals(LinkageStatus.PARTIAL_OK)) { return new LogicB(logicArgs); } if (status.equals(LinkageStatus.FORCED_OK)) { return new LogicC(logicArgs); } if (status.equals(LinkageStatus.COMPLETED)) { return new LogicD(logicArgs); } throw new InvalidStatusException (status); } } 本題 - ロジックと境界 class Feature { private Factory factory; public Feature(Factory factory) { this.factory = factory; } ResultData executeLogic( LinkageStatus status, LogicArguments logicArgs) { Strategy logic = factory.build( status, logicArgs); logic.execute(); } } ロジックのみに集中 「使用ロジックの選択」 の制御のみに集中 ロジックの実行のみ ( if文 がなくなる) 37 class LogicA implements Strategy { public void execute() { /* 略 */ } } class LogicA implements Strategy { public void execute() { /* 略 */ } } class LogicA implements Strategy { public void execute() { /* 略 */ } } class LogicA implements Strategy { public ResultData execute() { /* 略 */ } }
  33. class Feature { private Factory factory; public Feature(Factory factory) {

    this.factory = factory; } ResultData executeLogic( LinkageStatus status, LogicArguments logicArgs) { Strategy logic = factory.build( status, logicArgs); logic.execute(); } } 本題 - ロジックと境界 class ConcreteFactory { private final Map<LinkageStatus, Function<LogicArguments, Strategy>> statusToLogicBuilder; public Feature() { this.statusToLogicBuilder = Map.ofEntries( Map.entry(LinkageStatus.OK, args -> new LogicA(args)), Map.entry(LinkageStatus.PARTIAL_OK, args -> new LogicB(args)), Map.entry(LinkageStatus.FORCED_OK, args -> new LogicC(args)), Map.entry(LinkageStatus.COMPLETED, args -> new LogicD(args)) ); } Strategy build(LinkageStatus status, LogicArguments logicArgs) { if (!statusToLogicBuilder.containsKey(status)) { throw new InvalidStatusException(status); } return statusToLogicBuilder.get(status).apply(logicArgs); } } 38 ※知識(仕様)とロジックの分離版 class LogicA implements Strategy { public void execute() { /* 略 */ } } class LogicA implements Strategy { public void execute() { /* 略 */ } } class LogicA implements Strategy { public void execute() { /* 略 */ } } class LogicA implements Strategy { public ResultData execute() { /* 略 */ } } ロジックのみに集中 ロジックの実行のみ ( if文 がなくなる) (可能であれば) ここまでできると if 文も少なくなり、より単純明快に
  34. 本題 - ユニットテストしやすいコードにするには? • +1とは ◦ テストしにくいコードは外に出す(委譲する) = Humble Object

    pattern  ※Humble Object Pattern:テストしやすい物としにくい物を住み分ける実装パターン ◦ 外から渡す = DI (依存性の注入) 保守性 (特にモジュール性) を追い求めれば自ずと近づく、 そこに +1 する // (2) API呼び出し ApiResponse callApi(Request request) throws IOException { OkHttpClient client = new OkHttpClient(); try (Response response = client .newCall(request).execute()) { return new ApiResponse(response.isSuccessful(), response .body().string()); } } // Githubリポジトリ作成処理 public class GithubRepositoryCreator { public CreationResult execute( String accessToken) throws IOException { Request request = buildRequest(accessToken); ApiResponse response = callApi(request); return new CreationResult (response); } // (2)参照 ※「概要と詳細」のSampleコード再び GithubRepositoryCreator RestApiClient (API呼び出し処理) 外出し(委譲)
  35. 本題 - ユニットテストしやすいコードにするには? • 依存関係 ではなく 集約関係 にし、インスタンスを渡す( DIする)ための口を設ける 41

    class GithubRepositoryCreator { private static final String REPOS_PATH = "/user/repos"; public CreationResult execute(String accessToken) throws IOException { RestApiClient client = new RestApiClient(); Request request = buildRequest(accessToken); ApiResponse response = client.call(request); return new CreationResult (response); } } 依存関係では置き換えができない class GithubRepositoryCreator { private static final String REPOS_PATH = "/user/repos"; private final RestApiClient client; GithubRepositoryCreator (RestApiClient client) { this.client = client; } public CreationResult execute(String accessToken) throws IOException { Request request = buildRequest(accessToken); ApiResponse response = client.call(request); return new CreationResult (response); } } コンストラクタインジェクションを可能 にする ⇒ テストの際にモックを渡せる
  36. 本題 - ユニットテストしやすいコードにするには? GithubRepositoryCreator RestApiClient RequestBuilder 委譲 先ほどの例では … 42

    リクエスト生成 処理を内包 処理が大きいなら … 単体でテスト (品質担保済) 集約している部品はどちらも モックにして単体でテスト import static org.mockito.Mockito.*; import static org.assertj.core.api.Assertions.*; class GithubRepositoryCreatorTest { @Test void execute_success () throws IOException { String dummyAccessToken = "dummy_access_token" ; Request request = new Request.Builder()/* 略 */; var mockRequestBuilder = mock(RequestBuilder .class); var mockClient = mock(RestApiClient .class); when(mockRequestBuilder ).thenReturn(request); when(mockClient).call(request).thenReturn(new ApiResponse()); var repositoryCreator = new GithubRepositoryCreator ( mockClient, mockRequestBuilder ); CreationResult result = repositoryCreator .execute(dummyAccessToken ); assertThat(result.isSuccessful ()).isTrue(); assertThat(result.getHtmlUrl()).isEqualTo("https://github.com/~" ); verify(mockRequestBuilder , times(1)).build(dummyAccessToken ); verify(mockClient, times(1)).call(request); } class GithubRepositoryCreator { private final RestApiClient client; private final RequestBuilder requestBuilder; GithubRepositoryCreator(RestApiClient client, RequestBuilder requestBuilder) { this.client = client; this.requestBuilder = requestBuilder; } public CreationResult execute( String accessToken) throws IOException { Request request = requestBuilder .build(accessToken); ApiResponse response = client.call(request); return new CreationResult(response); }
  37. 本題 - ユニットテストしやすいコードにするには? import com.google.common.annotations.VisibleForTesting; class GithubRepositoryCreator { private final

    GithubApiClient client; private static final String REPOS_PATH = "/user/repos" ; // 製品コードではこちらを使用 GithubRepositoryCreator () { this.client = new GithubApiClient(); } @VisibleForTesting // テストコードではこちらを使用 GithubRepositoryCreator (GithubApiClient client) { this.client = client; } public CreationResult execute(String accessToken ) throws IOException { Map<String, Object> bodyData = buildBody(); ApiResponse response = client.call(REPOS_PATH, accessToken , bodyData); return new CreationResult(response); } } • テストコードのために専用のコンストラクタを設けるのも一手 43
  38. まとめ • 内部品質(保守性)が高いコードにする ≒ 他者への配慮 ◦ 他者が理解しやすいように、修正しやすいように … ▪ 可読性/視認性の高いコードにしよう

    ▪ 処理の概要を分かりやすくしよう ▪ 使い回しの良い部品に昇華しよう ▪ 知識として確立しよう ▪ 修正容易・テスト容易な仕組みにしよう ▪ インターフェースで適切な境界を設けよう ▪ デザインパターン等を適切に適用しよう ▪ SOLID原則に従おう ▪ (Javaなら) Streamで書けるなら書こう ▪ etc. • 我々ITエンジニアはコードで表現ができる (と個人的には思っています) ◦ コードで他者への配慮を表現しませんか? • 今日明日からでもできること ◦ これから追加/修正するコードは少しでも内部品質を上げていく (既存に捕られすぎず) ◦ 他者が見ても単純明快か? もっと良くできないか? を自問自答し続けよう • テストは目的ではなく内部品質 (保守性)を上げるための手段 (ツール) として活用していこう 44