Table of Contents
Open Table of Contents
コードの匂いってなんだ?
こんにちは。
私は普段ソフトウェア開発の現場でコードレビューを行いながら日々開発を行っているのですが、 その中で「コードの匂い」を嗅げることがとても重要だと思っています。
自分はコードの良し悪しを判断するときに、いちいち本を引いていません。
たとえば、コードレビューの中で妙に長い関数があったとします。 それを見たときに Martin Fowler の「リファクタリング」だったり、あるいは「リーダブルコード」の内容をそのまま適用することはあまり考えていないんですよね。 長すぎる関数を見ると、単体テストが書きづらそうだと感じます。そして、単体テストが書きづらいということは、一つの関数の中に複数の責務が入っているのではないか、と自然に考え始めます。 その結果として、関数を分割できないかを考えます。
条件分岐が何段にもネストしているコードも同じですね。 ネストが深くなるほど、コードを読む側は複数の条件を頭の中で覚えておきながら読み進めなければいけません。 認知的複雑度が高くなるので、早期リターンを使えないか、あるいは条件分岐そのものをデザインパターンなどによって別の構造に変えられないか、といったことを考え始めます。
また、命名についてもそうですね。 個人的には、関数名を見ただけで、その関数が何をしているのかある程度理解できる状態が望ましいと思っています。 関数名を見てもよく分からず、結局実装詳細を読みに行かなければならないのであれば、それだけコードを読むための負荷が増えます。 もちろん、名前だけですべてを表現できるわけではないので、必要に応じてdocstringやPHPDocのようなドキュメントも必要です。 ただ、少なくとも名前を見た時点で、その関数の責務について何も分からない状態は避けたいですよね。
こういったコードを見たとき、自分の中では必ずしも最初から明確な原則やコードが具体的に浮かんでいるわけではありません。(浮かんでいる場合ももちろんある) そうではなく、最初にあるのは「もっと単純に書けるんじゃないか」という違和感です。 本質的にやりたいことに対して、コードが必要以上に複雑ではないか。読んでいて妙に分かりづらくないか?
そうした認知的な負荷が、コードの匂いを感じる一つのシグナルになっています。 そして最近、このコードの匂いを嗅げるかどうかというのは、品質を満たすソフトウェアを作るうえで、 かなり本質的な能力なのではないかと考えるようになりました。
コードの匂いは必ずしも問題とは限らない
ただし、コードの匂いを感じたからといって、そのコードが必ず悪いわけではないということは触れておきます。 長い関数だからといって、必ず分割すべきとは限りませんし、ネストがあるからといって、必ず早期リターンにすべきとも限りませんし、コードが重複しているからといって、必ずDRYにすべきでもないと思います。
たとえば、責務の異なる二つのモジュールに、たまたま同じような処理が存在していることはよくあると思います。 その二つを「コードが重複しているから」という理由だけで共通化すると、本来独立しているべきモジュール同士が結合してしまう可能性があります。 そういう場合には、同じような処理が二回存在していても問題ないと思っています。DRY が一般的に良いことだからといって、必ず適用すべき原則かというと、そうではないですよね。
つまり、自分にとってコードの匂いは問題そのものではなく、問題が存在する可能性を知らせるシグナルです。 実際、コードレビューをしていて匂いを感じても、レビューコメントを入れないことは普通にあります。 少し調べてみたら、自分の認識のほうが間違っていたということもありますし、改善できなくはないけれど、わざわざ指摘するほどでもないこともあります。多少気になるものの、この程度であれば十分許容できると判断することもあります。
匂いを感じることと、そのコードを修正すべきだと判断することは別です。
まず違和感を持つ。その違和感が本当にソフトウェア品質上の問題なのかを考える。 コードの匂いを嗅ぐというのは、その最初のきっかけとなる事柄だと思います。で、それがとても重要だと思っています。
表面的なレビュー
私も最初からコードの匂いをなんとなく意識できるようになったわけではありません。 昔の自分のコードレビューを振り返ると、今よりもかなり表面的だったと思います。 命名がおかしいとか、この文法ならもっとこう書けそう、このコードはもう少し短くできそう、そういったレビューが多かったように思います。 当時は『リーダブルコード』や『リファクタリング』のような名著を読みながら、そこに書いてあることを実際のコードにどう適用できるのか、そういった表面的なレビューだったように思います。
一方で、今はそうした表面的な部分については、以前ほど積極的にレビューしなくなりました。そのあたりは他の人に任せることも多いです。 今の自分が見るようになったのは、もっと本質的な部分で、責務の置き場所は正しいのか、このドメインの制約は、本当にこの実装方法で表現するべきなのか、この抽象化は適切なのか、この設計は変更に耐えられるのか、といった 昔よりも、コードそのものの見た目ではなく、その背後にある本質を見るようになったように思います。
レガシーシステムと向き合う
この成長に一番影響したのは、既存のレガシーシステムを自分の手でリプレイスした経験だと思います。
リプレイスするためには、当然ながら既存システムの仕様を理解する必要があります。 そこで既存コードを読むわけですが、とにかく分からない。 処理がいろいろなところに飛び、依存関係が複雑で、全体がスパゲッティになっていました。 仕様を理解するためにコードを読んでいるはずなのに、コードを読んでも仕様が分からない。 さらに、ある処理を変更したときに、どこまで影響するのかも見渡せません。
特につらかったのが、テストを書こうとしても依存関係が複雑すぎて書けないことでした。 単体テストを書こうと思っても、対象のロジックが大量の依存関係に囲まれていて、簡単に切り出すことができない。
この経験によって、内部品質が悪いということが単に「コードが汚い」という感覚的な問題ではなく、生産性に関わる問題であることを実感しました。
内部品質が悪ければ仕様を理解できなくなりますし、変更の影響範囲を把握できなくなります。 自動テストも用意することが難しくなってしまいます。 結果として、ソフトウェアそのものを変更することが難しくなってしまいます。
リプレイスの過程では、DDDやクリーンアーキテクチャのような概念も当然理解していきながら、内部品質の悪いレガシーコードをどうモダンな構成に置き換えていくかをかなり考えました。 その中で「本質的に何が悪いのか」「なぜこのコードは変更しづらいのか」「どうすれば良くできるのか」ということを徹底的に言語化したんですよね。
当時は今のようなAIによるコーディングツールがなかったので、自分の手でコードを書き、自分の言葉でレビューする必要がありました。 分からないままAIに投げて、それっぽい答えを得ることもできなかったんですよね。今は懐かしき StackOverflow もよく見ていましたね。 だからこそ、自分自身で考え、自分自身で説明できるところまで持っていく必要がありました。 この経験が間違いなく、自分の中でコードに対する嗅覚を大きく培っていくことに繋がったと思っています。
知識と経験が圧縮されて直感が生まれる
そう考えると、コードの匂いを嗅ぐ能力というのは、単なる勘ではないと思うんですよね。
当然最初は知識から始まります。
- 関数が長くなると、複数の責務を持っている可能性があること
- ネストが深くなると、認知的複雑度が上がること
- 凝集度を高め、結合度を下げること
- 適切な抽象化を行うこと
- 継承を使うのであれば、リスコフの置換原則を満たす必要があること
- etc.
そういったことを本やインターネット上の記事から学び、その知識を実際の目の前のコードに対してすぐに適用して実践します。 実践を繰り返すたびに、自分がまだ理解できてないことに気づくんですよね。なんでこれがここで適用できるんだろう?
その繰り返しで、知識と経験が少しずつ圧縮されていきながら、自分の血となり肉となることで、コードの匂いを嗅ぎ分けられるようになるような気がします。
この状態で実際に匂いを嗅ぎ、違和感の正体を考えていくと、以前学んだ設計原則や、過去に経験した問題とリンクしてくるんですよね。 直感が先に来て、具体的な理論があとからその理由を説明する感じです。 これが、自分の考える「コードの匂いを嗅げる」という状態だと思っています。
見た目が綺麗でも、設計として匂うコード
コードの匂いというと、巨大な関数や深すぎるネストのような、見た目からして分かりやすいものをイメージするのではないでしょうか。 ただ、実際には見た目が綺麗でも、設計として違和感を覚えるコードはたくさんあるんですよね。
たとえば、値オブジェクトを使える場面で使っていないコードです。 本来、値オブジェクトによってドメイン上の制約を型そのものに持たせることができるにもかかわらず、プリミティブな値をそのまま扱い、ロジックの途中で逐次的に条件判定しているケースはよく見かけます。 条件判定自体は綺麗な関数にまとめられているかもしれませんし、コードだけを見ると読みやすいかもしれません。 でも「そもそも不正な状態を表現できる設計でいいのか」という違和感を私は感じます。
他には、契約による設計に関してもあると思います。 ドメインの境界や制約を仕組みとして実装できるにもかかわらず、それを個々の処理の中の条件分岐で毎回守っているような実装ですね。 ロジック単体では綺麗に見えるので、むしろ問題に気づきづらいのではないかと思います。
あとは、継承も分かりやすい例ですね。 クラスAとクラスBに同じ処理があるから、処理を共通化するためだけに親クラスABを作る。
コードの重複は減るので一見綺麗に見えますが、継承は本来、リスコフの置換原則に従って is-a の関係が成立するときに使うべきものです。 AとBが本当にABの一種であるという関係がないのであれば、単なる処理の共通化を目的として継承を使うのは違うと思っています。 こういうコードは、表面だけを見れば十分綺麗だと思います。なので、適切に匂いを嗅ぎ分けられないと、そこに対して適切な指摘ができない。
だからこそ、設計レベルの匂いを嗅げるかどうかが非常に重要になります。
コードの匂いと単なる好みを分ける
一方で、コードレビューにおける違和感を何でも「コードの匂い」と呼んでしまうのは危険だと思います。 単なる好みの問題があるからです。
自分の中では、ソフトウェア品質への影響を論理的に説明できるかどうかを、一つの境界にしています。
例えば、以下のようなケースです
- この依存関係によって単体テストが書きづらくなる
- この責務配置では仕様変更時の影響範囲が広がる
- この命名では実装詳細を読まなければ振る舞いを理解できず、認知負荷が高くなる
- この抽象化によって、本来独立している二つのモジュールが結合してしまう
- etc.
そういった形で説明できるのであれば、ソフトウェア品質についての議論として扱っていいと考えています。
一方で、「自分ならこう書く」「こっちの書き方のほうが好き」というところから先に進めないのであれば、それは好みに近いような気がします。
もちろん人間がレビューする以上、好みを完全に排除することは難しいでしょうし、ある意味好みが入り込むことによって、そのチーム独自の開発文化が育まれる側面もあると思います。 好みをレビューコメントにすること自体も、必ずしも悪いとは思っていません。 その場合には、「これは私の好みかもしれませんが」という前提を明示したうえでコメントすれば全然 OK だと思います。
自分自身は、そうした好みはなるべくレビューから排除するように心がけていますね。 実装者には実装者なりの意図がありますし、品質上の問題がないのであれば、ある程度は実装者の自由な意図をコードに組み込める状態のほうがいいと思うからです。
また、コードの匂いについて経験者同士で意見が割れることも当然ありますよね。 その場合は、身も蓋もないですが、チームで議論するしかないと思います。 そのためにも、普段から心理的安全性の高いチームを作っておくことは重要ですね…!
良いコードとはなにか?
ここまで考えると、そもそも良いコードとは何なのか、という話になってきそうです。
- 読みやすいこと
- テストしやすいこと
- 仕様変更に強いこと
- ビジネスの概念が適切にコードに表現されていること
- 認知的複雑度が低いこと
- 凝集度が高く、結合度が適切に抑えられていること
自分はこういったものはすべて重要だと思っています。 ただ、それらはそれ自体が最終目的ではなくて、最終的に変更容易性に繋がることが重要だと個人的には思っています。
ソフトウェアは作ってからが始まりで、ビジネスやマーケットが変われば、当然それに合わせてソフトウェアも変わります。 新しい機能を追加したり、既存の仕様を変えたり、利用者が増えれば、システムの構成自体を変えることもありますよね。
そのときに内部品質が悪く、変更すること自体が怖い状態になってしまうと、ソフトウェアがビジネスの変化についていけなくなってしまいます。 一方で、変更容易性の高いソフトウェアであれば、ビジネスの要求に合わせて継続的に形を変えていくことができます。
だから自分にとって良いコードとは、高い変更容易性を持つことで、ソフトウェアを継続的にビジネス価値を最大化する方向へ動かせるコードだと思っています。 コードを綺麗にすること自体が目的ではなくて、綺麗なコードを書くことによって最終的に何が得られるのか、というのが重要です。
AIはかなり「それっぽいコード」を書けるようになった
そして今、このコードの匂いというテーマを改めて考える理由の一つがAIです。
今のAIは、かなりそれっぽいコードが書けるようになりました。 命名もそれなりに良いですし、関数も適度に分割されているし、言語によっては型も付いているし、テストも生成できます。
既存のコードベースに明確な設計原則が存在していて、かつ既に質の高いコードが十分に用意されている場合、AIの出力はかなり安定すると思っています。 AIが既存のベストプラクティスを踏襲した状態で、より新しくコードを生み出すからです。
一方で、そうした土台がない場合にはかなりブレます。 実務でも、一見綺麗なのに、設計としておかしいコードが普通に紛れ込むのをとても多く観測しています。
コードの見た目だけを見ればそれなりに整っているので、それが厄介さを助長しているような気がします。 AIによって、表面的に綺麗なコードを書くコストは劇的に下がったと思います。 しかし、表面的に綺麗であることと、長期的に変更しやすいソフトウェアであることは別ですよね。
AI 時代における「実装力」
そう考えると、これからソフトウェアエンジニアに求められる実装力の意味も変わっていくでしょうね。 AIが大量のコードを高速に生成するようになった今、そのdiffを人間が一行一行読んで確認しているのであれば、どう考えてもレビューがボトルネックになってしまいますよね。
レビュー自体もAIに任せればいい、という考え方は当然あると思います。 ただ、コードを書くのもAI、レビューするのもAIで、人間自身はその出力が良いのか悪いのか判断できないという状態で、長期的にサービスを十分スケールさせていけるのかというと、自分はかなり疑問があります。
何か問題が起きたときに、その判断の妥当性を人間側で検証できないからです。 そして何より、AIが出したコードの良し悪しを判断できない人を、ソフトウェアエンジニアリングのプロフェッショナルと呼べるのか? ユーザーに提供する製品の品質を責任を持って担保したと言えるのか?
なのでAI時代だからこそ、コードの匂いを嗅ぐ能力がより重要になるのではないでしょうか。
AIが出てきたからといって、自分自身の基礎を怠っていい理由にはならなくて、むしろAIの出力を評価するためには基礎力が必要になります。 結局、AI以前もAI以後も、求められている根本的なものはそれほど変わっていないように思います。
いつかこの能力が不要になる可能性はある
とはいえ、自分は「コードの匂いを嗅ぐ能力が未来永劫必要だ」と考えているわけではありません。 もし将来、仕様を渡せばAIが正しく設計し、実装し、テストし、継続的に変更まで行ってくれるようになったとします。 そして、その品質が人間よりも十分に信頼できるようになったとします。 そこまで到達したのであれば、人間がコードを読んで、その良し悪しを判断する必要はなくなるでしょうね。
これは、コンパイラと同じですよね。 現在、多くのソフトウェアエンジニアは、アセンブリレベルの最適化を日常的には行っていません。 その多くをコンパイラが十分な品質で行ってくれるからです。
AIがコードに対して同じような存在になれば、人間にとってコードそのものが意識する必要のない抽象化レイヤーになる可能性は十分にあります。 そのときには、コードの匂いを嗅ぐ能力も不要になるのでしょう。
ただ、少なくとも今はまだそこではありません。 特に、大規模なエンタープライズシステムを作るうえで、すべてをAIに任せ、人間側は出力されたものの良し悪しすら判断できない、という開発は現実的ではないと思っています。 だから今は、コードの匂いをきちんと嗅げる必要があります。
終わりに
コードの匂いとは、悪いコードを機械的に判定するルールではなくて、 コードを見たときに感じる「本質的にやりたいことに対して、少し複雑すぎないか」「ここを変更すると後でつらくならないか」「この設計は何かおかしくないか」という違和感です。
最近は AI がコーディングのほとんどを行うようになり、このコードの匂いを体に叩き込むというのは難しくなってしまったように思います。 今は、AIに頼めばかなりそれっぽいコードがすぐに生成されるからです。
自分のコードを見る目が変わったのは、自分自身でコードを書き、レガシーコードに苦しみ、それをリプレイスし、なぜ悪いのか、どう直すべきなのかを徹底的に考えたからでした。
以前であれば、自分で実装する中で設計上の悪い点を自分自身で考察し、そして自分自身の手で実装して経験する機会がありました。 今はAIが、その摩擦をかなり取り除いてくれるようになりました。
もちろん、生産性という意味では素晴らしいことです。 ただ、その営みの中で身につけていた感覚まで失われる可能性は高そうです。 その意味では、これからエンジニアがコードの匂いを嗅げるようになるのは、以前より難しいのではないかと思ったりしています。
一つ方法があるとすれば、日常的に触れているコードに対して「これをもっとリファクタリングするとしたらどうするか」「実際に構造を変えると、どのような違いが生まれるか」という実験を意識的に行うことだと思います。
プライベートの時間で自分自身の学習として Before と After を作ってみる。 そしてその中でなぜこちらの方が良くて、どう設計に反映していったのかを自分の中で反芻しながら考えてみる。
結局、この能力はある程度、自分の手と汗を使わなければ身につかないのではないかと思っています。
コードの匂いに関して私の考えを書いてみました。何か参考になっていたら幸いです。