Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
肥大化するレガシーコードに立ち向かうためのインターフェース分離と依存の逆転 / JJUG CC...
Search
hirokuni-maeta
May 30, 2026
Programming
560
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
肥大化するレガシーコードに立ち向かうためのインターフェース分離と依存の逆転 / JJUG CCC 2026 Spring
hirokuni-maeta
May 30, 2026
More Decks by hirokuni-maeta
See All by hirokuni-maeta
複雑性に立ち向かうためのサーバーサイドコード分割 / JJUG CCC 2023 Spring
hirokunimaeta
0
12
コード分割から始める複雑さの解消に向けたkintoneのアーキテクチャ改善 / アーキテクチャConference 2025
hirokunimaeta
0
16k
Other Decks in Programming
See All in Programming
Lessons from Spec-Driven Development
simas
PRO
0
210
ローカルLLMを使ってB2Bサービスを作っていての学び
yaotti
0
170
ふつうのFeature Flag実践入門
irof
7
4k
並列実装の現場、2ヶ月間実務でAIを使い倒したAIもPCも私も限界が近い
ming_ayami
0
130
Go1.27で導入されるジェネリクスメソッドでできること
mackee
0
130
Semantic Version 単位で戦略を柔軟に変えて、パッケージアップデートを自動化する
daitasu
1
240
Make SRE Operations Easier with Azure SRE Agent
kkamegawa
0
6.3k
Vue × Nuxt × Oxc どこまで使える?実運用の現在地
andpad
0
260
Composerを使ったサプライチェーン攻撃の様子を眺めてみる #phpstudy
o0h
PRO
2
250
Datadog × OpenTelemetry 入門と実践のあいだ
kn_to_maxpno
1
160
The ROI of Quarkus for Spring Boot Applications
hollycummins
0
120
「AIで開発し、AIを届ける」をEvalでつなぐ 〜AIネイティブに始めるプロダクト開発の実践〜 / Connecting "Develop with AI, deliver AI" with Eval
rkaga
4
5.1k
Featured
See All Featured
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
160
Building AI with AI
inesmontani
PRO
1
1.1k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
340
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
360
30k
Faster Mobile Websites
deanohume
310
31k
GitHub's CSS Performance
jonrohan
1033
470k
Game over? The fight for quality and originality in the time of robots
wayneb77
1
200
Designing for Timeless Needs
cassininazir
1
260
How to train your dragon (web standard)
notwaldorf
97
6.7k
Documentation Writing (for coders)
carmenintech
77
5.4k
Getting science done with accelerated Python computing platforms
jacobtomlinson
2
230
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
22k
Transcript
肥大化するレガシーコードに立ち向かうための インターフェース分離と依存の逆転 前田 浩邦 (サイボウズ株式会社) JJUG CCC 2026 Spring 1
自己紹介 ▌前田 浩邦 / Hirokuni Maeta ▌2014年4月 サイボウズ入社 ▌kintone開発チーム /
プロダクトエンジニア ▌最近はサーバーサイドの改善を担当 2
kintone ▌業務システムを作成するためのノーコード・ローコードツール ⚫ 仕事に必要なデータを管理、チームで共有 ⚫ 案件管理システム、顧客管理システム等 3
本発表の内容 ▌機能開発と共に肥大化するレガシーコード ▌インターフェース分離と依存の逆転 ▌機能の公開インターフェース 4
機能開発と共に肥大化するレガシーコード 5
レガシーコード ▌長年の開発を経て、読みづらく、変えづらくなってしまったコード ▌レガシーコードの種類 ⚫ 巨大クラス、役割が不明なクラス ⚫ 循環参照 ⚫ 難解なデータ構造 ⚫
プロパティが多い、用途が分かりづらい ⚫ 多すぎる依存 6 今回の対象
kintoneのアプリ ▌簡単な”データベース” ⚫ 業務に必要な情報を、レコードとして蓄積 する ⚫ レコードをチームで共有、管理する ▌作りたい業務システムの分だけアプリを作る ⚫ 案件管理には案件管理アプリ、等
▌レコードを構成する項目 (フィールド) は、現場 の業務に沿うように柔軟に設定可能 ⚫ 案件管理: 顧客名、部署名、… 7 レコード フィールド
RecordData: レコードを表現するデータクラス ▌主に、フィールドとその値を保持 ⚫ “顧客名” → “池田学院大学”, … ▌レガシーコードになっている ⚫
データ構造が木構造で理解が難しい ⚫ 製品の性質上、様々な機能から依存 されやすい 8 レコード フィールド
木構造 ▌柔軟性、将来的な拡張性を持たせるため、木構造を採用 ⚫ アプリごとにフィールドは異なり、後から追加や削除も可能 ⚫ フィールドの値も様々、レコード自体も値として許容 (レコードの入れ子) ▌その結果、現行のkintoneの仕様以上に機能を持っているように見える ⚫ RecordData自体の作りを理解するのが難しくなっている
▌加えて、RecordDataから単純に値を取得・更新できなくなっている ⚫ 木構造を深さ優先探索するクラスや、探索する各々のノードで処理を実行 するcallbackクラスを駆使して処理を組み上げる必要がある 9
プロパティが多い、用途が分かりづらい ▌多くのプロパティを持っている ▌一部の機能でしか使われなさそうなプロパティを持っている ⚫ stateNameはProcessManagementServiceでしか使われない? 10 class RecordData { private
Long stateId ; private String stateName ; private RecordTitle title; …
多すぎる依存 ▌レコードを使うという機能は、製品の性質上どうしても増えてしまう ⚫ したがって、レコード = RecordDataへの依存も増えてしまう ▌依存元が一斉に影響を受けるので、 RecordDataの修正は困難 11 RecordData
Notification Service RecordTitle Service
そんな中で、レコードを使う新しい機能が増えていく ▌どうやって作る? ⚫ RecordDataに依存するしか… ⚫ 新機能は読みづらく、RecordDataはさらに修正が困難になっていく 12 RecordData Notification Service
RecordTitle Service AiService (新機能)
レガシーコードは減らず、むしろ肥大化する ▌普段の機能開発をしてもレガシーコードは減らない ⚫ kintoneでは、初期の設計にコードの継ぎ足しが繰り返されたことで、 プロパティや依存が増えていき、レガシー化 ⚫ 機能開発は止まらず、その中でどう変えていいのかも分からない ▌減るどころか、むしろ肥大化する一方 ⚫ 実際、RecordDataのようなコア機能でこそ肥大化していく傾向
13
レガシーコードは機能開発も阻害する ▌レコードをユーザーのDBに永続化する機能: 外部システムのアプリ化 ⚫ ユーザーの基幹システムのDBを、kintoneのアプリとして見せる ▌ユーザーのDBにあるデータを、RecordDataに変換して実現しようとしていた 14 サイボウズ DB ユーザーの基幹
システム RecordData Notification Service RecordTitle Service AiService
レガシーコードは機能開発も阻害する ▌アプリ化のために、レコードの各種idをStringで保持したい ⚫ RecordDataの修正は困難で、リスクが高い ⚫ 関係ないはずの、既存の永続化実装にまで影響が出てしまう 15 サイボウズ DB ユーザーの基幹
システム RecordData Notification Service RecordTitle Service AiService Longだった各種idを Stringにしたい
インターフェース分離と依存の逆転 16
当時のコードの凝集度を上げる改善 ▌機能に対応したパッケージにコードを分割し、依存を制限 ⚫ 例: レコード機能のためのパッケージに、レコード機能のコードを移動 ⚫ 可読性の向上、修正の影響範囲を把握しやすくする ⚫ 詳しくは「複雑性に立ち向かうためのサーバーサイドコード分割」として発表 ⚫
https://speakerdeck.com/hirokunimaeta/jjug-ccc-2023-spring ▌web APIのためのservice (WebApiService) を導入 ⚫ controllerはWebApiServiceを呼ぶだけ、他は何もさせない ⚫ 機能実装がwebレイヤーに露出しなくなる、 fat controllerが解消 17
レコード取得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 ( …
RecordWebApiServiceの導入 ▌controllerはRecordWebApiServiceを呼び出すだけ ⚫ 機能に関する一切の知識を持っていない 19 class RecordController { @GetMapping(“/app/{appId}/record/{recordId}”) public
GetResponse get(Long appId , Long recordId ) { return recordWebApiService.get ( appId , recordId ); } }
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); }
クラスと役割 21 GetResponse RecordController RecordWebApi Service RecordWebApi ServiceImpl
クラスと役割 22 RecordController RecordWebApi Service RecordWebApi ServiceImpl レコード機能に関する 知識は何も存在しない GetResponse
クラスと役割 23 RecordWebApi Service RecordWebApi ServiceImpl レコード機能 GetResponse
クラスと役割 24 RecordWebApi Service RecordWebApi ServiceImpl 機能の公開インターフェース 機能の内部実装 外から見えるのはここだけ GetResponse
レコード機能の公開インターフェース ▌レコードを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(); }
公開インターフェースが機能を定義する ▌レコード機能を外から利用する ための唯一のインターフェイス ▌ここを見れば、レコード機能が 何をするのかが分かる ▌レコード機能を定義している 26 interface RecordWebApiService {
AddResponse add( AddRequest request); GetResponse get(long appId , long recordId ); } interface GetResponse { RecordTitle title(); RecordResponse record(); } interface RecordResponse { List< FieldResponse > fields(); }
公開インターフェースの内部実装 ▌内部実装は、公開インター フェースが定義する通りの振る 舞いをするようになっている ▌逆に、この定義通りの振る舞い をすれば、内部実装として問題 ない 27 interface RecordWebApiService
{ AddResponse add( AddRequest request); GetResponse get(long appId , long recordId ); } interface GetResponse { RecordTitle title(); RecordResponse record(); } interface RecordResponse { List< FieldResponse > fields(); }
28 RecordWebApi Service RecordWebApi ServiceImpl RecordData RecordService 機能の公開インターフェース 機能の内部実装 GetResponse
RecordTitleService
アプリ化実装 (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 )); } … // 既存の処理
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 を返すという振る舞いにしたがった実装
31 RecordService 機能の公開インターフェース 機能の内部実装 UserDbData UserDbService RecordWebApi Service RecordWebApi ServiceImpl
RecordData RecordService GetResponse RecordTitleService
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(); } レコード機能の定義
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(); }
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 )); } …
35 RecordService 機能の公開インターフェース 機能の内部実装 UserDbData UserDbService RecordWebApi Service RecordWebApi ServiceImpl
RecordData RecordService GetResponse RecordTitleService
36 RecordService 機能の公開インターフェース 機能の内部実装 UserDbData UserDbService RecordWebApi Service RecordWebApi ServiceImpl
RecordData RecordService GetResponse RecordTitleService 今までとは異なり、RecordDataを使わない処理が増えたが、 公開インターフェースの振る舞いは変わっていない → 機能上は問題ない
残りの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); }
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 (); … // レコードタイトルの生成処理
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からレコードタイトルを生成
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からレコードタイトルを生成
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探索実装
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 ); …
直接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 )); …
最終的な設計: インターフェースによる分離 ▌responseやrecordForTitleを返却するインターフェイスを導入し、 appIdでの分岐やデータ取得の重複を解消 44 interface GenericRecordService { GenericRecord get(long
appId , long recordId ); } interface GenericRecord { RecordResponse response (); RecordForTitle forTitle (); }
最終的な設計: インターフェースによる分離 ▌分岐は無くなり、永続化実装の詳細も隠ぺいされた 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); }
依存の逆転 46 機能の公開インターフェース 機能の内部実装 UserDb Record CybozuDb Record UserDbData RecordData
RecordResponse RecordForTitle GenericRecord
依存の逆転 47 機能の公開インターフェース 機能の内部実装 UserDb Record CybozuDb Record UserDbData RecordData
RecordResponse RecordForTitle GenericRecord コード上はGenericRecordに依存しつつも、 実行時はCybozuDbRecord実装を通して、 RecordDataを使った今まで通りの処理が流れる → 依存の逆転
レガシーコードは改善されたのか? ▌レガシーコード: 読みづらく、変えづらくなってしまったコード ⚫ 難解なデータ構造 ⚫ プロパティが多い、用途が分かりづらい ⚫ 多すぎる依存 ▌レガシーコードによる機能開発の阻害
48
難解なデータ構造 ▌レコードタイトル機能はRecordDataに 直接依存しなくなった ⚫ 難解なデータ構造を理解しなくても、 機能実装を理解可能になった ▌依存しなくなることで、パッケージ内の コードだけを見て機能実装が理解可能に なった ⚫
機能実装の凝集度が上がった 49 RecordData RecordTitle Service RecordForTitle impl
難解さが広がってしまうことによるレガシー化 ▌レコードタイトル生成の理解には、 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 =… … // レコードタイトルの生成処理
難解さを抑え込むRecordForTitle ▌RecordDataの代わりにフィールドの値 を提供 (getValue) するインターフェイス ▌レコードはフィールドの値を保持するもの だったので、レコードがgetValueを持つの はおかしくない 51 RecordData
RecordTitle Service RecordForTitle impl
難解な処理が知られず癒着してレガシー化 ▌レコードタイトル生成が 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からレコードタイトルを生成
このような癒着もRecordForTitleにより解消 ▌RecordForTitleによって、レコードタイトル の生成部分とFieldの探索が分離された ▌RecordDataを変える際は影響範囲を implに抑えることができる ⚫ RecordForTitleが持つのは、レコードが 持っていてもおかしくない振る舞い (getValue) のみなので、レコードタイトル
の処理に踏み込まずにimplを変更可能 53 RecordData RecordTitle Service RecordForTitle impl
RecordDataはそのまま ▌RecordDataそのものの難解さ (木構造) は変わっていない ⚫ 木構造は柔軟性、将来的な拡張性のために必要 ⚫ 単純に難解さを取り除いてしまうと、柔軟性、拡張性も失われてしまう ▌今回のレガシーコードの肥大化の正体は、RecordDataの難解さが広がったり、 周囲のコードと癒着してしまうこと
⚫ RecordDataそのものの難解さはレガシーコードの肥大化とは別の問題 ▌もちろん、 RecordDataをリファクタリングして、柔軟性、拡張性を担保しつつ、 難解さを解消していくことも重要 ⚫ そのためにも、インターフェイスを切っておけば安心 54
多すぎる依存 ▌同様のアプローチで直接の依存を減らしていくことが可能 55 RecordData impl impl impl RecordTitle Service RecordForTitle
Notification Service RecordForNtf AiService RecordForAi
依存の数とクラス数のトレードオフ ▌直接の依存が減る代わりに、機能毎にレコードクラスが増えていく ▌今後の課題 ⚫ 単純に共通化をしても以前の状態に戻るだけで意味がない ⚫ 機能のあり方を踏まえた振る舞いの蒸留が必要 56 RecordTitle Service
RecordForTitle Notification Service RecordForNtf AiService RecordForAi
プロパティが多い、用途が分かりづらい ▌impl側に持たせられないか検討すればよい 57 GetResponse RecordData impl RecordResponse ProcessManagementService impl RecordForProcessManagement
プロパティの移動の例 ▌RecordDataが、画面に表示するための値も保持していた ⚫ 永続化した数値 (model): 1000 ⚫ 画面表示用にフォーマットした文字列 (view): “1,000”
▌画面に表示する値を持つべきなのはRecordResponseレイヤー 58 Record Data ・ 1000 ・ “1,000” RecordWebApi ServiceImpl Record Response ・ 1000 ・ “1,000” DB
プロパティの移動の例 ▌画面に表示する値はRecordResponse実装で生成し、 RecordDataには永続化した値だけを持たせる 59 CybozuDb Record ・ 1000 ・ “1,000”
・ 1000 Record Data RecordWebApi ServiceImpl Record Response DB
プロパティの移動の例 ▌依存する側にとって必要なプロパティを保持する先がRecordDataしか なかったため、プロパティの数が増え、用途が分かりづらくなっていた 60 CybozuDb Record ・ 1000 ・ “1,000”
・ 1000 Record Data RecordWebApi ServiceImpl Record Response DB
二つのレコード実装による阻害の解消 61 UserDb Record CybozuDb Record UserDbData RecordData GenericRecord サイボウズ
DB ユーザーの基幹 システム
二つのレコード実装による阻害の解消 62 UserDb Record CybozuDb Record UserDbData RecordData GenericRecord サイボウズ
DB ユーザーの基幹 システム 各種idをStringで保持可能 こちらは影響を受けない
機能の公開インターフェース 63
機能の公開インターフェース ▌機能を、外部 (他の機能、web) から利用できるようにする唯一の窓口 ⚫ 内部実装は、公開インターフェイスを通じてのみ外部とやりとりする 64 RecordForTitle RecordTitleService RecordWebApiService
レコード機能の 公開インターフェース レコードタイトル機能の公開インターフェース 内部実装 内部実装 RecordResponse
機能の公開インターフェース ▌機能の公開インターフェースは、パッケージ分割やwebとの分離といった、機能 実装の凝集度を上げる活動の結果、必ず生まれる ▌例: 機能Aの実装の凝集度を上げる ⚫ 点在する機能Aの実装を集約し、外部 (他の機能、web) から隠ぺい ⚫
機能Aを外部から使えるようにするためには、機能Aの公開インターフェース が必要 65 外部 機能A Service impl 機能A 機能A 機能A 公開 インターフェース 外部
機能の公開インターフェース ▌公開インターフェースは、外側に近いレイヤーで使うというイメージが強い ⚫ 外部から使えるようにする、という性質のため ▌実際、元のRecordResponseの生成はそのようになっていた 66 Record Response Record Data
RecordWeb ApiServiceImpl RecordResponseを生成
レガシーコード改善を可能にしたアイディア ▌機能の公開インターフェースを、内部実装にも流用すること 67 interface GenericRecordService { GenericRecord get(long appId ,
long recordId ); } interface GenericRecord { RecordResponse response(); RecordForTitle forTitle (); } 公開インターフェースを内部に持ってくることで、RecordDataを隠ぺい
公開インターフェースを、より内部に ▌公開インターフェースは、内部実装よりも安定して存在する ⚫ レガシーな内部実装では、そもそもクラスの役割分担は曖昧で不安定 ⚫ 一方で、機能が存在限り、外部用の窓口という役割も必ず存在する ⚫ 役割がはっきりしていて、後に役割が増えてしまう等の懸念が少ない ▌内部実装に流用しても、リスクは低いと考えられる ⚫
役割が曖昧になっている中へさらに変なものを持ち込んでいる、という ことにはならない 68
公開インターフェースを、より内部に ▌RecordResponseを、より内部で生成 ⚫ これにより、レガシーコード (RecordData) を隠ぺい ▌隠ぺいできただけでなく、実態に即した構造にもなったと考えられる ⚫ 無理なく内部に持ってくることができたのは、RecordDataが既に RecordResponse相当のプロパティを保持していたため
⚫ 元々RecordResponseを内部で生成する構造だったと解釈できる 69 CybozuDb Record Record Data Record Response Generic Record RecordWeb ApiServiceImpl RecordResponseを生成
依存の逆転に公開インターフェースを利用 ▌依存の逆転という手法は一般的に知られているものの、インターフェース をどう切るかという部分が難しい ⚫ インターフェイスをレガシーな内部実装から見出すと、開発と共に役割 が変わったり、無くなってしまうこともあり得るため、依存先として不安定 ▌今回は公開インターフェースを利用し、依存の逆転を行った ⚫ 公開インターフェイス自体は必ず存在しており、内部実装由来でなく 役割も明確なので、依存先としては比較的安定している
⚫ レガシーコード改善の第一歩として現実的 70
まとめ ▌レガシーコードの改善について紹介 ⚫ 機能実装の凝集度を上げる活動 (パッケージ分割やwebとの分離) により、 機能の公開インターフェースが生まれる ⚫ 公開インターフェースに必要な振る舞いをしている限りは、内部実装は自由 ⚫
公開インターフェースをより内部に持ち込み、依存の逆転に利用することで、 レガシーコードへの依存を制限する ▌レガシーコードへの依存を制限した後は、個別に改善していくことが可能 ▌レガシーコードは自然には減らない。機能開発をしていると肥大化し、悪影響 を及ぼす。したがって、レガシーコードへの依存を制限し、改善することは重要。 71
補足 72
レコード機能の中での“レコード”について 73
RecordDataの役割の変化 ▌元々は、RecordData = レコードという考え方で処理を組んでいた ⚫ レコードを永続化する = RecordDataを永続化する ⚫ この考え方により、RecordDataへの依存が多くなっていた
▌今では、RecordData = レコードという考え方は通用しなくなっている ⚫ RecordDataを経由しなくてもレコード機能として振る舞えるため ⚫ プロパティ移動の改善により、RecordDataはサイボウズのDBに永続 化されたデータ、になりつつある 74
“レコード”とは何か? ▌それでは、 コード内のどの部分がレコードに相当するのか? ▌そもそもレコード機能に“レコード”はない ⚫ なぜなら、レコード機能の定義はRecordWebApiServiceにある通りで、 この定義に“レコード”と呼べそうなものは存在していないため ▌“レコード”が何であっても、それどころかあってもなくても、機能には影響しない ⚫ “レコード”を持ち込まなくても実装可能
⚫ 実装上は、 “レコード”が何かを考える必要はない 75
“レコード”とは何か? ▌素朴に思い浮かべる“レコード”は、コード内に存在しないのか? ⚫ GenericRecordが近そう 76 “レコード” RecordTitle Service RecordForTitle Notification
Service RecordForNtf AiService RecordForAi
“レコード”とは何か? ▌“レコード”は、機能に応じていろいろな振る舞い方をしている ⚫ レコードタイトルではフィールドの値を提供する ⚫ 通知では…、AIでは… ▌“レコード”は、各機能視点からのレコードに求める振る舞い (RecordForTitle等) を束ねたものである、と考えることができる ⚫
それをコード内で実現しているのがGenericRecord ▌機能 (コンテキスト) 毎の振る舞いの束であると考えるとつじつまが合う 77