メインコンテンツまでスキップ

Software Design 202508

リファクタリング

リファクタリングは議論になりがち

リファクタリングとは、外部からみた振る舞いを保ちつつ、コードをきれいにすること。

小さくて安全な改修行為を継続的に積み重ねることである。 テストがあることは必須条件である。

先送りにし続けてきた問題が顕在化してから、大規模な改修をまとめて実施したあげく、事故りました。 みたいなのはリファクタリングとは呼ばないので、誤用しないこと。

リファクタリングのサイズによって、取るべき手法や期間が変わる。

  • Small: 単一の関数
  • Medium: 複数の関数を組み合わせて何らかの機能を提供するもの(データ永続化、税額計算、API通信、など)
  • Large: ユースケースの単位

ソフトウェア開発は複雑さと不確実性に立ち向かうものであり、継続的なリファクタリングは必要不可欠である。 しかし、リファクタリングの成果は目に見えづらく、ビジネスと技術で情報が非対称なこともあって議論になりがち。 お互いにビジネスの成功という共通目的を意識しつつ、共通のメンタルモデルを作って建設的な議論をすることが大事。

リファクタリングの実施判断力

リファクタリングの実施対象は優先順位づけが必要。

  • 優先度高: 修正予定のあるモジュールや、頻繁に触るモジュール
  • 優先度低: 修正予定のない安定したブラックボックス。せいぜいテスト追加に留める。

小さいリファクタリングを息をするごとくこまめに行うのが大事。 小ささの判断基準は、種類、時間、範囲の3つ。

  • 関数の抽出やインライン化、不要コードの削除など、軽微な種類の作業
  • 数分から一時間など、短い時間で終わる
  • 影響範囲が自分だけ

全部直したくなる誘惑に打ち勝ち、適切なタイミングで切り上げる勇気も大事。

リファクタリングのアンチパターン

  • 前提がズレている
    • あるべき姿や目的の認識がチーム内でズレているパターン。まずは目線を揃えよう。
    • 理想的なコードとは、変更容易性の高いコードである。
    • 例えばカプセル化され、関心が分離されていることなど。
  • 無意味にデザインパターンを適用する
    • ハンマーを持ったからって釘を探し回るな
  • 自分の知っているパターンだけで実装する
    • たとえば巨大なサービスパターン(クラス)は、ユースケースパターン(クラス)により分割・改善できることをご存知ですか
  • コードの間違った共通化
    • Linterの指摘を鵜呑みにしない
  • 名前の改善を躊躇する
    • なるべくユビキタス言語で名前をつける
    • 文脈を踏まえて名前付けしよう。そうしないと意図を読み取れなかったり、関心がコード内に混在することになりがち。
  • 複雑すぎて半端で終わる
    • イベントストーミングなどを通じて設計からやり直せ
  • 振る舞いを変える
    • バグ修正、パフォーマンス改善、機能変更を同時にやるな
  • 歴史的経緯の無視
    • トレードオフで仕方なくそうなってるかもしれないよ
  • テストがないからリファクタを諦める
    • AIにテストを書かせろ(本文にはプロンプト紹介あり)
  • リファクタリングを禁じる、後回しにする、しなくていいところにやる、やりすぎる
    • 常日頃から、優先度の高い箇所に、適切なコストで実施し続けることが大事
  • ビッグリファクタリングをする
    • 巨大なコードに対峙すると分析麻痺症候群に陥ってしまう
    • まずは優先度の高い箇所で小さく始めてみる

プロダクトマネージャー視点でのリファクタリング

PdMがリファクタリングを考えるときは価値の視点が最も大事である。

  • 価値
    • 外部: ユーザー体験向上、プロダクト信頼性向上
    • 内部: コードの理解や変更のしやすさ
  • リスク
    • 外部: バグや障害によるチャーン誘発、信頼関係の喪失
    • 内部: 開発スピード低下、属人化、モチベーション低下
  • 速度
    • 外部: 価値の素早い提供
    • 内部: 機能追加のしやすさ、開発効率やスループットの向上

上記に加え、プロダクトライフサイクル(導入期、成長期、成熟期、衰退期)も意識して判断せよ。

リファクタリングは「やる or やらない」だけでなく、 「いつやる」「どこまでやる」といったグラデーションでもよい。

また、不要な機能の削除といったプロダクトのリファクタリングというPdMにしかできない仕事をやるのも大事。

ファイルシステム入門

ファイルとディレクトリ

UNIXではすべてをファイルとして扱うことで、統一的な操作を可能にしている。

実際には実体を持たない「ファイルのようなもの」もある(/dev/null/proc/cpuinfoなど)。

NTFSやAPFSといったファイルシステムは多くの種類が存在する。これは過去の経緯や最適化の結果である。

inodeはファイルやディレクトリのメタデータを格納する構造体で、inode番号というIDが振られる。 ディレクトリは、そのデータとして、ファイル名とinode番号のペアを格納している。 ファイル自体を格納しているわけではない点に注意。

だからこそ、同じinode番号を持つファイルを複数もつハードリンクが可能になる。 なお、循環参照を防ぐため、ディレクトリに対するハードリンクは作れない。

ファイルの種類はいくつかある。ls -lしたときの頭が種類を表す。

  • ディレクトリ d
  • キャラクタデバイス c (e.g. dev/null)
  • ブロックデバイス b (e.g. dev/sda)
  • ソケット s
  • シンボリックリンク l
  • FIFO p
  • 通常ファイル -

シンボリックリンクはリンクファイルという特殊なファイル。 inode番号を直接参照しないので、別のファイルシステムへの参照を持つことができる。

プロセスがファイルを使っていると、ディレクトリからは消えたけどinodeやファイルの実体は残っているという状態があり得る。 プロセスが終了した時点で初めて削除される。

ファイルの読み書き

まずファイルを開いて ファイルディスクリプタ(fd) を取得する。 これはファイルを識別するための整数である。 あるプロセスが開いているファイルは/proc/{PID}/fd/ディレクトリで確認できる。 用事が終わったら閉じる必要がある。

読み書き時にはページキャッシュという仕組みが動作する。 メモリ上とストレージ上のデータが異なる領域をDirty Pageと呼ぶ。 それが増えたり一定時間が経過したりすると、Dirty Pageが書き込まれる(flush)。 sync, syncfs, fsyncなどのシステムコールで明示的に書き込むことも可能。

遅延アロケーションは、ストレージの位置決めを遅延させることで効率化を図る仕組み。 現代のファイルシステムでは標準的。

マウントとは、あるディレクトリに別のファイルシステムを接続すること。 どこにでもマウントできるが/mnt/media配下にマウントするのが慣習。 /etc/fstabに必要な情報を書いておくと起動時に自動マウントされる。

通常のマウントが別のファイルシステムをマウントするのに対し、 bind mountという、ファイルシステム内の(一般的には)ディレクトリを 別のディレクトリからアクセス可能にする仕組みがある。 Dockerのボリュームマウントはこの仕組みで実現されている。 同じinode番号を降ることで実現される。

いろいろなファイルシステム

  • FAT32
    • USBメモリでよく使われるファイルシステム
    • 最小構成、シンプル、汎用的
    • 512Bのセクタを基本単位としつつ、4KB(8セクタ)や8KB(16セクタ)というクラスタ単位で領域管理する
    • FATテーブルと呼ばれる場所でクラスタチェーン(保存場所の連鎖)を管理する
    • ファイルもディレクトリも実体はクラスタ上にデータとして保存される
    • ディレクトリのデータはディレクトリエントリと呼ばれる
    • ディレクトリエントリにはファイルのメタデータが保存されている(ファイルのデータには、メタデータは保存されない点に注意)
  • ext4
    • Linuxで標準的なファイルシステム
    • 4KB(通常)のブロックが基本単位
    • extentという連続ブロック範囲の概念でファイル配置を効率的に管理
    • 変更操作を事前に「ジャーナル」と呼ばれる専用領域に記録しておく仕組みにより、事故からの回復が可能
    • ディレクトリはファイルと本質的に同じ扱いである
    • ファイルやディレクトリは inode というメタデータを格納する構造体で管理される
      • 権限、タイムスタンプ、データブロックへのポインタなどを含む
      • ファイル名やディレクトリ名など、名前は含まない
    • 名前はディレクトリエントリで管理される
      • エントリはディレクトリのデータブロックに羅列される
      • 配下のファイル等群のinode番号と名前がペアで保存される
  • OverlayFS
    • 複数のディレクトリを重ね合わせて統合ビューを提供する
      • 下位層(Lower): 読み取り専用のベースイメージ
      • 上位層(Upper): 書き込み可能な差分レイヤ
      • 作業層(Work): 一時的な作業領域
    • ファイル変更時はCopy-on-Write(CoW)により下位層から上位層にコピーして変更
    • コンテナでよく利用される

ストレージの種類

  • ブロックストレージ
    • 最も基本的な構成
    • NVM over FabricsやiSCSIなどで接続する
    • ボリュームを払い出してサーバへ提供する
    • ボリュームにはファイルシステムすら存在しない状態なのでフォーマットして使う
    • 複数サーバから同一ボリュームを参照するときはキャッシュを意識し、必要に応じてflushする必要あり
  • ファイルストレージ
    • NASやSMBなど
    • あらかじめフォーマットされているのでマウントだけして使う
  • オブジェクトストレージ
    • HTTPS経由で使う
    • 性能は低くなりがちな一方、ほぼ無限にデータを保存できる
    • バケット単位で管理

つまみぐい関数型プログラミング / パターンマッチ

問題は複数のパーツに分割すると簡単になる。 分割したパーツは、再び合成する必要がある。 合成するときに必要な糊となるのが、パターンマッチと高階関数である。

多くの関数型言語では、パターンマッチはswitch, match, caseなどの式で表現される。 プリミティブな値に対してだけではなく、Optionalな値や、オブジェクトや配列に対しても使用できる。

パターンマッチを使うと複雑な条件分岐を完結に記載できる。 また、例外の送出の可能性をなくすことができる。

さらに、Algebraic data types の Sum types(直和型、タグ付きユニオンなどともいう)と組わせると強力である。 複雑なデータ構造と処理を簡潔に表現できるようになるし、 網羅性を検証できるためうっかりミスがなくなる。

ドメイン解体新書 / 監視

クリティカルなドメインは、GitHub Actionの定期ジョブで以下などを監視しておくと安心かも

  • RDAP情報の監視
    • 登録者情報、権威サーバが正しいか。有効期間が切れそうになっていないか。
    • e.g. https://rdap.verisign.com/com/v1/domain/example.com
  • Certificate Transparency ログの監視
    • CT Logは認証局(CA)が発行したすべてのデジタル証明書を公開ログに記録する仕組み。 証明書の発行を監視可能にし、不正な証明書の検出を容易にするためのもの。
    • サブドメインの勝手な作成を検出するのにも有効
    • e.g. curl -s "https://crt.sh/?q=example.com&output=json"
  • 権威サーバのAレコード、MXレコードなどの監視
    • e.g. dig example.com NS
    • e.g. dig example.com MX