シンプルコード は何をしますか?
同じ出力を生成する2つのプログラムは等価ではありません。人がより速く理解できる方が優れています。すべてのクラス、ヘルパー、パラメータ、レイヤーは、次の読み手が学ばなければならないものです。そのため、それぞれがプログラムの動作を変更することによって、その存在価値を示さなければなりません。
目標は、新しいチームメンバーが一度上から下まで読めば理解でき、説明でき、安全に変更できるコードです。
作業手順
- ジョブを明確にする。 何が入力で、何が出力か、どのルールとエッジケースが要求されたか、そしてユーザーが指定した品質目標やデザインをすべてリストアップします。これらはすべて要件です。このリストに基づいて作業し、コードがいつか必要になるかもしれないという可能性に基づいて作業してはいけません。
- まず退屈なバージョンを書く。 プレーンな関数、
if文、ループ、リテラルデータを、人間が手順を説明する順序で記述します。これが、明示された要件を満たさない限り維持するデザインです。 - 削除テストを実行する。 各クラス、ヘルパー、パラメータ、定数、インポート、分岐、
tryについて、頭の中でそれを削除してみます。品質目標を含むすべての明示された要件が依然として満たされるなら、実際に削除します。 - テストして、テストを残す。 ユーザーがそうしないと言わない限り、明示されたすべてのルールとエッジケースを、プロジェクトの慣習に従った小さなプレーンなテストファイルでカバーします。テストは次の読み手にルールを示し、変更を安全にするため、ジョブの一部であり、追加作業ではありません。
- 平易に報告する。 何を構築したかを述べます。意図的に何かを省略した場合は、それを一行で名前を挙げます(「CLIやロギングは要求されていないため、含めていません」など)。そうすれば、ユーザーはそれを要求できます。
シンプルさの定義
今日の要件のために構築する。 「後で追加するかもしれない」という漠然とした考えは、今すぐ構築する機能ではありません。シンプルなコードは拡張が最も簡単です。なぜなら、回避すべきものが何もないからです。クーポンを追加するということは、まずフレームワークを学習することではなく、一行を追加することを意味すべきです。
最もシンプルな方法で、明示された品質目標を満たす。 リクエストに「プロダクション対応」「堅牢」「スケーラブル」「エンタープライズ級」とある場合、このコードの使用で実際に何が問題になり得るかを問い、それらの問題に直接対処します。不正な入力は拒否され、障害は可視化され、何も静かに失われず、金額は正確に計算されます。これらの言葉は、そのような注意を要求しています。それらは、レイヤー、プラグインシステム、フレームワークを要求しているわけではありません。
ユーザーが要求したデザインを提供する。 アーキテクチャや特定の構造が要求された場合、それを最小限の有用な形で提供し、「それは後回しにすべきだ」と返答してはいけません。例えば、さらにフォームが追加される予定なら、デザインは、次のフォームが再利用する共有チェック関数と、フォームを追加する方法についての一行の説明かもしれません。想像上のケースだけが必要とするエンジンを作り込むのは避けます。
機能する最もプレーンな構造を使用する。 おおよそ以下の順序で選択します。
- 値またはリテラルdict(設定システムの代わりに)
if/elifまたはルックアップdict(レジストリ、ストラテジ、ファクトリ、プラグインシステムの代わりに)- 関数(クラスやメソッドを持つ構造体の代わりに。複数の操作が共有する状態を保持する場合のみクラス/構造体を追加する)
- 具象型(インターフェースやジェネリクスの代わりに。今日、実際に2つの実装または型が必要な場合のみそれらを追加する)
- 逐次コード(goroutineやチャネルの代わりに。並行性が要件である場合を除く)
- 標準ライブラリとプロジェクトに既にあるヘルパー(新しい依存関係の代わりに)
フローを真っ直ぐに保つ。 ネストする代わりに早期リターンを使用します。深さが約3レベルを超える場合は、通常、フローを平坦化できます。一行に一つのことを行います。ネストされた内包表記、連鎖三項演算子、reduce、巧妙な正規表現よりも、読みやすいループを優先します。メタプログラミング、リフレクション、カスタムデコレータは、タスクがそれらに特化している場合を除き避けます。
小さな繰り返しを許容する。 3行の類似コードは、フラグ付きのヘルパーよりも明確であることがよくあります。実際のアイデアに適切な名前を付けたり、ロジックを同期させる必要がある場合に関数を抽出します。単に2行が似ているからという理由だけで抽出してはいけません。1つの呼び出し元しか使用しないパラメータや、誰も要求していないパラメータを追加してはいけません。
人間のために命名する。 完全な普通の単語を使用します(sf や FeeCalculationContext ではなく shipping_fee)。良い名前はコメントの必要性を排除します。
理由のみをコメントする。 驚くべきルールや制約について記述します。コードを繰り返すコメント、セクションバナー、シグネチャを言い換えた長いdocstringはスキップします。
発生しうる障害を処理し、大きく失敗する。 要件または実際の境界(ユーザー入力、ネットワーク、ファイル)が要求するものを検証し、不正な入力には明確なエラーを返し、もっともらしい間違った答えを生成しないようにします。コードがデータをスキップまたはドロップする場合、それを実行者に可視化します(例:ログ行にカウントを含める)。例外をキャッチして再スローするだけのことはせず、コードが到達できない状態に対してガードしてはいけません。
外部からの数値を算術演算の前にチェックする。 リクエストからの数量、金額、カウントは巨大になる可能性があり、それらを加算または乗算すると、後続のすべてのチェックを通過する小さな値や負の値にオーバーフローする可能性があります。何かに追加する前に、受信値をその制限(在庫、最大値)と比較し、その型が保持できる最大値を送信するテストを追加します。これは1つのifであり、追加のコードではありません。
マルチステップ変更の途中で再起動が何を行うかを決定する。 コードが保存された状態を変更し、支払いプロバイダーへの呼び出しなどの遅いまたは外部の何かを呼び出し、その後再び状態を変更する場合、プロセスがこれらのステップの間で停止したらどうなるかを問います。呼び出しの前に、明確な中間状態(pending など)を保存します。再起動時には推測してはいけません。成功した可能性のあるステップを元に戻さない(例:課金が行われた可能性のある charge の在庫を戻さない)、そして再試行が二重課金する可能性があるように作業を再開してはいけません。レコードを中間状態のままにし、ブロックされたままにし、可視化します(例:起動時に保留中のレコード数をログに記録する)。そうすれば、人間が解決できます。このルールをデザインの1〜2行に記述し、保留中のレコードで再起動することでテストします。
余計なものを追加しない。 CLI、ロギング、設定、リトライ、キャッシング、メトリクス、型階層は、それらが要求された場合、またはジョブや明示された目標がそれなしでは達成できない場合を除き、追加してはいけません。例えば、無人で実行されるコードは、問題を報告する何らかの方法を必要とします。
既存コードを変更する場合
プロジェクトの慣習に従い、ジョブを実行するための最小の変更を行います。ついでに隣接するコードを再構築したり、新しいパターンを導入したりしてはいけません。複雑なコードが機能している場合は、それを維持します。ただし、簡略化がタスクであるか、変更に必要な場合を除きます。削除可能と思われる場合は、代わりにそのことを言及します。
シンプルさが意味しないもの
以下のいずれかを犠牲にして行数を減らしてはいけません。
- 正しい動作(明示されたすべてのルールとエッジケースを含む)
- セキュリティ:信頼境界での入力検証、安全なクエリ、コード内のシークレットなし
- 重要な精度:金額は正確に計算されなければならず(小数点以下2桁の端数処理を含む)、テストでそれを証明すべきです。これを行う最もプレーンな方法を使用します。価格が小数点以下数桁で、float がとにかく関数境界を越える場合、バイナリノイズを除去する小さな丸めヘルパーを持つ float で十分です。整数セントや
big.Ratなどの正確な分数は、明示されたルールで必要とされる場合、またはより単純な方法が誤った結果をもたらすことをテストが示した場合にのみ使用します。 - タスクが実際に要求した、または測定が重要であることを示したパフォーマンス
- ユーザーが明示的に要求した抽象化。それを最もシンプルな形で構築します
行数が少ないことも目標ではありません。コードゴルフは過剰工学と同じくらい読みにくいので、より明確な場合は長いバージョンを優先します。すべてを行う1つの長い関数はシンプルではありません。関数が読み取り、解析、要約などの異なるステップを混在させる場合、各ステップに適切に名前付けられた関数を与えます。問題に真の複雑さがある場合、それを維持し、適切に名前付けられた1つの場所に隔離し、なぜそうするのかを短いコメントで説明します。
例
タスク:会員割引を適用し、50未満の注文には送料5を課金する。
これは避ける。同じ出力を生成するが、読むべきものがはるかに多い:
type DiscountStrategy interface {
Apply(amount float64) float64
}
type MemberDiscount struct{}
func (MemberDiscount) Apply(amount float64) float64 {
return amount * 0.95
}
type PriceCalculator struct {
Strategy DiscountStrategy
ShippingThreshold float64
ShippingFee float64
}
func (c PriceCalculator) Calculate(amount float64) float64 {
discounted := c.Strategy.Apply(amount)
if discounted < c.ShippingThreshold {
return discounted + c.ShippingFee
}
return discounted
}
これを書く:
func FinalPrice(amount float64, isMember bool) float64 {
if isMember {
amount = amount * 0.95
}
if amount < 50 {
amount = amount + 5
}
return amount
}
後でVIP割引を追加するには、もう1つ if を追加するだけで済み、インターフェースと3つの型を学習する必要はありません。