🔥

技術的負債が解消できない

2024/06/03に公開

はじめに

レガシーコード改善ガイドに通じるようなお話です。
技術的負債を抱えているプロジェクトに参画しており、解消方法に悩んでおりますが、
過去に関わったプロジェクトでも似たような経験があり、共通点があったので書き残しておきます。

書かないこと

技術的負債の解消の仕方(HOW)は記載していません。
「良いコード」を書くための参考書籍は豊富に存在するので、記載しません。
HOWを知っていても、実践できないという内容となります。

プロジェクトの共通点

  • SESで(お客様のシステムを)開発している
    • 自社開発であれば状況が異なる可能性があります
  • 開発メンバーが、5~10名以上でスキルも様々
  • テストチームが充実している

結局何をやっておくべきだったのか

レガシーコード改善ガイド の帯にあるように、テストコードを書いておくべきだったが結論となります。

テストチームが充実していると、製品の品質はテストチームの試験結果により担保・判断されるため、テストコードの重要性が薄れるのかもしれません。
技術的負債を抱えていると感じたプロジェクトは、アプリケーションの品質を(手作業の)テストで担保していました。

テストコードを書かないと何が起きるのか

CI・CDができなくなる

CI・CD のうち、CIはテストコードによって成り立っていると言って良いでしょう。
テストコードがないということは、CIができないということになります。
デプロイの自動化ができていても、テストが手動では、Continuous Delivery とは言い難いでしょう。

コード品質が悪化する

テストコードを書こうとすると、関数は単機能で小さい方がよく、依存関係はなるべく外部から引数等で渡す方が良いので、
そのような点を意識したロジックになると思います。
巨大な関数でもテスト出来ないことは無いかもしれませんが、テストコードを書いている際に、書きづらいことに気づき、リファクタリングの機会となるでしょう。

逆に、テストコードがない場合、テスタブルな関数であることが強制されないため、コードの品質は悪化しやすくなります。
コードレビューで一定の品質は担保できるのかもしれませんが、テストコードがなくて良いという免罪符とはならないでしょう。

関数の仕様がわからない

テストコードは、その関数のインプット・アウトプットの仕様書になります。
テストコードが無いということは、その仕様が分からないということです。
ソースコードのコメントに書いてある? そのコメントは、今も正しいと担保されているのでしょうか。
(テストケースが十分であるという前提ですが、)テストが成功していれば、インプット・アウトプットの関係は担保されています。

技術的負債は解消(返済)できるのか

技術的負債を解消するためには、テストコードを書くといった、リファクタリングが必要となりますが、このリファクタリングが難しいのです。

ビジネス的な難しさ

まず、このアプリケーションは、コードの品質はともかく、テストチームのテストは合格しており、一定基準の品質をクリアしています。
顕在化している大きな問題はありません。

よってリファクタリングで解消できるかもしれないものは、潜在的な問題・軽微な問題となります。
テストコードも乏しいため、リファクタリングすることで、新たに不具合を発生させる可能性があります。

つまり、お客様に対して、表面上は問題なく動作しているアプリケーションの

  • 潜在的な問題を直すために、
  • 機能的には全くアップデートがないが、
  • 新たな不具合を起こすかもしれない改修にお金を払ってくれ」

というものです。

ビジネスとして、今大きな問題なく動いているならば、新しい機能に取り組みたいと思うのは当然でしょう。
ビジネスレイヤーでは、受注側(まして、いち開発者)が問題視していても仕方なく、お客様サイドのCTOや品質管理部門の方針が重要に感じます。

技術的な難しさ

ビジネスレイヤーの問題が解消し、いざリファクタリングしようとなった時、果たしてテストコードを書いていけるでしょうか。
デグレなくテストコードを書くためには、リファクタリングした関数のアウトプットが変更前後で一致している必要があります。
つまり、何をインプットしたら、何がアウトプットされるか、事前に分かっている必要があるのです。

その関数を作ったメンバーが近くにいれば、まだ幸いです。
しかし不具合修正や機能追加で、他のメンバーの手が入っている可能性もあります。
仕様変更等でいつの間にかデッドコードが含まれている可能性もあります。

そして、テストチームのテストが通っているからといって、必ずしも、その関数が常に正しい結果を返しているとは限りません。
このアプリケーションは、ユーザに影響が少ないエッジケースの不具合が含まれている可能性があるのです。
ユニットテストレベルで見れば、期待値が誤っている、あるいは、正しいと断言できない関数もあるでしょう。

今のロジックから、インプット・アウトプットを抽出し、その妥当性を判断した上でリファクタリングしていく必要があります。
他人のコードを読み解く能力も必要となりますし、アウトプットの妥当性判断にドメイン知識が必要な場合もあります。
直感的に考えられる期待結果と異なる場合、実装ミスによりそうなっているのか、システムの制約によりそうなっているのか、判断しなければなりません。
(ソースコードにコメントでもあれば幸いでしょう)

このように後からテストコードを追加することは、一朝一夕に行かない作業であることがわかります。これをデグレなしで実施する必要があります。
お客様には品質向上という観点でリファクタリングを実施しているのですから、デグレが発生しようものなら、この取り組みの信頼性は失墜するでしょう。

テストコードを書くべきタイミングは、機能実装直後あるいは、TDDでもあるように機能実装前で、実装した人(その実装に知見のある人)が書くのがベストです。
後から書けば良いという点について、一般にテストチームのテストに合格した後は、デグレを起こす可能性があるため、ロジックの変更はNGです。
ロジックを変更せずにテストコードだけ追加できれば良いですが、前述したように、テストを考えずに作られた関数はテストがしにくいことが多いです。
無理やりテストを書くのは無駄に時間がかかるでしょうし、何のテストをしているか分かりにくくなるでしょう。
結局、テストしづらいコードは「良いコード」とは言い難いので、技術的負債を返済できているとは言えないのです。

機能追加と並行してできるのか

前述したように、リファクタリングだけでは、機能的なアップデートがなく、お客様が求めているアプリケーションの価値は向上しません。
そこで、機能追加とリファクタリングを並行して実施しようという案が出てきます。
現実的に採用できるのは、この案だと思いますが、課題もあります。

一般的なgit運用をしていれば当然、機能追加のブランチと、リファクタリングのブランチは別々に作業をすることになります。
機能追加のブランチは、技術的負債を抱えたままのコードがベースとなります。

割れ窓理論にもあるとおり、機能追加では既存のソースコードを流用・模倣するため、技術的負債をそのまま引き継ぐこととなります。
そのまま機能追加した場合、別軸でリファクタリングしても、新たな負債が増えていたということになります。

負債を増やさないためには、機能追加の際に、技術的負債を抱えないように作り変え、
テストコードを記載してリリース(テストチームに渡す)するということが必要となります。
既存のコードにも手を加える場合、どこまでテストコードを書くのか、テストをするのか、という点も考える必要があります。
開発チームに新たな文化を根付かせる必要があり、しばらくは開発スピードが落ちるでしょう。

開発メンバーの一部をリファクタリングに割く、当面の機能追加の工数も増加するため、新規機能のリリース速度は遅くなります。
将来的なCI・CDのため、直近の開発スピードが落ちるということを、ビジネスレイヤーにも理解いただく必要があります。
(ビジネスレイヤーの理解が得られない場合、ここでスピード優先を求められる可能性もあります。)

最初からやっておけば良かったのか(開発レイヤーの問題か)

最初からテストコードが書かれていれば、技術的負債は減っていたのでしょう。
しかしプロジェクト序盤であるほど、

  • とりあえず動くものが求められる
  • ドメイン知識が充実していない
  • プロジェクトの体制・メンバーのスキルも成熟していない

ことが多いと思います。そして重要なのは、テストチームのテストは合格しているという点で、
テストコードがなくても、(不具合修正を重ねた上で、)一定の水準はクリア出来たということになります。
一定水準の品質が担保できたのであれば、テストコードが無い限りリリースさせないといった、仕組みでも無い限り、最初からやっておけば良かったという批判は当てはまらないと思います。

また、テストコードがあったからといって、リファクタリングの機会がなくても良いということにはなりません。
月日を重ねれば技術は新しくなりますし、ドメイン知識も深くなるため、より良いコードを書けるようになるでしょう。
定期的にリファクタリングの機会は欲しいところです。この時、テストコードがあることが重要になります。
しかし、これは開発者のただの自己満足である可能性を否定できません。

チャンスをくれないのが問題なのか(ビジネスレイヤーの問題か)

ビジネスレイヤーがリファクタリングの機会をくれないのが問題なのでしょうか。
前述したとおり、リファクタリングでは機能的なアップデートはありません。

少なくともテストチームのテストが合格しているアプリケーションにおいては、
大幅なリファクタリングをするよりも、ユーザに明確なメリットのある機能追加を優先し、
不具合が顕在化したら、影響を最小にしてアップデートする、という方が費用対効果も良いと考えられます。

結局のところ、技術的負債は解消できるのか

究極的なところ、お客様が求めていないのであれば、(もちろん、全く求めていない訳ではないでしょうが、)技術的負債を解消する必要はありません。

つまり必要とされていなければ、出来ないということです。
(無償で、デグレなく実施でき、それが担保できるならば可能でしょう (笑) )

これはお客様のアプリケーションであり、開発者のアプリケーションではないのです。
一定水準で動いている以上、技術的負債を解消しようとしているのは、開発者の自己満足なのかもしれません。
要望されていないのに勝手にリファクタリングを実施し、新たな不具合でも出した際には、信頼は失墜するでしょう。

もちろん、お客様が求めていないからといって、技術的負債を放置していることには、デメリットもあります。

一つ目に、新たにメンバーが入ってきた時、事情を知らないため、技術的負債を抱えたままのソースコードを見て絶望し、その人の中でのプロジェクト(チーム)の評価が下がる可能性があります。
また、知見のない新人は、一般に先輩たちのソースコードを悪いものだと思っていないでしょうから、悪い習慣を身につけてしまう可能性があります。
これは、良くないものを良くないと分かっている人よりも厄介でしょう。

二つ目に、このアプリケーションの開発が他社に渡った時、(お客様から見た)我々の評価が下がるでしょう。
これは過去に経験のあったことですが、機能開発で、他社が開発したソースコードを渡され所感を求められたことがあります。
技術的負債の詰まったソースコードでしたので、素直にそう伝えたところ、難しい顔をされていました。
(真相は分かりませんが、)他社さんも、気づいていたけれど修正できない状態であった可能性はあります。
しかし、その事情を知らない他人からは、技術的負債を抱えたアプリケーションという評価となってしまいます。

You can lead a horse to water but you can’t make it drink
「馬を水飲み場まで連れて行くことはできるが、馬に水を飲ませることはできない」

という言葉があります。「嫌われる勇気」で知ったフレーズで、私も共感しています。

開発者としては、「良いコード」を追求し、技術的負債に対して問題意識を持ち、お客様に提示はしますが、最終的にはお客様の判断に任せるというのが、私の今の結論です。
それ以上は開発者の自己満足になってしまうという考えに至りました。

もちろん、「機会がなく出来ない」と「スキル的に出来ない」では大きく異なります。
開発者としては「機会があれば出来る」状態にしておきましょう。

終わりに

アプリケーション開発者として、「良い設計」「良いコード」についての書籍を読み漁り、
ノウハウを学んではいるものの、なかなか実践できないという苦悩を文章にしました。

CTO等であれば、いかにビジネスレイヤーのマインドを変えていくか、という話に発展するのかもしれません。
ややドライですが、現場レベルでは、必要ないと言われていることを、やる必要はないという結論に至っています。

潜在的な問題の解決のためにコストかける必要があるが、新たな問題を顕在化してしまうリスクを孕んでいるという行為を、
ボトムアップで納得してもらうのは、なかなか難しいでしょう。
「動いていれば良い」というビジネスレイヤーの意見も一理あり、何がベストかは状況によるのでしょう。

開発者目線では、もうウンザリです。何も改善できません状態なのかもしれません。

私個人としては、引き続き「良い設計」「良いコード」について追求し、使える状態にしておきたいと思います。

GitHubで編集を提案

Discussion