Slide 1

Slide 1 text

肥大化するレガシーコードに立ち向かうための インターフェース分離と依存の逆転 前田 浩邦 (サイボウズ株式会社) JJUG CCC 2026 Spring 1

Slide 2

Slide 2 text

自己紹介 ▌前田 浩邦 / Hirokuni Maeta ▌2014年4月 サイボウズ入社 ▌kintone開発チーム / プロダクトエンジニア ▌最近はサーバーサイドの改善を担当 2

Slide 3

Slide 3 text

kintone ▌業務システムを作成するためのノーコード・ローコードツール ⚫ 仕事に必要なデータを管理、チームで共有 ⚫ 案件管理システム、顧客管理システム等 3

Slide 4

Slide 4 text

本発表の内容 ▌機能開発と共に肥大化するレガシーコード ▌インターフェース分離と依存の逆転 ▌機能の公開インターフェース 4

Slide 5

Slide 5 text

機能開発と共に肥大化するレガシーコード 5

Slide 6

Slide 6 text

レガシーコード ▌長年の開発を経て、読みづらく、変えづらくなってしまったコード ▌レガシーコードの種類 ⚫ 巨大クラス、役割が不明なクラス ⚫ 循環参照 ⚫ 難解なデータ構造 ⚫ プロパティが多い、用途が分かりづらい ⚫ 多すぎる依存 6 今回の対象

Slide 7

Slide 7 text

kintoneのアプリ ▌簡単な”データベース” ⚫ 業務に必要な情報を、レコードとして蓄積 する ⚫ レコードをチームで共有、管理する ▌作りたい業務システムの分だけアプリを作る ⚫ 案件管理には案件管理アプリ、等 ▌レコードを構成する項目 (フィールド) は、現場 の業務に沿うように柔軟に設定可能 ⚫ 案件管理: 顧客名、部署名、… 7 レコード フィールド

Slide 8

Slide 8 text

RecordData: レコードを表現するデータクラス ▌主に、フィールドとその値を保持 ⚫ “顧客名” → “池田学院大学”, … ▌レガシーコードになっている ⚫ データ構造が木構造で理解が難しい ⚫ 製品の性質上、様々な機能から依存 されやすい 8 レコード フィールド

Slide 9

Slide 9 text

木構造 ▌柔軟性、将来的な拡張性を持たせるため、木構造を採用 ⚫ アプリごとにフィールドは異なり、後から追加や削除も可能 ⚫ フィールドの値も様々、レコード自体も値として許容 (レコードの入れ子) ▌その結果、現行のkintoneの仕様以上に機能を持っているように見える ⚫ RecordData自体の作りを理解するのが難しくなっている ▌加えて、RecordDataから単純に値を取得・更新できなくなっている ⚫ 木構造を深さ優先探索するクラスや、探索する各々のノードで処理を実行 するcallbackクラスを駆使して処理を組み上げる必要がある 9

Slide 10

Slide 10 text

プロパティが多い、用途が分かりづらい ▌多くのプロパティを持っている ▌一部の機能でしか使われなさそうなプロパティを持っている ⚫ stateNameはProcessManagementServiceでしか使われない? 10 class RecordData { private Long stateId ; private String stateName ; private RecordTitle title; …

Slide 11

Slide 11 text

多すぎる依存 ▌レコードを使うという機能は、製品の性質上どうしても増えてしまう ⚫ したがって、レコード = RecordDataへの依存も増えてしまう ▌依存元が一斉に影響を受けるので、 RecordDataの修正は困難 11 RecordData Notification Service RecordTitle Service

Slide 12

Slide 12 text

そんな中で、レコードを使う新しい機能が増えていく ▌どうやって作る? ⚫ RecordDataに依存するしか… ⚫ 新機能は読みづらく、RecordDataはさらに修正が困難になっていく 12 RecordData Notification Service RecordTitle Service AiService (新機能)

Slide 13

Slide 13 text

レガシーコードは減らず、むしろ肥大化する ▌普段の機能開発をしてもレガシーコードは減らない ⚫ kintoneでは、初期の設計にコードの継ぎ足しが繰り返されたことで、 プロパティや依存が増えていき、レガシー化 ⚫ 機能開発は止まらず、その中でどう変えていいのかも分からない ▌減るどころか、むしろ肥大化する一方 ⚫ 実際、RecordDataのようなコア機能でこそ肥大化していく傾向 13

Slide 14

Slide 14 text

レガシーコードは機能開発も阻害する ▌レコードをユーザーのDBに永続化する機能: 外部システムのアプリ化 ⚫ ユーザーの基幹システムのDBを、kintoneのアプリとして見せる ▌ユーザーのDBにあるデータを、RecordDataに変換して実現しようとしていた 14 サイボウズ DB ユーザーの基幹 システム RecordData Notification Service RecordTitle Service AiService

Slide 15

Slide 15 text

レガシーコードは機能開発も阻害する ▌アプリ化のために、レコードの各種idをStringで保持したい ⚫ RecordDataの修正は困難で、リスクが高い ⚫ 関係ないはずの、既存の永続化実装にまで影響が出てしまう 15 サイボウズ DB ユーザーの基幹 システム RecordData Notification Service RecordTitle Service AiService Longだった各種idを Stringにしたい

Slide 16

Slide 16 text

インターフェース分離と依存の逆転 16

Slide 17

Slide 17 text

当時のコードの凝集度を上げる改善 ▌機能に対応したパッケージにコードを分割し、依存を制限 ⚫ 例: レコード機能のためのパッケージに、レコード機能のコードを移動 ⚫ 可読性の向上、修正の影響範囲を把握しやすくする ⚫ 詳しくは「複雑性に立ち向かうためのサーバーサイドコード分割」として発表 ⚫ https://speakerdeck.com/hirokunimaeta/jjug-ccc-2023-spring ▌web APIのためのservice (WebApiService) を導入 ⚫ controllerはWebApiServiceを呼ぶだけ、他は何もさせない ⚫ 機能実装がwebレイヤーに露出しなくなる、 fat controllerが解消 17

Slide 18

Slide 18 text

レコード取得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 ( …

Slide 19

Slide 19 text

RecordWebApiServiceの導入 ▌controllerはRecordWebApiServiceを呼び出すだけ ⚫ 機能に関する一切の知識を持っていない 19 class RecordController { @GetMapping(“/app/{appId}/record/{recordId}”) public GetResponse get(Long appId , Long recordId ) { return recordWebApiService.get ( appId , recordId ); } }

Slide 20

Slide 20 text

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); }

Slide 21

Slide 21 text

クラスと役割 21 GetResponse RecordController RecordWebApi Service RecordWebApi ServiceImpl

Slide 22

Slide 22 text

クラスと役割 22 RecordController RecordWebApi Service RecordWebApi ServiceImpl レコード機能に関する 知識は何も存在しない GetResponse

Slide 23

Slide 23 text

クラスと役割 23 RecordWebApi Service RecordWebApi ServiceImpl レコード機能 GetResponse

Slide 24

Slide 24 text

クラスと役割 24 RecordWebApi Service RecordWebApi ServiceImpl 機能の公開インターフェース 機能の内部実装 外から見えるのはここだけ GetResponse

Slide 25

Slide 25 text

レコード機能の公開インターフェース ▌レコードを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(); }

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

28 RecordWebApi Service RecordWebApi ServiceImpl RecordData RecordService 機能の公開インターフェース 機能の内部実装 GetResponse RecordTitleService

Slide 29

Slide 29 text

アプリ化実装 (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 )); } … // 既存の処理

Slide 30

Slide 30 text

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 を返すという振る舞いにしたがった実装

Slide 31

Slide 31 text

31 RecordService 機能の公開インターフェース 機能の内部実装 UserDbData UserDbService RecordWebApi Service RecordWebApi ServiceImpl RecordData RecordService GetResponse RecordTitleService

Slide 32

Slide 32 text

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(); } レコード機能の定義

Slide 33

Slide 33 text

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(); }

Slide 34

Slide 34 text

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 )); } …

Slide 35

Slide 35 text

35 RecordService 機能の公開インターフェース 機能の内部実装 UserDbData UserDbService RecordWebApi Service RecordWebApi ServiceImpl RecordData RecordService GetResponse RecordTitleService

Slide 36

Slide 36 text

36 RecordService 機能の公開インターフェース 機能の内部実装 UserDbData UserDbService RecordWebApi Service RecordWebApi ServiceImpl RecordData RecordService GetResponse RecordTitleService 今までとは異なり、RecordDataを使わない処理が増えたが、 公開インターフェースの振る舞いは変わっていない → 機能上は問題ない

Slide 37

Slide 37 text

残りの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); }

Slide 38

Slide 38 text

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 (); … // レコードタイトルの生成処理

Slide 39

Slide 39 text

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からレコードタイトルを生成

Slide 40

Slide 40 text

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からレコードタイトルを生成

Slide 41

Slide 41 text

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探索実装

Slide 42

Slide 42 text

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 ); …

Slide 43

Slide 43 text

直接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 )); …

Slide 44

Slide 44 text

最終的な設計: インターフェースによる分離 ▌responseやrecordForTitleを返却するインターフェイスを導入し、 appIdでの分岐やデータ取得の重複を解消 44 interface GenericRecordService { GenericRecord get(long appId , long recordId ); } interface GenericRecord { RecordResponse response (); RecordForTitle forTitle (); }

Slide 45

Slide 45 text

最終的な設計: インターフェースによる分離 ▌分岐は無くなり、永続化実装の詳細も隠ぺいされた 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); }

Slide 46

Slide 46 text

依存の逆転 46 機能の公開インターフェース 機能の内部実装 UserDb Record CybozuDb Record UserDbData RecordData RecordResponse RecordForTitle GenericRecord

Slide 47

Slide 47 text

依存の逆転 47 機能の公開インターフェース 機能の内部実装 UserDb Record CybozuDb Record UserDbData RecordData RecordResponse RecordForTitle GenericRecord コード上はGenericRecordに依存しつつも、 実行時はCybozuDbRecord実装を通して、 RecordDataを使った今まで通りの処理が流れる → 依存の逆転

Slide 48

Slide 48 text

レガシーコードは改善されたのか? ▌レガシーコード: 読みづらく、変えづらくなってしまったコード ⚫ 難解なデータ構造 ⚫ プロパティが多い、用途が分かりづらい ⚫ 多すぎる依存 ▌レガシーコードによる機能開発の阻害 48

Slide 49

Slide 49 text

難解なデータ構造 ▌レコードタイトル機能はRecordDataに 直接依存しなくなった ⚫ 難解なデータ構造を理解しなくても、 機能実装を理解可能になった ▌依存しなくなることで、パッケージ内の コードだけを見て機能実装が理解可能に なった ⚫ 機能実装の凝集度が上がった 49 RecordData RecordTitle Service RecordForTitle impl

Slide 50

Slide 50 text

難解さが広がってしまうことによるレガシー化 ▌レコードタイトル生成の理解には、 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 =… … // レコードタイトルの生成処理

Slide 51

Slide 51 text

難解さを抑え込むRecordForTitle ▌RecordDataの代わりにフィールドの値 を提供 (getValue) するインターフェイス ▌レコードはフィールドの値を保持するもの だったので、レコードがgetValueを持つの はおかしくない 51 RecordData RecordTitle Service RecordForTitle impl

Slide 52

Slide 52 text

難解な処理が知られず癒着してレガシー化 ▌レコードタイトル生成が 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からレコードタイトルを生成

Slide 53

Slide 53 text

このような癒着もRecordForTitleにより解消 ▌RecordForTitleによって、レコードタイトル の生成部分とFieldの探索が分離された ▌RecordDataを変える際は影響範囲を implに抑えることができる ⚫ RecordForTitleが持つのは、レコードが 持っていてもおかしくない振る舞い (getValue) のみなので、レコードタイトル の処理に踏み込まずにimplを変更可能 53 RecordData RecordTitle Service RecordForTitle impl

Slide 54

Slide 54 text

RecordDataはそのまま ▌RecordDataそのものの難解さ (木構造) は変わっていない ⚫ 木構造は柔軟性、将来的な拡張性のために必要 ⚫ 単純に難解さを取り除いてしまうと、柔軟性、拡張性も失われてしまう ▌今回のレガシーコードの肥大化の正体は、RecordDataの難解さが広がったり、 周囲のコードと癒着してしまうこと ⚫ RecordDataそのものの難解さはレガシーコードの肥大化とは別の問題 ▌もちろん、 RecordDataをリファクタリングして、柔軟性、拡張性を担保しつつ、 難解さを解消していくことも重要 ⚫ そのためにも、インターフェイスを切っておけば安心 54

Slide 55

Slide 55 text

多すぎる依存 ▌同様のアプローチで直接の依存を減らしていくことが可能 55 RecordData impl impl impl RecordTitle Service RecordForTitle Notification Service RecordForNtf AiService RecordForAi

Slide 56

Slide 56 text

依存の数とクラス数のトレードオフ ▌直接の依存が減る代わりに、機能毎にレコードクラスが増えていく ▌今後の課題 ⚫ 単純に共通化をしても以前の状態に戻るだけで意味がない ⚫ 機能のあり方を踏まえた振る舞いの蒸留が必要 56 RecordTitle Service RecordForTitle Notification Service RecordForNtf AiService RecordForAi

Slide 57

Slide 57 text

プロパティが多い、用途が分かりづらい ▌impl側に持たせられないか検討すればよい 57 GetResponse RecordData impl RecordResponse ProcessManagementService impl RecordForProcessManagement

Slide 58

Slide 58 text

プロパティの移動の例 ▌RecordDataが、画面に表示するための値も保持していた ⚫ 永続化した数値 (model): 1000 ⚫ 画面表示用にフォーマットした文字列 (view): “1,000” ▌画面に表示する値を持つべきなのはRecordResponseレイヤー 58 Record Data ・ 1000 ・ “1,000” RecordWebApi ServiceImpl Record Response ・ 1000 ・ “1,000” DB

Slide 59

Slide 59 text

プロパティの移動の例 ▌画面に表示する値はRecordResponse実装で生成し、 RecordDataには永続化した値だけを持たせる 59 CybozuDb Record ・ 1000 ・ “1,000” ・ 1000 Record Data RecordWebApi ServiceImpl Record Response DB

Slide 60

Slide 60 text

プロパティの移動の例 ▌依存する側にとって必要なプロパティを保持する先がRecordDataしか なかったため、プロパティの数が増え、用途が分かりづらくなっていた 60 CybozuDb Record ・ 1000 ・ “1,000” ・ 1000 Record Data RecordWebApi ServiceImpl Record Response DB

Slide 61

Slide 61 text

二つのレコード実装による阻害の解消 61 UserDb Record CybozuDb Record UserDbData RecordData GenericRecord サイボウズ DB ユーザーの基幹 システム

Slide 62

Slide 62 text

二つのレコード実装による阻害の解消 62 UserDb Record CybozuDb Record UserDbData RecordData GenericRecord サイボウズ DB ユーザーの基幹 システム 各種idをStringで保持可能 こちらは影響を受けない

Slide 63

Slide 63 text

機能の公開インターフェース 63

Slide 64

Slide 64 text

機能の公開インターフェース ▌機能を、外部 (他の機能、web) から利用できるようにする唯一の窓口 ⚫ 内部実装は、公開インターフェイスを通じてのみ外部とやりとりする 64 RecordForTitle RecordTitleService RecordWebApiService レコード機能の 公開インターフェース レコードタイトル機能の公開インターフェース 内部実装 内部実装 RecordResponse

Slide 65

Slide 65 text

機能の公開インターフェース ▌機能の公開インターフェースは、パッケージ分割やwebとの分離といった、機能 実装の凝集度を上げる活動の結果、必ず生まれる ▌例: 機能Aの実装の凝集度を上げる ⚫ 点在する機能Aの実装を集約し、外部 (他の機能、web) から隠ぺい ⚫ 機能Aを外部から使えるようにするためには、機能Aの公開インターフェース が必要 65 外部 機能A Service impl 機能A 機能A 機能A 公開 インターフェース 外部

Slide 66

Slide 66 text

機能の公開インターフェース ▌公開インターフェースは、外側に近いレイヤーで使うというイメージが強い ⚫ 外部から使えるようにする、という性質のため ▌実際、元のRecordResponseの生成はそのようになっていた 66 Record Response Record Data RecordWeb ApiServiceImpl RecordResponseを生成

Slide 67

Slide 67 text

レガシーコード改善を可能にしたアイディア ▌機能の公開インターフェースを、内部実装にも流用すること 67 interface GenericRecordService { GenericRecord get(long appId , long recordId ); } interface GenericRecord { RecordResponse response(); RecordForTitle forTitle (); } 公開インターフェースを内部に持ってくることで、RecordDataを隠ぺい

Slide 68

Slide 68 text

公開インターフェースを、より内部に ▌公開インターフェースは、内部実装よりも安定して存在する ⚫ レガシーな内部実装では、そもそもクラスの役割分担は曖昧で不安定 ⚫ 一方で、機能が存在限り、外部用の窓口という役割も必ず存在する ⚫ 役割がはっきりしていて、後に役割が増えてしまう等の懸念が少ない ▌内部実装に流用しても、リスクは低いと考えられる ⚫ 役割が曖昧になっている中へさらに変なものを持ち込んでいる、という ことにはならない 68

Slide 69

Slide 69 text

公開インターフェースを、より内部に ▌RecordResponseを、より内部で生成 ⚫ これにより、レガシーコード (RecordData) を隠ぺい ▌隠ぺいできただけでなく、実態に即した構造にもなったと考えられる ⚫ 無理なく内部に持ってくることができたのは、RecordDataが既に RecordResponse相当のプロパティを保持していたため ⚫ 元々RecordResponseを内部で生成する構造だったと解釈できる 69 CybozuDb Record Record Data Record Response Generic Record RecordWeb ApiServiceImpl RecordResponseを生成

Slide 70

Slide 70 text

依存の逆転に公開インターフェースを利用 ▌依存の逆転という手法は一般的に知られているものの、インターフェース をどう切るかという部分が難しい ⚫ インターフェイスをレガシーな内部実装から見出すと、開発と共に役割 が変わったり、無くなってしまうこともあり得るため、依存先として不安定 ▌今回は公開インターフェースを利用し、依存の逆転を行った ⚫ 公開インターフェイス自体は必ず存在しており、内部実装由来でなく 役割も明確なので、依存先としては比較的安定している ⚫ レガシーコード改善の第一歩として現実的 70

Slide 71

Slide 71 text

まとめ ▌レガシーコードの改善について紹介 ⚫ 機能実装の凝集度を上げる活動 (パッケージ分割やwebとの分離) により、 機能の公開インターフェースが生まれる ⚫ 公開インターフェースに必要な振る舞いをしている限りは、内部実装は自由 ⚫ 公開インターフェースをより内部に持ち込み、依存の逆転に利用することで、 レガシーコードへの依存を制限する ▌レガシーコードへの依存を制限した後は、個別に改善していくことが可能 ▌レガシーコードは自然には減らない。機能開発をしていると肥大化し、悪影響 を及ぼす。したがって、レガシーコードへの依存を制限し、改善することは重要。 71

Slide 72

Slide 72 text

補足 72

Slide 73

Slide 73 text

レコード機能の中での“レコード”について 73

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

“レコード”とは何か? ▌それでは、 コード内のどの部分がレコードに相当するのか? ▌そもそもレコード機能に“レコード”はない ⚫ なぜなら、レコード機能の定義はRecordWebApiServiceにある通りで、 この定義に“レコード”と呼べそうなものは存在していないため ▌“レコード”が何であっても、それどころかあってもなくても、機能には影響しない ⚫ “レコード”を持ち込まなくても実装可能 ⚫ 実装上は、 “レコード”が何かを考える必要はない 75

Slide 76

Slide 76 text

“レコード”とは何か? ▌素朴に思い浮かべる“レコード”は、コード内に存在しないのか? ⚫ GenericRecordが近そう 76 “レコード” RecordTitle Service RecordForTitle Notification Service RecordForNtf AiService RecordForAi

Slide 77

Slide 77 text

“レコード”とは何か? ▌“レコード”は、機能に応じていろいろな振る舞い方をしている ⚫ レコードタイトルではフィールドの値を提供する ⚫ 通知では…、AIでは… ▌“レコード”は、各機能視点からのレコードに求める振る舞い (RecordForTitle等) を束ねたものである、と考えることができる ⚫ それをコード内で実現しているのがGenericRecord ▌機能 (コンテキスト) 毎の振る舞いの束であると考えるとつじつまが合う 77