145
180

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

最良のコード

Last updated at Posted at 2024-04-21

導入

  • 🪄 変数の利用を最小化し、代わりに定数を多用する
  • 🪄 副作用がなく引数しか参照しない関数を量産する
  • 🪄 判定関数を量産する

より良いコードにする為の、こういった個々のテクニックはそれはそれで大切で、無数のテクニックを日々身に付け磨いて行く必要があるでしょう。この記事では、そういった言わば小手先の個々のテクニックではなく、もっと大筋の、そもそもどう言うコードを目指すべきなのか? コードはどうあるべきなのか? といったテーマを扱います。

《最良のコード》を目指してはいけません!

そもそも《最良のコード》とはどんなコードでしょうか? コードには常に先立って《要求》が存在します。どんなコードが最良であるかも《要求》に依存します。つまり、コード単体を以てして、それが最良かどうかを言及する事はできません。それは採点方法も決まってないのに、100点満点だ!と言ってしまう様なものです。

また、コストを掛けずに《最良のコード》を書けたのなら、それに越した事はないでしょうが、多くの場合、それはオーバーエンジニアリングの産物であり、必要以上にコストがかかっている事でしょう。

そして残念ながら、《要求》は変化します。《最良のコード》を目指したところで後出しジャンケンで採点方法やゴールが動いてしまうワケです。

《要求》の変化がごく稀な事であるならば割り切ってしまうのもありでしょうが、いざ実装ができ上がってみたら「コレジャナイ!」となってしまったり、市場の変化や、その他の理由で計画が変わる等々によって、《要求》は「割と普通に」変化します。

《要求》が変化すると、《最良のコード》は《最良のコード》でなくなり、《最良のコード》の為に費やしたコストは回収不能な埋没コストとなります。そしてそれは「割と普通に」起こります。

👉「こちらを上げればあちら下がる、あちらを下げればこちらが下がる」と言った、《最良のコード》どころではなく、「どちらがまだマシなコードか?」と言った、そもそも《最良のコード》と呼べる様な《解が存在しない状況》すら往々にして存在します。

《最良のコード》を目指してはいけません!

  • 💡 どんなコードが《最良のコード》であるかは《要求》に依存する
  • 💡《要求》は「割と普通に」変化する
  • 💡《最良のコード》の為にかけたコストは「割と普通に」回収不能な埋没コストになる
  • 💡《最良のコード》と呼べる様な《解が存在しない状況》すら存在する
  • ✅ コストがかかってない《最良のコード》
  • 🚫 コストをかけた《最良のコード》
  • 🚫 コストをかけて《最良のコード》を書く

👉 ごく短い関数などは、品質の振れ幅も狭く「普通に丁寧に」書けば《最良のコード》になってしまいがちで、それを無理に雑に書く必要はありません。「普通に丁寧に」書いてください。

《必須要件》と《努力要件》

では、どうすべきなのか? まずは前提となる《要求》について掘り下げる必要があります。

👉《要求》そのものを事前にしっかり精査して精度を上げておくことで、《要求》の変化を抑えるのも大切だったりしますが、現物が出来上がってこない事には見えてこない事も多く、《要求》の精査をする為に手戻りを承知の上で実装を進めるなんて事も多いでしょう。

《要求》は大きく《必須要件》と《努力要件》に切り分けられます。

《必須要件》は「必ず達成されなければならない」要求であり、「xxxであってはならない」と言った禁止事項的な内容も含み、また、覆すのが困難な、プログラミング言語や環境の仕様からくる制限等も暗に含まれる事になります。《必須要件》はその定義からして、通常は必ず対応しますが、実装コスト及び将来のメンテナンスコストがあまりに大きく、その要求のバリューに見合いそうにない場合には必要に応じてその《必須要件》を修正あるいは取り消します。

《努力要件》は「必ず達成されなければならないと言うワケではないが、そうである事が望ましい」要求であり、言わば《都合》です。《努力要件》は通常対応しませんが、実装及び将来のメンテナンスコストがあまり掛かりそうになく、その要求のバリューに十分旨みがあると考えられる場合にのみ実装・対応します。

同じ要素に対する《要求》でも《必須要件》と《努力要件》は同時に存在することがあり、ある処理の実行速度が「速ければ速い程望ましい」と言うのは《努力要件》であって、《必須要件》としては「3秒以内」と言った具合になります。

👉 なお、処理速度のような要求は、直接的なパフォーマンス改善で達成するより、いくら処理に時間がかかってもユーザーのストレスにならない形にしてしまうと言ったアプローチで解決する方が望ましいです。単純な処理速度改善では、要求時間内にとても処理しきれない処理内容になった時に必然的に破綻するので。

通常、「xxxな程良い」と言った「《客観的に明確な達成条件》が存在しない《要求》」は《努力要件》となります。《必須要件》は必ず達成しなければならない《要求》である以上、《必須要件》とするには《客観的に明確な達成条件》を定義する必要があります。この為、「できる限り高速に動作すること」、「使いやすいインターフェイス」、「高いメンテナンス性」などと言った《要求》は全て《努力要件》となります。(もちろん《客観的に明確な達成条件》はあるが《必須要件》ではない《要求》である《努力要件》も存在します。)

  • 💡《要求》を《必須要件》と《努力要件》に切り分ける
  • 💡《必須要件》は default で対応する
  • 💡《努力要件》は default で対応しない
  • 💡《必須要件》には《客観的に明確な達成条件》の定義が必要
  • 💡《客観的に明確な達成条件》がない《要求》は全て《努力要件》

目指すべきモノ

《最良のコード》の代わりに目指すべきは《正当性》と《簡潔さ》です。

  • 💡《最良のコード》の代わりに目指すべきは《正当性》と《簡潔さ》

《正当性》

ここで言う《正当性》は簡単に言うと「コードの理論的な正しさ」です。整数値を2倍にしたいからと言って、ビットシフトするのは間違ってますし、ビットシフトが必要とされてる時に2倍にするのも間違ってます。 isDog() と言う判定関数が必要な場面で、 ! isCat() で代用するのは間違ってますし、その時点では CatDog しかなくとも Bird が増えた時点で大変な事になります。

👉 プログラミング言語や環境などの《都合》上、理論的に正しいコードだとあまりに《都合》が悪かったり、理論的に正しいコードを書きようがないと言った場合、その部分だけ個別の関数として抜き出し、理論的な誤りが他に伝播しない様に正しくないコードを隔離してください。( isDog() の実装が ! isCat() になってるのは許されます。 Bird が増えた場合やその他の不都合が発覚した場合でも isDog() の実装を修正するだけで済みます。 )

《正当性》を目指す目的はそのまんまコードを正しくする為です。

👉《正当性》として目指すのはあくまで「コードの理論的な正しさ」であって、《要求》に対する正しさではない点に注意してください。

👉 例えば、プログラムが自身のプロセスを終了するのに各種後処理をスキップしていきなり abort する様にしたとしましょう。昨今の一般的な環境であればプログラムが確保していたメモリも、握っていたファイル等のリソースのハンドルも、プログラムが終了すれば自動的に解放され、素直に後処理としてちまちまと一つずつ全部解放していくよりずっと高速にプログラムを終了できます。これは前提にもよりますが、《要求》に対して望ましく正しいコードになったとしても、理論的に《正当なコード》ではなく《ハック》です。そして後日、スキップするワケにはいかない後処理を行う必要が出てきた時に、ただ abort を辞めるだけでは済まず、いままで実行されてなかった為に発覚してなかった後処理の潜在バグの対処に苦しむ事になるのはお約束のパターンの一つです。

👉 その他、あらゆる《ハック》的手法を用いたコードは《正当なコード》ではありません。《ハック》を使ってはならないと言うワケではなく、《ハック》による各種デメリット( 堅牢性、メンテナンス性、等々の低下 )をメリットが上回るならその《ハック》を採用するのはありでしょう。しかし、それは許されると言うだけであって決して《正当なコード》ではなく、様々なデメリットを背負う必要もあります。

勿論、《必須要件》に対する正しさは必ず満たさなければならないワケですが、「コードの理論的な正しさ」を軸足とする事で、近視眼的に《要求》に惑わされたコードになってしまう事を避け、《要求》の変化に対するコードへの影響も最小化します。

副次的に結果として、セキュリティホールやその他バグの発生率が下がり、メンテナンス性が高く《要求》の変化時にも手戻り的なコード修正の発生率も下がる事でしょう・・・これは実質的に《最良のコード》の目指せと言ってる様なモノですが、「結果として」であって直接的に「目指す」のではありません。

  • 💡《正当性》は「コードの理論的な正しさ」
  • 💡《正当性》によってコードを正しく保つ

《簡潔さ》

ここで言う《簡潔さ》は簡単に言うと「コードの意味的な簡潔さ」です。

👉《必須要件》に対して過剰な正確性・精確性を減らす事です。「その条件や分岐は《必須要件》を満たす為に本当に必要ですか?」

👉 勿論、それ以前に、単純に未熟だったり推敲が不十分で無駄・無用に複雑になってしまってるコードは、普通に清書して《簡潔》なコードにする必要があります。

👉 コードがより短い事は必ずしもより《簡潔》である事を意味しない点に注意

《簡潔さ》を目指す目的は《正当性》を求める為にコストを掛け過ぎない様にする為です。

👉「コストを掛け過ぎない様に」と言うのはあくまで目的です。ただコストを掛けない事を是として、ただ雑なだけのコードを量産する事を許してしまわない様に、目指すのはコストを掛けない事ではなくあくまで《簡潔さ》です。ただし、同時に《簡潔さ》の為に多大なコストを掛けてしまっては本末転倒になってしまうので「コストを掛け過ぎない様に」と言う目的意識も重要です。

副次的に結果として、読み書き及び動作確認・追跡・検証がより楽でセキュリティホールやその他バグの発生率が下がるコードになるでしょう・・・これも実質的に《最良のコード》の目指せと言ってる様なモノですが、「結果として」であって直接的に「目指す」のではありません。

  • 💡《簡潔さ》は「コードの意味的な簡潔さ」
  • 💡《簡潔さ》によってコストを抑える
  • 🚫 コストを掛けないように雑なコードを書く
  • 🚫 多大なコストを掛けて《簡潔さ》を追求

優先順位

《正当性》をとことん追求すると《最良のコード》を目指した場合と良くも悪くも同様の状態になりかねません。そのカウンターとして《簡潔さ》が重要になり、著しく《正当性》を損なわない範囲で《正当性》よりも《簡潔さ》が優先されます。

《簡潔さ》よりは《必須要件》が優先されますが、《簡潔さ》を緩めるとコストは簡単に爆発するので、《必須要件》を満たす事が可能な範囲で且つ著しく《正当性》を損なわない範囲で最大限に《簡潔さ》を追求します。

そして《簡潔さ》を優先する前提で《正当性》を追求します。いくら《簡潔》でも《正当性》に欠ける様では、それはただ書き殴っただけのコードと変わりません。コストを掛け過ぎるワケにはいかないですが《正当性》は重要であり、それなりにコストを掛けてでも《正当性》を追求する必要があります。

👉《簡潔さ》を優先した為に、間接的に或いは《要求》の変化などにより《必須要件》を満たせなくなり、大幅な書き換えが発生する事も時にあるでしょうが、《簡潔さ》が優先されたコードは、コストが対して掛かってないので、丸々書き直しになっても損失は比較的小さく済むでしょう。

👉 基本的に《都合》や《努力要件》の為に《ハック》を用いる事は推奨されませんが、《必須要件》を満たした上で著しく《正当性》を損なわない範囲内での「《簡潔さ》の為の《ハック》」は推奨されます。

  • 💡《正当性》よりも《簡潔さ》を優先
  • 💡《必須要件》を満たせる範囲で且つ著しく《正当性》を損なわない範囲で最大限に《簡潔さ》を追求
  • 💡《簡潔さ》を優先する前提で《正当性》を追求

《都合》や《努力要件》で《正当性》と《簡潔さ》を台無しにしない!

十分に絞られた《必須要件》を元に《正当性》と《簡潔さ》を追求して行くことは比較的楽に実践できる事でしょうし、欲を出して《必須要件》や対応する《努力要件》を増やす程、困難になります。《必須要件》や対応する《努力要件》を削る《勇気》はとても重要です。

往々にして《都合》や《努力要件》を優先すれば、コードから《正当性》も《簡潔さ》も失われます。

《正当性》と《簡潔さ》を優先した 結果、以下の様なコードになってるのは問題ありません。

  • 速いコード
  • 短いコード
  • 分かりやすいコード
  • 使いやすいコード

しかし、《正当性》と《簡潔さ》を優先したコードでそうはならなかったからと言って、安易にこれらの特徴を持つ様にコードを修正して《正当性》と《簡潔さ》を崩してはなりません。通常、それらの特徴よりも《正当性》と《簡潔さ》が優先されます。《正当性》と《簡潔さ》を犠牲にせず、あまりコストもかけずにそうできる場合にのみ、これらの特徴を持つ様にコードを修正しても良いでしょう。

  • 💡《都合》や《努力要件》で《正当性》と《簡潔さ》を台無しにしない

速いコード

コードの動作速度の改善は、往々にしてメンテナンス性の観点からは最悪な事になりがちで、プロファイリングを行いホットスポットである事が証明された上でのみ行われるべきで、さらに、動作速度の改善の為に《正当性》と《簡潔さ》が著しく損なわれる場合には、その後の問題の追跡や、処理内容の理解、いざと言う時の差し戻しの為に、オリジナルのコードは残しておいてメンテナンスし続ける事が推奨されます。

  • 💡 コードの動作速度改善は、プロファイルを行いホットスポットである事を確認した上で
  • 💡 コードの動作速度改善する前のコードも、その後の問題の追跡や、処理内容の理解、いざと言う時の差し戻しの為に、オリジナルのコードは残しておいてメンテナンスし続ける

短いコード

《正当性》と《簡潔さ》を崩してまで短いコードにするのは単純にやめましょう。その必要性やバリューは無く、単純にただの改悪になります。

  • 💡《正当性》と《簡潔さ》を崩してまで短いコードにするのは単純にただの改悪なのでやらない

分かりやすいコード

初めて目にする理想的なコードと、いま自分自身で書き殴ったばかりのクソコード、どちらが分かりやすいでしょうか? 残念ながら、これはどうしようなく後者です。この様に《分かりやすさ》と言うのは、どうしようもなく主観的です。同一人物ですら、時間が経過すれば、割と評価基準すら激しく変化します。《正当性》や《簡潔さ》も、主観的な部分がなくはないですが、《分かりやすさ》に比べると遥かにマシです。

《正当性》と《簡潔さ》を優先したコードが時に分かりにくいコードになってしまう事はあるでしょう。しかし、ここで《正当性》と《簡潔さ》を崩してまで《分かりやすさ》を優先する事は、メンテナンス性を悪化させたり、バグを誘発しやすいコードになってしまう事でしょう。かと言って、分かりにくいコードと言うのは、それもそれでメンテナンス性が悪かったり、バグを誘発させかねないので、《正当性》と《簡潔さ》を優先したコードがそうなってしまった場合は、コメントで補足・補完しましょう。

  • 💡《正当性》と《簡潔さ》を崩してまで分かりやすいコードにするより、コメントで補足・補完する

使いやすいコード

どのようなコードが使いやすいかは、呼び出し側コードのスタイルや構造、利用用途等、様々な要因で変化します。善かれと思って調整しても、ちょっと使い方が変わると一変して使い難いコードに化けたりします。とは言え、使い難いコードになってしまってるままだと、呼び出し側が無駄に煩雑で冗長なコードになってしまったりバグを誘発したりといった事になりがちなので、《正当性》と《簡潔さ》を優先したコードが使い難いコードになってしまった場合は、必要に応じて使いやすくする為のラッパーコードを別途書いて、本体コードの《正当性》と《簡潔さ》を維持しましょう。

  • 💡《正当性》と《簡潔さ》を崩してまで使いやすいコードにするより、必要に応じてラッパーコードを書く

安易な指標

よくある、長い関数はダメだとか、多い引数はダメだとかいった指標がありますが、その種の指標に守破離的に一時的に従う事までは否定しませんが、基本的にそれらの指標に従う必要はありません。

《正当性》と《簡潔さ》を優先したコードは、結果的にそれらの指標も満たすでしょうし、逆に言うと、元よりそれらの指標はその様に結果的に満たされるべき指標でしかなく、直接的にそれらの指標を満たす事を目指すものでもなければ、直接的に目指す意味もありません。

《正当性》と《簡潔さ》を優先したコードがそれらの指標を満たさない事があったとしても、それらの指標を満たす様に修正する必要はありません。

👉 それらの指標に重きを置き、従うにしても、それでもやはり直接的に従うべき指標ではないので、安易に盲目的に直接修正するのではなく、なぜ、その指標を満たさないコードになってしまったのか? 本質的・根本的な問題がなにかを見定め、その問題を対処する事によって、あくまでも結果的に満たす様にしましょう。

《メタな特殊化》と《メタな共通化》

《正当性》は「コードの理論的な正しさ」であり、それを突き詰めた極論としては言わば「コードの適切で《メタな特殊化》」です。(《正当性》とイコールではない点に注意)

《簡潔さ》は「コードの意味的な簡潔さ」であり、それを突き詰めた極論としては言わば「コードの適切で《メタな共通化》」です。(《簡潔さ》とイコールではない点に注意)

《メタな特殊化》と《メタな共通化》は次の様な共通化と特殊化の総称です。

  • ☯️ 異なる分類に抜き出す、同じ分類にまとめる。
  • ☯️ 異なる命名をする、同じ命名をする。
  • ☯️ 関数の引数や戻り値の型を変える、同じにする。
  • ☯️ 異なる処理を別の関数に抜き出す、同じ処理を1つの関数のまとめる。
  • ☯️ 処理構造を変える、同じにする。
  • ☯️ 分岐を増やす、減らす。
  • ☯️ アクセスするデータを増やす、減らす。
  • ☯️ データ構造を変える、同じにする。
  • ☯️ データ項目を増やす、減らす。
  • ☯️ データ構造と処理構造を合わせない、合わせる。

コードは《メタな共通化》が完全に行われきっている《無》の状態から始まり、《要求》を満たす為に《メタな特殊化》をして行く事になります。ひたすら《メタな特殊化》だけが行われるとコードは容易く《混沌》状態なります。

《混沌》状態のコードとは、簡単に言えば《クソコード》です。

ここで重要なのは《メタな共通化》を伴っていないだけで、その他のあらゆる点で理想的なコードになっていようと、《メタな共通化》を伴っていないと言う1点だけで、《クソコード》になる事が不可避である点です。

コードが《混沌》状態になる事を防ぐには《メタな特殊化》と併せて可能な限り《メタな共通化》を進めていく必要があります。しかし、いくら《メタな共通化》を頑張っても、《メタな共通化》より《メタな特殊化》が勝ってないと《要求》を満たす事ができない以上、次々と《要求》を満たして行けば、必然的にコードは《混沌》状態になって行く事は理屈的・本質的に避けられません。

可能なのは、対応する《要求》を絞って《メタな特殊化》を減らし、ひたすら《メタな共通化》を進めて、泥臭く足掻き続ける事だけです。

  • 💡 コードが《混沌》状態(クソコード)になる事を防ぐに《メタな特殊化》と併せて可能な限り《メタな共通化》を進めて泥臭く足掻き続ける

👉《メタな共通化》をひたすら頑張れば《クソコード》になることを回避できると言うモノでもなく、《メタな特殊化》と《メタな共通化》が共に「適切に」、つまり「上手に」行われる必要があり、それには日々の研鑽による熟練が必要となるでしょう。

《無駄》や《不完全》を受け入れる勇気を!

コード上の《無駄》

《正当性》や《簡潔さ》を優先したコードは、無駄も多く処理パフォーマンスの悪い冗長なコードになりがちです。しかし、その無駄を受け入れる事こそが肝です。その《無駄》を嫌って《正当性》や《簡潔さ》を台無しにしてはなりません。

ボルトとナットにしろ、蝶番にしろ、寸法に若干《遊び》を持たせないとオス・メスが噛み合わなかったり、噛み合わせる事ができても、まともに回転させる事ができず用を成しません。自動車のハンドルにしたって《遊び》が無ければ手ブレがセンシティブに反映されて蛇行する羽目になります。キーボードのキーに《遊び》が無ければキートップに軽く触れただけでキー入力判定されて誤タイプだらけになってしまいます。タブレット機のベゼルにしたっていまの技術があれば容易くもっと狭くできるにも関わらず1cm近いベゼル幅があるのも、そうしないとタブレットを持つ手がタッチパネルに触れてしまい誤タップを量産する羽目になってしまいます。

コードも同じです。《正当性》や《簡潔さ》を優先したコードに存在する《無駄》は《必要な遊び》です。

《無駄》を削ぎ落としたコードは、何かと融通が悪くなり《要求》の変化に対して弱いコードとなり、《正当性》を損なった分だけバグを誘発しやすいコードになり、《簡潔さ》を損なった分だけ《要求》されてる処理内容の割には複雑なコードになります。

  • 💡《無駄》を嫌って《正当性》や《簡潔さ》を台無しにしない
  • 💡《正当性》や《簡潔さ》を優先したコードに存在する《無駄》は《必要な遊び》

《不完全》なコード

処理内容に対する理解が十分でない、あるいは適した処理構造の発想に欠けるなどして、十分に《正当性》や《簡潔さ》を優先したコードになかなか上手いこと仕上げられない事もあるでしょう。

字を綺麗に書けない人が、綺麗に書けないからといって汚い字を書き殴るのと、汚いなりに《丁寧に書いた》字では雲泥の差があり、多くの場面で《丁寧に書いた》字で十分である様に、《正当性》や《簡潔さ》を優先して《丁寧に書いた》コードは多くの場面でそれ十分です。

もちろん、上手く《正当性》や《簡潔さ》を優先したコードに仕上げられてる事が望ましいですが、なかなか上手く書けない場面とは、上手く書く為の《情報》《理解》《発想》などが欠けてる状態なので、その場でそのままどうにか上手く書けないかと足掻いてもただただ時間を浪費する羽目になりがちで、コストかけ過ぎるのはそれはそれで良くないですし、そもそもコストをかけて十分に《正当性》や《簡潔さ》を優先したコードに仕上げたところで《要求》の変化により、それは最悪の場合、丸々不要なコードになります。《不完全》なコードを受け入れましょう。

👉 後日、欠けていた《情報》《理解》《発想》が揃うと、割と簡単に上手く《正当性》や《簡潔さ》を優先したコードに仕上げる事ができたりするものなので、必要に応じて、その時に書き直しましょう。そして、「その時」に書き直しができるだけの時間的な余裕を残すべく、早めに《見切り》、一旦、《不完全》なコードを受け入れましょう。

  • 💡 十分に《正当性》や《簡潔さ》を優先したコードになかなか仕上げられない場合でも、《正当性》や《簡潔さ》を優先して《丁寧に書いた》コードで十分

まとめ

結局のところ、一言で言えば、なんの事はない「対応する《要求》と《ハック》の使用を必要最小限に控えつつ《丁寧なコード》を書きましょう」って話でしかないんですが、王道、正道なんて、そんなもんです。ただ、それでも言語化もされず解像度の低い認識で臨むのと、詳細に言語化してより解像度の高い認識で臨むのでは雲泥の差があります。

後日、自ら覆す事もあるかも知れませんが、この記事の内容は現時点での私の《結論》です。多くの事を疑ってかかる事が望ましい《研究》・《学習》・《練習》などの場面で《結論》を持つ事は《思考停止》を招くデメリットともなり得ますが、惑う事なく強い自信を持って事に臨む事が望ましい《実用》・《実践》・《本番》の場面では、より多く、より強固な《結論》とその《結論》が間違っていた際に負う損失・損害の《覚悟》を持ち合わせる事でこそ、地に足の付いたパフォーマンスを発揮する事ができます。

👉 《結論》だけだと《迷い》が生まれ、迷ってる様では《結論》を持っている事の効果が薄れる為、《結論》と合わせて《覚悟》が必要になります。

この記事の内容に賛同できずとも、あなたのより良いコードに対する発想の足掛かりになったり、より良いコードに対する認識の言語化や解像度の向上、あなたなりの《結論》に辿り着く為の、一助になれば幸いです。

  • 👑 対応する《要求》と《ハック》の使用を必要最小限に控えつつ《丁寧なコード》を書きましょう

関連記事

この記事の転載元URL: https://wraith13.github.io/writing/?programming%2Fbest.code.md

145
180
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
145
180

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?