🔌

消耗せずに「良いコード」とはなにかを考える

2024/11/03に公開

次の記事が最近公開されたので、読んでみました。

https://developersblog.dmm.com/entry/2024/11/01/110000

結論としては、例えば同著者の「良いコード/悪いコードで学ぶ設計入門」という書籍と比較すると、だいぶ受け入れやすい主張になっていると感じました。(以前の書籍についてのコメント記事へのリンク
ところで、私は「良いコード」についての議論や指摘や検討を積極的にやったほうがよいと思っていますが、主に「消耗しない」という観点でこの記事についていくつかの構造理解やテクニックの部分で補足できそうだったので、以下補足していきます。

ざっくりとした主張でいうと、

  • トレードオフに見える部分は学習・教育で解決できるケースも多くある
  • 品質特性への還元が難しいがコードの良し悪しを定める概念がある
  • Webアプリにおいても再利用性は必要だし、モバイルアプリでも再利用性を求めて失敗することがある
    • 再利用性というよりは、現実に即した概念の線をどこで引くかのバランスを大事にする
  • すぐにはっきりした結論が出ない議論への耐性をつけて、長期的に議論する

といった感じです。

ソフトウェア品質特性間のトレードオフが生じる事は、どちらかというと例外

元の記事の提案3では、機能性と変更容易性の間でのトレードオフ構造についての言及があります。たしかに、論理的にはトレードオフ構造が発生するケースはあるのですが、では、それはどれぐらいの頻度で発生するものなのでしょうか?
具体的なデータを提示できず、また機能性・変更容易性それぞれの具体的尺度も提供できないのが申し訳ないのですが、私の個人的な体感では、(よほど拘らない限りは)多くても2割程度ではないかと思います。別の言い方をすると、だいたいの場合はソースコードのレベルでは機能性と変更容易性は両立します。そこにトレードオフの構造があるように見えるのは、 実装・できあがったソースコードとは別のレイヤーの問題 と思っています。つまり、実装する個人において、

  • 機能性の高いソースコードを書けるが、変更容易性の高いソースコードの書き方を知らない
  • 変更容易性の高いソースコードを書けるが、本質的な機能性の発想力や理解が乏しい

という差があり、それによってトレードオフ構造があるように見える、ということです。
これの根本的な解決方法は割と単純で、学習・教育 です。つまり、単純に足りていない知見を学ぶことで、両立できるような書き方をする・そのようなコードを書くことを認知科学的な意味で自動化していくのが、長期的に良い対応ということです。
ここで重要なのは、コードを書くことを認知科学的な意味で自動化するということです。例えばある種のソースコードの記述方法の定義を理解していたとしても、それがプログラマの中で自動化された生きた知識になっていなければ、結局は記述が拙いままになったり、過剰な時間がかかったりしてしまいます。自動化してしまえば、トレードオフの構造がなくなり、「機能性と変更容易性をある程度両立できるという意味での良いコード」を自然に記述できるようになります。
極限的な状況では両立が難しいケースも確かにありますが、ほとんどの場合について、まずそれを両立する方法がないか、また両立するような書き方を自動化できないか(例えば、同じロジックを空から何度も記述するなどして体に馴染ませて、暗記しているつもりではないのに同じ出力が勝手に出てくるという状態にできないか)ということを考えたほうがよいと思います。
ここで自動化と言っていることの感覚は、例えば次の記事で説明されているような感覚です。

3ステップ目や5ステップ目で何回書いてもだいたい同じコードになる感覚をこの練習を初めて1ヶ月くらいで持つようになりました。

https://hayapenguin.com/notes/Posts/2024/04/24/how-to-practice-coding-effectively

品質特性への還元が難しい議論

具体的な品質特性に直ちに還元するのは難しいが、しかし重要な観点というものも存在します。
例えば、オブジェクト指向的なポリモーフィズムを多用した実装にするのか、高階関数を多用した実装にするのか、高階関数ではないごく単純な関数呼び出しを基調にしたトランザクションスクリプト的な実装にするのか。これは、抽象的なレベルでは品質特性と関連付けすることも可能かもしれませんが、定量的に正しい関連付けをすることは、少なくとも今の時点では不可能であると思います。しかし、全体の設計方針としてはそこそこ重要な選択になるはずで、そうしたことを全く検討せずに決めるわけにはいかないでしょう。
常に品質特性に還元するというよりは、ときにはもっと違った観点での議論も含みつつ、多角的に「良いコード」について検討をすることが重要だと思います。
また、そうして多角的に検討する際は、 仮にトレードオフが発生しているように見えてもそれが見かけ上のものであったり、教育の不足などの別の事由で発生しているものではないかといった観点で自分の直観を否定的に検証する ということが重要です。よくあるのは、現時点の自分の知識や過去の経験に必要以上に引っ張られてしまって事実認識が歪められるということだと思います。そうしたことがないようにフラットに議論をするということが重要なことで、その中の一つの方法論として品質特性への分解がある、といった理解がよいのかなと思いました。

何をもって再利用性とするか - 再利用性は必要

私は主にWebアプリ開発をしていますが、Webアプリにおいても再利用性は重要だと感じています。
例えば、ECサイトで決済結果を確定して確保した在庫を配送に回す処理をAとするとき、この処理Aは以下のような場面で必要となる場合があります。

  • 購入者向けのWeb画面において、決済を行った場合(特にクレジットカードなど)
  • バッチ処理や結果通知で、決済のステータスを確認して決済が完了していた場合(特にコンビニ決済など)
  • 管理画面でWeb申込のステータスを決済確定に更新した場合
  • 外部向けに公開しているAPIで、外部システム側で決済が完了した場合
  • 内部向けAPIで対面決済をサポートしていて、対面決済が完了した場合(決済端末を利用した決済など)

このような処理Aについて、もし再利用性が低いコードを書いていると個別に実装を行ってしまう可能性があります。もちろん、細かく制御すべき内容が異なる部分もあるでしょうが、結果の確定処理の主要な部分は基本的には同じ概念で括れる共通の内容のはずです。
一般に、このような確定の処理はかなり複雑な内容になることが多いはずで、それを個別に実装すると大変なことになると思います。
もっと細かいことをいうと、例えばWeb決済に限っても決済方法によっては完了通知を受けるエンドポイントやコールするAPIが違っていたりすると思いますが、そうした様々なエンドポイントでもし個別に確定処理を書くと、極論では決済方法が一つ増えるたびにそれを実装することになります。テスト・テストコードは決済方法の数に比例すると思いますが、単純な実装部分のコード量については、なるべく増加を抑えるようにしたほうがよいでしょう。

これは、元の記事で

※かと言って重複コードを許してよいというわけではありません。 目的や意図が同じコードはDRYにしましょう。目的や意図が異なるコードをDRYにしてはいけません。

と書かれている部分と同じことを言っているのだと思いますが、目的や意図によってコードを再利用できるようにするという考え方は指針として重要なことではあると思うので、誤解の無いように書いておきました。再利用性が高い状態というのは、個別の処理の独立性が高いことがその一つの要件となるので、再利用性が高い状態にしようとすると自然と悪いコードになりにくくなる傾向もあります。 再利用自体は必要、しかし再利用すべきでない別の概念までまとめるのは不適切、 という受け止めの方が実践的に役に立つかな?と思いました。

ちなみに、処理Aは、抽選申込をサポートしている場合には「抽選処理の中で決済を行って、決済が成功した場合」にも必要になったりします。処理Aが適度にモジュール化されていれば、ゼロから抽選を実装するよりもむしろコストが低くなるのですが、そうでない場合にはゼロからの実装とコストが変わらなかったり、あるいは実装後のメンテナンスコストがとんでもないことになったりします。例えば配送事業者が増えた場合に、すべての箇所について個別に修正が必要ということになると、目も当てられません。

※処理Aの実際の実装としては、各処理の中で完全な処理を直接呼ぶのではなくて、各処理ではステータス更新に留めて実際には別のバッチ処理・パイプライン等で行うという方法もあります。ただし、その場合であっても、ステータス更新の処理・仕様については、やはり再利用性を考慮しておくべきです。

Webアプリではなくて、下手に再利用性を求めすぎてはいけない場合

一方、Webアプリではないモバイルアプリの実装で、下手に再利用性を求めることで失敗するケースもあります。
モバイルアプリで現金決済を実現する場合には、自動釣銭機と呼ばれる現金を取り扱う機材との接続が必要になります。一般論として、モバイルアプリからプリンタへの接続を考えるときは、プリンタへの印字命令の部分を抽象化してインターフェイスとして扱うようにして、具体的に印字する部分のみを個別に実装するという線の引き方が自然です。そこで、自動釣銭機への接続においても、共通の自動釣銭機インターフェイスを作ってそれを個別実装するという考え方で再利用性を高めようとして実装すると...?

なんと、爆死します。

というのは、自動釣銭機はプリンタほどのインターフェイスの抽象化が進んでおらず、個別実装がいくつもあるからです。わかりやすい例を一つ挙げると、例えば一万円札の取扱について。一万円札というのは、普通は釣り銭で使うことはない札なので、一万円札をお釣りとして出せるようにするか・出せないようにするかという実装がメーカーによって異なる場合があります。一万円札をお釣りで出せない仕様の場合、概念的には、金庫が2つ(以上)あると言って差し支えありません。お釣りとして出せる金庫と、出せない金庫です。このとき、例えば現在の釣銭機内の金額(≒売上)や、出金作業時の出金可能枚数の表示はどのように考えるべきなのか?
金庫が2つの場合でも金庫が1つの場合と同様に考えるという方法もありますが、私の結論としては、これらの場合は表示インターフェイスから根本的に変えるべき、です。
他にも大小様々な違いが存在して、例えばX社とY社では出金にかかる時間が大きく異なるとか、共通化して考えると苦しくなる要素がたくさんあります。

もっとも、どこかのレイヤーでは、(概念的な)共通のインターフェイスを作ることは必要だと思います。この自動釣銭機の場合には、「自動釣銭機」という抽象化にメリットがなく、「決済方法」という抽象化で十分で、「決済方法」の中に「現金」ではなくて「自動釣銭機A」「自動釣銭機B」を個別に入れるのが適切だった、ということです。
概念的なモデルをきれいにして世界を見ようとすると、「現金」という抽象化、あるいは「自動釣銭機」という抽象化の中に「自動釣銭機A」「自動釣銭機B」という区分を考えたくなりますが、実際にはそれらの現金の取り扱い方が(金庫にお金を貯めて、投入金額に従って釣り銭を自動で出すだけなのに)本質的に違っている部分もあって、うまくはまるモデルを作れないということがあります。
この場合でいうと、例えば決済と印刷は独立であるべきで、実際に別モジュール・機能として整理して、その2つの間の無数の組み合わせを実現可能にできますが、では決済機能の中にどう線を引いていくかというときに、安易に自動釣銭機という抽象化をしようとすると、現実世界の制約によって下位概念の実装に振り回されてしまうということでした。(だから、上位概念は下位概念に依存してはいけないという理想論は正しいのですが、一方で現実はそうなっていない時もある、ということですね!かなしい)

※ちなみに、店舗のモバイルアプリのレイヤーではこうなのですが、一方でそれを管理集計する立場でいうと、やはり現金としてまとめたいというニーズもあります。複数の店舗を管理して売上を集計する場合、店舗Sで釣銭機A、店舗Tで釣銭機Bを使っているとかはどうでもよくて、それが現金かクレジットかというような決済種別で見たいことがあります。ただし、クレジットはブランドによって料率が異なるケースがあるので、そちらはブランド別に見たいということもあります。電子マネーも同様の理由でブランド別で見たいのですが、しかしそれがSUICAかPASMOかICOCAかという区別は必要なくて、これらは交通系ICという区分で十分だったりもします。地獄

ちなみに、この金庫2つの場合をどう扱うかみたいなことは機能性、互換性、使用性などに該当すると思いますが、これを定量的に扱うことは相当難しいでしょう。でも、どのような設計が良い設計であり、ひいては良いコードであるかは、可能な限り事前に検討・相談した方が良いです。爆死するかしないかの運命が分かれるからです。私は何度も爆死しました

なぜ消耗するのかを別の観点で考える

ところで、議論で消耗するということについて、別の観点で考えてみましょう。例えば、スポーツチームにおいては、良い技術の紹介や良い戦術の検討をすることは有意義で価値のあることです。開発チーム内でそうした議論をする際に消耗してしまうとしたら、もう少し別の問題があるように思います。元の記事に示されているパターン以外でありがちなのは、以下のようなことでしょうか。

コードの指摘が人格の否定として受け取られる

トレードオフ構造が知識の不足で発生することを指摘しましたが、そうするとコードへの指摘がその人の知識不足への指摘と直結しがちになってしまい、

  • 指摘を受けた当人の知識では、指摘の内容を理解できず、結果としてただの人格否定として受け止めてしまう
  • 指摘を受けた当人が内容を理解できるが、それを自身の知識の無さへの一種の人格否定として受け止めてしまう
  • そうした指摘への反論が人格否定(または人格否定として捉えられそうな内容)になってしまう

といったことがあります。これについては、コードへの指摘がただちに人格を否定しているものではなく、より良いコードを目指しての指摘であるということをお互いに理解しておく必要があります。

コードの改善が生じる効果について納得できない・良いコードの価値を信じられない

安易な相対主義に陥ってしまって、実際にコードの改善が良い効果を生じるという事実、あるいは その人が良いコードを書けるか否かが一緒に働くか否かを決める一つの理由になり得る(ぐらいに大事) という事実を認められないというケースがあります。
これは、多くの場合は単に自分の知識の中だけで考えるとわからない、ということだと思うので、まずは事実を受け止めようとすることが大事だと思います。といっても、定量的に示すのが難しいことがほとんどではあるので、まずは 議論する相手を信頼する ということを考えましょう。

はっきり白黒の出ない議論や、複雑な結論を有する議論・認知に慣れていない

良いコードについての議論の結果、はっきりとした白黒がつかず、たとえばこのケースはこう、別のケースではこう、といった複雑な結論になってしまうこともあります。これは、コードを書くことが本質的に難しいので、単一の観点に落とし込めない対象であるということの示唆だと思いますが、そうした議論にそもそも慣れていないというケースがあるように思います。
一般論として、物事には単純な結論が出ないことも多くあり、良いコードというのもそうなのだと思います。
ただ、難しいからといって考えなくてよい訳ではなく、先に述べたように一緒に働く同僚を選別する一つの材料になったり、実際に品質特性に大きな影響を与えたりもするので、そうした構造を踏まえて理解を深めることが重要です。

むすび

良いコードの概念は難しいですが、難しいからといって考えなくてよいかというとそんな事はなく、常に考え続けるべき対象であると思います。ただ、それを考えるにあたって消耗するケースがあるというのも事実です。不必要に消耗することは避けつつ、しかし本質的に考えて体力を使うことは必要だと思って、良いコードについて考えていきたいですね。

ざっくりまとめ:

  • トレードオフに見える部分は学習・教育で解決できるケースも多くある
  • 品質特性への還元が難しいがコードの良し悪しを定める概念がある
  • Webアプリにおいても再利用性は必要だし、モバイルアプリでも再利用性を求めて失敗することがある
    • 再利用性というよりは、現実に即した概念の線をどこで引くかのバランスを大事にする
  • すぐにはっきりした結論が出ない議論への耐性をつけて、長期的に議論する

合わせて読むと面白いかもしれない記事たち

https://zenn.dev/339/articles/ecc4986473ca88

https://zenn.dev/339/articles/e3c174fdcc083e

https://zenn.dev/339/articles/83caa21b9ad736

https://zenn.dev/339/articles/4ba6794874d097

https://zenn.dev/339/articles/1c673f087b5748

Discussion