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
- e.g.