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

肥大化するレガシーコードに立ち向かうためのインターフェース分離と依存の逆転 / JJUG CC...

肥大化するレガシーコードに立ち向かうためのインターフェース分離と依存の逆転 / JJUG CCC 2026 Spring

Avatar for hirokuni-maeta

hirokuni-maeta

May 30, 2026

More Decks by hirokuni-maeta

Other Decks in Programming

Transcript

  1. 自己紹介 ▌前田 浩邦 / Hirokuni Maeta ▌2014年4月 サイボウズ入社 ▌kintone開発チーム /

    プロダクトエンジニア ▌最近はサーバーサイドの改善を担当 2
  2. kintoneのアプリ ▌簡単な”データベース” ⚫ 業務に必要な情報を、レコードとして蓄積 する ⚫ レコードをチームで共有、管理する ▌作りたい業務システムの分だけアプリを作る ⚫ 案件管理には案件管理アプリ、等

    ▌レコードを構成する項目 (フィールド) は、現場 の業務に沿うように柔軟に設定可能 ⚫ 案件管理: 顧客名、部署名、… 7 レコード フィールド
  3. RecordData: レコードを表現するデータクラス ▌主に、フィールドとその値を保持 ⚫ “顧客名” → “池田学院大学”, … ▌レガシーコードになっている ⚫

    データ構造が木構造で理解が難しい ⚫ 製品の性質上、様々な機能から依存 されやすい 8 レコード フィールド
  4. 当時のコードの凝集度を上げる改善 ▌機能に対応したパッケージにコードを分割し、依存を制限 ⚫ 例: レコード機能のためのパッケージに、レコード機能のコードを移動 ⚫ 可読性の向上、修正の影響範囲を把握しやすくする ⚫ 詳しくは「複雑性に立ち向かうためのサーバーサイドコード分割」として発表 ⚫

    https://speakerdeck.com/hirokunimaeta/jjug-ccc-2023-spring ▌web APIのためのservice (WebApiService) を導入 ⚫ controllerはWebApiServiceを呼ぶだけ、他は何もさせない ⚫ 機能実装がwebレイヤーに露出しなくなる、 fat controllerが解消 17
  5. レコード取得web API ▌controllerに機能実装が書かれている ⚫ 機能についての知識を持っている 18 class RecordController { @GetMapping(“/app/{appId}/record/{recordId}”)

    public GetResponse get(Long appId , Long recordId ) RecordData recordData = recordService.get ( appId , rec… RecordResponse response = recordDataToResponse ( recor … RecordTitle title = recordTitleService.make ( …
  6. RecordWebApiServiceの導入 ▌もともとcontrollerにあった処理はこちらに移動 20 class RecordWebApiServiceImpl implements RecordWebApiService { public GetResponse

    get(long appId , long recordId ) { RecordData recordData = recordService.get ( appId , recordId ); RecordResponse response = recordDataToResponse ( recordData ); RecordTitle title = recordTitleService.make ( appId , recordData ); return new GetResponseImpl (title, response); }
  7. レコード機能の公開インターフェース ▌レコードをaddで追加し、その後 getで取得できる、といったように、 レコード機能の振る舞い (契約) が抽出される 25 interface RecordWebApiService {

    AddResponse add( AddRequest request); GetResponse get(long appId , long recordId ); } interface GetResponse { RecordTitle title(); RecordResponse record(); } interface RecordResponse { List< FieldResponse > fields(); }
  8. 公開インターフェースが機能を定義する ▌レコード機能を外から利用する ための唯一のインターフェイス ▌ここを見れば、レコード機能が 何をするのかが分かる ▌レコード機能を定義している 26 interface RecordWebApiService {

    AddResponse add( AddRequest request); GetResponse get(long appId , long recordId ); } interface GetResponse { RecordTitle title(); RecordResponse record(); } interface RecordResponse { List< FieldResponse > fields(); }
  9. 公開インターフェースの内部実装 ▌内部実装は、公開インター フェースが定義する通りの振る 舞いをするようになっている ▌逆に、この定義通りの振る舞い をすれば、内部実装として問題 ない 27 interface RecordWebApiService

    { AddResponse add( AddRequest request); GetResponse get(long appId , long recordId ); } interface GetResponse { RecordTitle title(); RecordResponse record(); } interface RecordResponse { List< FieldResponse > fields(); }
  10. アプリ化実装 (RecordDataへの依存を増やす方式) 29 class RecordWebApiServiceImpl implements RecordWebApiService { … RecordData

    recordData = recordService.get ( appId , recordId ); … class RecordService { public RecordData get(long appId , long recordId ) { if ( isUserDbApp ( appId )) { return userDbDataToRecordData ( userDbService.get ( appId , recordId )); } … // 既存の処理
  11. RecordDataを中心にして処理を組む 30 class RecordWebApiServiceImpl implements RecordWebApiService { … RecordData recordData

    = recordService.get ( appId , recordId ); … class RecordService { public RecordData get(long appId , long recordId ) { if ( isUserDbApp ( appId )) { return userDbDataToRecordData ( userDbService.get ( appId , recordId )); } … // 既存の処理 呼び出し元は変更不要 recordIdを受け取ってRecordData を返すという振る舞いにしたがった実装
  12. RecordDataとレコード機能の関係 ▌RecordWebApiService にRecordDataは現れない ⚫ RecordDataを使うこと は、レコード機能と無関係 ▌RecordDataを使っている のは、現状がそうであるだけ 32 interface

    RecordWebApiService { AddResponse add( AddRequest request); GetResponse get(long appId , long recordId ); } interface GetResponse { RecordTitle title(); RecordResponse record(); } interface RecordResponse { List< FieldResponse > fields(); } レコード機能の定義
  13. RecordDataを使わないアプローチ ▌getの場合、必要なのは GetResponseや RecordResponseを返すこと ▌これらを返すことができさえすれば、 RecordDataは使わなくてもいい 33 interface RecordWebApiService {

    AddResponse add( AddRequest request); GetResponse get(long appId , long recordId ); } interface GetResponse { RecordTitle title(); RecordResponse record(); } interface RecordResponse { List< FieldResponse > fields(); }
  14. RecordDataを経由せずRecordResponseを生成 ▌UserDbData → RecordData → RecordResponseとなって いた処理を合成し、userDbDataToResponseとする 34 public GetResponse

    get(long appId , long recordId ) { RecordResponse response; if ( isUserDbApp ( appId )) { response = userDbDataToResponse ( userDbService.get ( appId , recordId )); } else { response = recordDataToResponse ( recordService.get ( appId , recordId )); } …
  15. 36 RecordService 機能の公開インターフェース 機能の内部実装 UserDbData UserDbService RecordWebApi Service RecordWebApi ServiceImpl

    RecordData RecordService GetResponse RecordTitleService 今までとは異なり、RecordDataを使わない処理が増えたが、 公開インターフェースの振る舞いは変わっていない → 機能上は問題ない
  16. 残りのRecordDataへの依存について 37 public GetResponse get(long appId , long recordId )

    { RecordResponse response; if ( isUserDbApp ( appId )) { … } else { … } RecordTitle title = recordTitleService.make ( appId , recordData ); // ??? return new GetResponseImpl (title, response); }
  17. RecordDataでないといけないのか? ▌特定のデータクラスでないと生成できない、ということはないはず 38 class RecordTitleServiceImpl implements RecordTitleService { RecordTitle make(long

    appId , RecordData recordData ) { Long fieldId = getTitleFieldId ( appId ); RecordDataFieldSearcher.search ( recordData , new SearchCallback () { void handleField ( RecordData.Field field) { if (field.id() == fieldId ) { FieldValue fieldValue = field.value (); … // レコードタイトルの生成処理
  18. RecordDataでないといけないのか? 39 class RecordTitleServiceImpl implements RecordTitleService { RecordTitle make(long appId

    , RecordData recordData ) { Long fieldId = getTitleFieldId ( appId ); RecordDataFieldSearcher.search ( recordData , new SearchCallback () { void handleField ( RecordData.Field field) { if (field.id() == fieldId ) { FieldValue fieldValue = field.value (); … // レコードタイトルの生成処理 Fieldの探索 FieldValueからレコードタイトルを生成
  19. FieldValueがあればレコードタイトルは生成可能 40 interface RecordForTitle { FieldValue getValue (long fieldId );

    } class RecordTitleServiceImpl implements RecordTitleService { RecordTitle make(long appId , RecordForTitle record) { Long fieldId = getTitleFieldId ( appId ); FieldValue value = record.getValue ( fieldId ); … // レコードタイトルの生成処理 フィールドの探索部分をインターフェースに切り出し FieldValueからレコードタイトルを生成
  20. Field探索の実装は元のものを再利用すればよい 41 class SearchFieldbyFieldId implements SearchCallback { private final long

    fieldId ; @Getter private FieldValue value = null; void handleField ( RecordData.Field field) { if (field.id() == fieldId ) { value = field.value (); } } } 元々のField探索実装
  21. RecordDataに直接依存せずRecordTitleを生成 42 … RecordForTitle recordForTitle ; if ( isUserDbApp (

    appId )) { recordForTitle = userDbDataToRecordForTitle ( userDbService.get ( appId , recordId )); } else { recordForTitle = recordDataToRecordForTitle ( recordService.get ( appId , recordId )); } RecordTitle title = recordTitleService.make ( appId , recordForTitle ); …
  22. 直接RecordDataに依存しない実装へ ▌単純に置き換えると、appIdでの分岐やデータの取得が重複する ▌これらはresponseやrecordForTitleを生成する部分 43 RecordResponse response ; if ( isUserDbApp

    ( appId )) { response = userDbDataToResponse ( userDbService.get ( appId , recordId )); … RecordForTitle recordForTitle ; if ( isUserDbApp ( appId )) { recordForTitle = userDbDataToRecordForTitle ( userDbService.get ( appId , recordId )); …
  23. 最終的な設計: インターフェースによる分離 ▌分岐は無くなり、永続化実装の詳細も隠ぺいされた 45 public GetResponse get(long appId , long

    recordId ) { GenericRecord record = genericRecordService.get ( appId , recordId ); RecordResponse response = record.response (); RecordTitle title = recordTitleService.make ( appId , record.forTitle ()); return new GetResponseImpl (title, response); }
  24. 依存の逆転 47 機能の公開インターフェース 機能の内部実装 UserDb Record CybozuDb Record UserDbData RecordData

    RecordResponse RecordForTitle GenericRecord コード上はGenericRecordに依存しつつも、 実行時はCybozuDbRecord実装を通して、 RecordDataを使った今まで通りの処理が流れる → 依存の逆転
  25. 難解さが広がってしまうことによるレガシー化 ▌レコードタイトル生成の理解には、 RecordDataの構造や関連クラス (SearchCallback等) の理解も 必要だった ▌RecordDataの難解さがレコード タイトル生成にも広がっていた 50 class

    RecordTitleServiceImpl implem … RecordTitle make(long appId , Reco… Long fieldId = getTitleFieldId (… RecordDataFieldSearcher.search (… new SearchCallback () { void handleField ( RecordData … if (field.id() == fieldId … FieldValue fieldValue =… … // レコードタイトルの生成処理
  26. 難解な処理が知られず癒着してレガシー化 ▌レコードタイトル生成が Fieldの探索と癒着していた ▌RecordDataを変える際 は、レコードタイトル生成の 理解も必要になっていた 52 class RecordTitleServiceImpl implem

    … RecordTitle make(long appId , Reco… Long fieldId = getTitleFieldId (… RecordDataFieldSearcher.search (… new SearchCallback () { void handleField ( RecordData … if (field.id() == fieldId … FieldValue fieldValue =… … // レコードタイトルの生成処理 Fieldの探索 FieldValueからレコードタイトルを生成
  27. RecordDataはそのまま ▌RecordDataそのものの難解さ (木構造) は変わっていない ⚫ 木構造は柔軟性、将来的な拡張性のために必要 ⚫ 単純に難解さを取り除いてしまうと、柔軟性、拡張性も失われてしまう ▌今回のレガシーコードの肥大化の正体は、RecordDataの難解さが広がったり、 周囲のコードと癒着してしまうこと

    ⚫ RecordDataそのものの難解さはレガシーコードの肥大化とは別の問題 ▌もちろん、 RecordDataをリファクタリングして、柔軟性、拡張性を担保しつつ、 難解さを解消していくことも重要 ⚫ そのためにも、インターフェイスを切っておけば安心 54
  28. プロパティの移動の例 ▌RecordDataが、画面に表示するための値も保持していた ⚫ 永続化した数値 (model): 1000 ⚫ 画面表示用にフォーマットした文字列 (view): “1,000”

    ▌画面に表示する値を持つべきなのはRecordResponseレイヤー 58 Record Data ・ 1000 ・ “1,000” RecordWebApi ServiceImpl Record Response ・ 1000 ・ “1,000” DB
  29. 二つのレコード実装による阻害の解消 62 UserDb Record CybozuDb Record UserDbData RecordData GenericRecord サイボウズ

    DB ユーザーの基幹 システム 各種idをStringで保持可能 こちらは影響を受けない
  30. レガシーコード改善を可能にしたアイディア ▌機能の公開インターフェースを、内部実装にも流用すること 67 interface GenericRecordService { GenericRecord get(long appId ,

    long recordId ); } interface GenericRecord { RecordResponse response(); RecordForTitle forTitle (); } 公開インターフェースを内部に持ってくることで、RecordDataを隠ぺい
  31. まとめ ▌レガシーコードの改善について紹介 ⚫ 機能実装の凝集度を上げる活動 (パッケージ分割やwebとの分離) により、 機能の公開インターフェースが生まれる ⚫ 公開インターフェースに必要な振る舞いをしている限りは、内部実装は自由 ⚫

    公開インターフェースをより内部に持ち込み、依存の逆転に利用することで、 レガシーコードへの依存を制限する ▌レガシーコードへの依存を制限した後は、個別に改善していくことが可能 ▌レガシーコードは自然には減らない。機能開発をしていると肥大化し、悪影響 を及ぼす。したがって、レガシーコードへの依存を制限し、改善することは重要。 71
  32. RecordDataの役割の変化 ▌元々は、RecordData = レコードという考え方で処理を組んでいた ⚫ レコードを永続化する = RecordDataを永続化する ⚫ この考え方により、RecordDataへの依存が多くなっていた

    ▌今では、RecordData = レコードという考え方は通用しなくなっている ⚫ RecordDataを経由しなくてもレコード機能として振る舞えるため ⚫ プロパティ移動の改善により、RecordDataはサイボウズのDBに永続 化されたデータ、になりつつある 74