K8sに学ぶコードの可読性

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/e75a7311-2e79-458c-9ea6-b9fc6081fbe3.png)](https://leapcell.io/?lc_t=q_gok8scoding)

Kubernetes のコードには、さまざまなコードカプセル化の理論やテクニックが実践されています。そのため、コードを読む過程で直感的な方法で知りたい内容を推論でき、またコードの意図もすぐに理解できます。

Kubernetes のソースコードには、優れたコメントや変数名もあり、開発者がコード設計の意図をよりよく理解する助けになっています。では、Kubernetes のソースコードのコメントや変数名には、私たちが学ぶべき点はどのようなものがあるのでしょうか？

## 変数編

### 変数名は詳細であればあるほど良いというわけではない

変数名で具体的な意味を正確に表現しようとすると、必然的に長くなってしまう問題に直面します。しかし、非常に長い変数名が何度もコード中に現れると、それは「ベロジョルスキー」や「トヴェルスキー」のように多くの「運転手」がいるようなもので、見ていて非常に頭が痛くなります。

このような過度に正確な繰り返し命名による混乱を避けるために、私たちは文脈の意味によって、短くて精悍な変数名により多くの意味を持たせることができます。

```go
func (q *graceTerminateRSList) remove(rs *listItem) bool{
    //...
}
```

Kubernetes の`graceTerminateRSList`構造体の定義では、`graceTerminateRealServerList`と書く必要はありません。対応する文脈で`listItem`内部ですでにフルネームの定義が使われているからです。

```go
type listItem struct {
  VirtualServer *utilipvs.VirtualServer
  RealServer    *utilipvs.RealServer
}
```

したがって、この文脈では、`rs`は`realServer`を指すだけであり、`replicaSet`や他のものを指すことはありません。もしそのような曖昧さが発生する可能性があれば、このような省略はできません。

また、`graceTerminateRSList`の`rs`を削除するメソッドでは、`removeRS`や`removeRealServer`と書く必要はありません。引数のシグネチャにすでに`rs *listItem`があり、このメソッドで削除されるのは`rs`だけだからです。メソッド名に`rs`を付けるとかえって冗長になります。

命名時には、できるだけ短い名前に多くの意味を持たせるようにしましょう。

```go
func CountNumber(nums []int, n int) (count int) {
  for i := 0; i < len(nums); i++ {
    // もし値を代入するなら v := nums[i]
    if nums[i] == n {
      count++
    }
  }
  return
}

func CountNumberBad(nums []int, n int) (count int) {
  for index := 0; index < len(nums); index++ {
    value := nums[index]
    if value == n {
      count++
    }
  }
  return
}
```

`index`は`i`より多くの情報を持っているわけではなく、`value`も`v`より良いとは言えません。この例では、省略形を使うことができます。しかし、省略形にも必ずしも利点があるわけではなく、具体的な状況で曖昧さが生じるかどうかを判断する必要があります。

### 変数名は誤解を避ける必要がある

イベントに参加するユーザー数（int 型）を表現したい場合、`userCount`のほうが`user`や`users`よりも良いです。なぜなら、`user`はユーザー情報のオブジェクトを示す場合があり、`users`はユーザー情報のスライスを示す可能性があるため、これらを使うと誤解を招きます。

もう一つ例を見てみましょう。`min`は場合によって最小値（minimum）を示すこともあれば、分（minutes）を示すこともあります。混同しやすい場面では、省略せずにフルスペルを使いましょう。

```go
// 最低価格とプロモーションイベントの残り時間を計算する
func main() {
  // 商品価格リスト
  prices := []float64{12.99, 9.99, 15.99, 8.49}

  // 各商品のプロモーション残り時間（分）
  remainingMinutes := []int{30, 45, 10, 20}

  // min := findMinPrice(prices) // 変数名 "min"：最低価格を示す
  minPrice := findMinPrice(prices)
  fmt.Printf("商品の最低価格: $%.2f\n", min)

  // min = findMinTime(remainingMinutes) // 変数名 "min"：最短残り時間を示す
  remainingMinute := findMinTime(remainingMinutes)
  fmt.Printf("プロモーションイベントの最短残り時間: %d minutes\n", min)
}
```

この例では、`min`は商品の最低価格も示せますし、イベント残りの最小分数も示せます。このような場合、省略形は使わないようにします。これにより、最低価格を見つけたのか、最小分数を見つけたのかを明確に区別できます。

### 同じ意味の変数は一貫性を保つ

プロジェクト全体で同じ意味を持つ変数名は、できる限り一貫性を保つべきです。例えば、プロジェクト内でユーザー ID を`UserId`と書いているのであれば、他の場所でコピーするときも`Uid`などと変えないようにします。`UserId`と`Uid`が同じものなのかどうか、疑問に思ってしまうからです。

このような状況を軽視しないでください。複数のシステムでユーザー ID が存在している場合、すべて保存する必要が出てきます。もしプレフィックスを付けて区別しなければ、使いたいときにどれがどれなのか分からなくなってしまいます。

例えば、ユーザー A がバイヤーとして売り手の商品を購入し、配達員が配送する場合を考えてみましょう。

ここには 3 つのユーザー ID が現れます：バイヤー、セラー、配達員。

このようなときは、モジュールのプレフィックスを付けて区別することができます：`BuyerId`、`SellerId`、`DriverId`。

この時も、省略形にしない方がいいです。これらは既に十分短いので、もし関数の引数の`SellerId`を`Sid`と省略すると、後々`ShopId`（店舗 ID）という概念が出てきたとき、`Sid`が`SellerId`なのか`ShopId`なのか疑問になります。もし個人セラーが`SellerId`で`ShopId`を埋めていた場合、オンライン上でバグが発生してしまいます。

## コメント編

### コメントはコードだけでは表せない情報を説明する

関数の内部ロジックが複雑すぎる場合、コメントを使うことで読む人が関数の細部まで追わなくて済み、コードの意図を短時間で把握できます。これはコードリーディングの助けになります。

Kubernetes の Pod の同期ループは非常に複雑なので、関数のコメントで処理の説明を行っています。

```go
// syncLoopIteration reads from various channels and dispatches pods to the
// given handler.
//
// ......
//
// With that in mind, in truly no particular order, the different channels
// are handled as follows:
//
//   - configCh: dispatch the pods for the config change to the appropriate
//     handler callback for the event type
//   - plegCh: update the runtime cache; sync pod
//   - syncCh: sync all pods waiting for sync
//   - housekeepingCh: trigger cleanup of pods
//   - health manager: sync pods that have failed or in which one or more
//     containers have failed health checks
func (kl *Kubelet) syncLoopIteration(ctx context.Context, configCh <-chan kubetypes.PodUpdate, handler SyncHandler,
  syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool {
}
```

コメントの冒頭で、様々なチャネルから受け取った Pod 情報をそれぞれの処理ロジックに渡すことを説明しています。また、各チャネルがどんな処理を行うかの概要もまとめられています。

もしコードがそれほど複雑でなく、意図もコードから分かるのであれば、コメントは不要です。

以下の例では、ユーザーがサインインした時、通常の VIP であれば基本の 10 ポイントを加算、VIP ユーザーであればさらに 100 ポイントを加算します。

```go
const (
    basePoints  = 10
    vipBonus    = 100
)

type User struct {
    IsVIP    bool
    Points   int
}

// SignIn 处理用户签到并根据是否是 VIP 增加积分
func (u *User) SignIn() {
    pointsToAdd := basePoints
    if u.IsVIP {
        pointsToAdd += vipBonus
    }
    u.Points += pointsToAdd
}
```

このコードから関数の意図が十分に読み取れるので、コメントは特に必要ありません。

また、業務がまだ安定していない場合は、特に重要であったり誤解が生じそうな箇所だけコメントをつけるのが良いです。頻繁にビジネスロジックが変更される中で、内部のロジックが修正されたのにコメントが更新されていなければ、読む人を混乱させてしまいます。

しかし、コードロジックがあまりにも複雑な場合は、ガイド的なコメントの代わりに、コードを階層化・分割することで解決できます。
Kubelet が設定シグナル（`configCh`）を処理する例を見てみましょう。

```go
func (kl *Kubelet) syncLoopIteration(...) bool {
  select {
  case u, open := <-configCh:
    switch u.Op {
    case kubetypes.ADD:
      klog.V(2).InfoS("SyncLoop ADD", "source", u.Source, "pods", klog.KObjSlice(u.Pods))
      handler.HandlePodAdditions(u.Pods)
    case kubetypes.UPDATE:
      klog.V(2).InfoS("SyncLoop UPDATE", "source", u.Source, "pods", klog.KObjSlice(u.Pods))
      handler.HandlePodUpdates(u.Pods)
    case kubetypes.REMOVE:
      klog.V(2).InfoS("SyncLoop REMOVE", "source", u.Source, "pods", klog.KObjSlice(u.Pods))
      handler.HandlePodRemoves(u.Pods)
    case kubetypes.RECONCILE:
      klog.V(4).InfoS("SyncLoop RECONCILE", "source", u.Source, "pods", klog.KObjSlice(u.Pods))
      handler.HandlePodReconcile(u.Pods)
    case kubetypes.DELETE:
      klog.V(2).InfoS("SyncLoop DELETE", "source", u.Source, "pods", klog.KObjSlice(u.Pods))
      handler.HandlePodUpdates(u.Pods)
    case kubetypes.SET:
      // TODO: Do we want to support this?
      klog.ErrorS(nil, "Kubelet does not support snapshot update")
    default:
      klog.ErrorS(nil, "Invalid operation type received", "operation", u.Op)
    }
  }
}
```

このように、各イベントごとの処理を個別のメソッドに分けることで、switch 文の分岐にすべてロジックを書き並べることを避けられます。

例えば、`kubetypes.ADD`に対応する処理を`HandlePodAdditions`メソッドにカプセル化することで、全体のコードが「目次」のように分かりやすくなります。

Pod 追加処理の流れを知りたければ、直接`HandlePodAdditions`メソッドを見ればいい、という形です。

次に、Kubernetes の`BoundedFrequencyRunner`の`Run`メソッドのコメント例を見てみましょう。

```go
// Run the function as soon as possible.  If this is called while Loop is not
// running, the call may be deferred indefinitely.
// If there is already a queued request to call the underlying function, it
// may be dropped - it is just guaranteed that we will try calling the
// underlying function as soon as possible starting from now.
func (bfr *BoundedFrequencyRunner) Run() {
  select {
  case bfr.run <- struct{}{}:
  default:
  }
}
```

ここでは、コメントを通じて関数自体のコードからは分からないポイントが伝えられています。

- ループ（`Loop`）が実行されていなければ、実行シグナルは無期限に遅延される可能性がある（消費者がいないため）
- 既に他の場所で`Run`が呼ばれていた場合、シグナルが失われる可能性がある（今すぐタスクを実行しようとするが、必ずしも実行されるとは限らない）

これら 2 点は関数本体だけを見ても読み取れない内容です。著者はこうした隠れた情報をコメントで伝えることで、利用者がこのメソッドをどう扱うべきかをすばやく理解できるようにしています。

正規表現のコンパイル例も見てみましょう。

```go
func ExecRegex(value string, regex string) bool {
  regex, err := decodeUnicode(regex)
  if err != nil {
    return false
  }
  if regex == "" {
    return true
  }

  rx := regexp.MustCompile(regex)
  return rx.MatchString(value)
}
```

ここで、渡された正規表現が`decodeUnicode`で一度処理されています。では、その具体的な処理内容を見てみましょう。

```go
func decodeUnicode(inputString string) (string, error) {
  re := regexp.MustCompile(`\\u[0-9a-fA-F]{4}`)
  matches := re.FindAllString(inputString, -1)

  for _, match := range matches {
    unquoted, err := strconv.Unquote(`"` + match + `"`)
    if err != nil {
      return "", err
    }
    inputString = strings.Replace(inputString, match, unquoted, -1)
  }

  return inputString, nil
}
```

この関数単体では「入力された式をエスケープしている」ことしか分かりません。なぜエスケープが必要なのか、エスケープしないとどうなるのかは分かりません。
この関数に該当するコメントを追加してみましょう。

```go
// decodeUnicodeは正規表現文字列をエスケープし、[\\u4e00-\\u9fa5]のようなCJK判別用の正規表現を渡した際のpanicを回避する。
func decodeUnicode(inputString string) (string, error) {
  //...
}
```

この一行コメントで、Go で CJK 正規表現をパースする際にエスケープしないと panic になることが明示され、関数の意図もすぐに分かります。
さらに、後からこの部分を触る人も、エスケープ後の文字列を安易に扱って新たなバグを生むリスクを減らせます。

コードの中で、変数名やコメントは自然言語に最も近い内容なので、最も理解しやすい部分です。この部分をしっかり推敲して書けば、可読性は一気に向上します。

空行も、実は一種のコメントであり、コードの論理的な区切りとなります。これは、読む人に「ここで一つのロジックが終わりました」と伝える役割があります。

上記の`decodeUnicode`メソッドでも、事前処理用の正規表現のマッチ、具体的な処理ループ、最終的な返却文を空行で三つに分けており、コードを視覚的にも分かりやすくしています。

Kubernetes の`graceTerminateRSList`で、RS が存在するかどうかを判定する例を見てみましょう。

```go
func (q *graceTerminateRSList) exist(uniqueRS string) (*listItem, bool) {
  q.lock.Lock()
  defer q.lock.Unlock()

  if rs, ok := q.list[uniqueRS]; ok {
    return rs, true
  }
  return nil, false
}
```

ここではロックとアンロックの間に空行を入れて、ロック操作が一段落し、その後は存在チェックのロジックが始まることが一目で分かるようになっています。読む人が注目すべきは下半分のロジック判定だとすぐ把握でき、重要ポイントを素早く掴めます。

```go
func (q *graceTerminateRSList) exist(uniqueRS string) (*listItem, bool) {
  q.lock.Lock()
  defer q.lock.Unlock()
  if rs, ok := q.list[uniqueRS]; ok {
    return rs, true
  }
  return nil, false
}
```

もし空行を省略した場合、コードを読んでもこのメソッドの重点が分かりにくくなります。

### 使わなくなったコードはコメントアウトせず、直接削除する

多くの場合、将来また使うかもしれないからと、コードをコメントアウトして残しておくことがあります。

しかし、もう一つのパターンとして、将来使うために一旦コメントアウトしていたものが、実際に再利用しようとした際、既存のコードと互換性がなくバグを生み、結局書き直す羽目になることもあります。

それなら最初からコードを削除しておいた方がよいでしょう。もし将来必要になれば、git のコミット履歴から以前のコードを探して復元すればよいのです。その際、テストもやり直せるため、古いコードに振り回されずに最適な形にできます。

この原則は、Kubernetes の yaml ファイルを書く時にも当てはまります。

```yaml
spec:
  spec:
    # ...
  # server-confというconfigMapボリュームをマウント
  #  - name: server-conf-map
  #    configMap:
  #      name: server-conf-map
  #      items:
  #        - key: k8s-conf.yml
  #          path: k8s-conf.yml
  #      defaultMode: 511
```

このように、yaml 定義を見るときに大量の使わないコメントで中断されると分かりにくくなります。もし`server-conf-map`がすでに削除されているなら、なおさら混乱を招きます。
ですから、自分のプロジェクト内で外部依存のないコードなら、直接削除し、必要になったら git で復元しましょう。

プロジェクト内に外部の第三者が依存しているコードがある場合は、「非推奨（Deprecated）」コメントを追加する方が、削除するより良い選択肢となります。他者向けにパッケージを提供している場合、コードを直接消してしまうと利用者が新しいバージョンにアップグレードした際、多くのエラーが発生します。
この時は、新しい使い方に誘導する必要があります。

たとえば、gRPC の`WithInsecure`メソッドの非推奨コメントを見てみましょう。

```go
// Deprecated: use WithTransportCredentials and insecure.NewCredentials()
// instead. Will be supported throughout 1.x.
func WithInsecure() DialOption {
  return newFuncDialOption(func(o *dialOptions) {
    o.copts.TransportCredentials = insecure.NewCredentials()
  })
}
```

このコメントでは、何を代替として使えば良いのか、引数は何を渡すのかまで明記されています。

こうすることで、新しいメソッドへの置き換えもスムーズになり、利用者が新機能を簡単に使えるように導けます。

## 最後に

この記事で学んだ内容を振り返ってみましょう。

- 文脈に応じて適切に変数名や関数名を省略し、合理的に命名する方法
- コメントはコードから読み取れない内容を表現するために使う
- 適切なガイドコメントを使うことで、後からプロジェクトに参加する人が読みやすくなるが、さらに良い方法はメソッド抽出などによる「自己説明的な」コードにすること
- 削除できるコードはコメントアウトせず削除し、削除できない（第三者が使っている等）場合は、分かりやすい「非推奨（Deprecated）」コメントで新しい方法への移行を促すこと

Kubernetes のような大規模なオープンソースプロジェクトのコードには、設計や可読性のためのノウハウが数多く詰まっています。
命名規則やコメント運用、コードの抽象化手法をしっかり学ぶことで、個人の開発・チーム開発問わず、より読みやすく・バグの少ない実装につなげることができます。

可読性を意識したコードは、他の開発者だけでなく将来の自分自身にとっても大きなメリットとなります。
日々の開発の中でも、少し立ち止まって「名前やコメントはこれでよいか？」と考えてみましょう。それだけでも、プロジェクトの品質やチーム全体の生産性が大きく向上するはずです。

---

### [私たちはLeapcell、Goプロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_gok8scoding)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/38bdf3de-1df0-4782-9f12-10c7e1aada31.png)](https://leapcell.io/?lc_t=q_gok8scoding)

[Leapcell](https://leapcell.io/?lc_t=q_gok8scoding)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/480bfa3e-2600-4da8-adf1-61c693954c81.png)](https://leapcell.io/?lc_t=q_gok8scoding)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/learning-code-readability-from-kubernetes)